The API is currently in beta. Contact us to get access.

Create a service account

In order to call the API you will first need to create a service account. Go to your Unthread dashboard and find the Service Accounts tab under Settings. Click “Add service account”, give your service account a name, and click “Submit”.

api-1.png

After creating the new service account, you will see a window displaying the service account’s API key. Copy this key and put it somewhere safe and secret. Once you close the window you will not be able to view the key again.

Calling the API

The URL for calls to the API will have the following form, with $ being replaced by the endpoint you are trying to call:

https://api.unthread.io/api/${endpoint}

In order to authenticate, you must send an HTTP header with the key “X-Api-Key” and the value set to the service account key created in the previous section. All request and response bodies will be in JSON.

API Conventions

The Unthread API follows RESTful conventions when possible, with most operations performed via GET, POST, PATCH, and DELETE requests. Request and response bodies are encoded as JSON.

List Endpoints

All list endpoints follow the same conventions. Requests are made in the following format:

interface Where<WhereField> {
  field: WhereField;
  operator: '==' | '!=' | '>' | '<' | 'in' | 'notIn' | 'contains' | 'notContains' | 'like';
  value: string | number | string[];
}

interface ListRequest<SelectField, OrderByField, WhereField> {
  select? Array<SelectField>;
  order?: Array<OrderField>;
  where?: Array<Where<WhereField>>;
  limit?: number;
  descending?: boolean;
  cursor?: string;
}

Accepted values for SelectField, OrderByField and WhereField depend on the object. Responses are made in the following format:

interface ListResponse<ObjectType> {
  data: Array<ObjectType>;
  totalCount: number;
  cursors: {
    hasNext: boolean;
    hasPrevious: boolean;
    next?: string;
    previous?: string;
  };
}

Responses are paginated by providing a cursor to the next or previous page. The limit per page is 100. An example list request is provided below:

// Request
// POST /conversations/list
{
  "select": [
		"id",
		"title",
		"initialMessage.ts",
		"initialMessage.text",
		"tags.id",
		"tags.name"
	],
  "order": ["createdAt", "id"],
  "where": [
    {
      "field": "customerId",
      "operator": "==",
      "value": "9YQjOSTFXeOPF8xNURFd"
    }
  ],
  "descending": true,
  "limit": 1,
  "cursor": "W3RydWUsWyIwNTU4MTc5Zi1kYTJkLTQxZjItOTZlYy1iZGY2YzkzZGQ1MjIiXV0="
}

// Response
{
  "data": [
    {
      "id": "0476bfd0-0c3f-4dfb-b462-d75a4a6908e1",
		  "title": "A bug report",
		  "initialMessage": {
        "ts": "1675183719.124879",
        "text": "Help, I found a bug"
      } ,
		  "tags": [
        {
          "id": "32561c55-0f01-47af-aa55-1a3d638f41e8",
          "name": "Bug"
        }
      ]
    }
  ],
  "totalCount": 688,
  "cursors": {
    "hasNext": true,
    "hasPrevious": true,
    "next": "W3RydWUsWyIwNDUzYmFiNi1iNDY3LTQ0MGMtYmMzZi1mNWEyODQ5YzlkYWUiXV0=",
    "previous": "W2ZhbHNlLFsiMDI4ZDg3NzMtY2I5Ni00MDQzLTlmYjYtMGZhOWUzYmFmY2MzIl1d"
  }
}

Create and Update Endpoints

Create and update endpoints can specify a select query param that takes a comma-separated list of fields to return in the response. Example below:

// Request
// PATCH /conversations/0476bfd0-0c3f-4dfb-b462-d75a4a6908e1?select=id,title,initialMessage.ts,initialMessage.text,tags.id,tags.name
{
  "status": "closed"
}

// Response
{
  "id": "0476bfd0-0c3f-4dfb-b462-d75a4a6908e1",
  "title": "A bug report",
  "initialMessage": {
    "ts": "1675183719.124879",
    "text": "Help, I found a bug"
  } ,
  "tags": [
    {
      "id": "32561c55-0f01-47af-aa55-1a3d638f41e8",
      "name": "Bug"
    }
  ]
}

Objects

Conversation

interface Conversation {
  id: string;
  status: "open" | "in_progress" | "on_hold" | "closed";
  customerId: string | null;
  channelId: string;
  wasManuallyCreated: boolean;
  friendlyId: number;
  createdAt: string;
  updatedAt: string;
  closedAt: string | null;
  statusUpdatedAt: string | null;
  responseTime: number | null;
  responseTimeWorking: number | null;
  resolutionTime: number | null;
  resolutionTimeWorking: number | null;
  title: string | null;
  priority: string | null;
  initialMessage: Message;
  assignedToUserId: User | null;
  tags: Array<Tag>;
  customer: Customer | null;
  wakeUpAt: string | null;
  summary: string | null;
}

Message

interface Message {
  ts: string;
  userId: string;
  userTeamId: string;
  botId: string;
  botName: string;
  text: string;
  subtype: string | null;
  conversationId: string;
  timestamp: string;
  threadTs: string | null;
  conversation: Conversation;
  user: User | null;
}

Customer

interface Customer {
  name: string;
  primarySupportAssigneeId: string | null;
  primarySupportAssigneeType: "user" | "team" | null;
  secondarySupportAssigneeId: string | null;
  secondarySupportAssigneeType: "user" | "team" | null;
  replyTimeoutMinutes: number | null;
  defaultTriageChannelId: string | null;
  disableAutomatedTicketing: boolean | null;
  botHandling: "off" | "all" | null;
  autoresponder: AutoresponderOptions | null;
  supportSteps: Array<SupportStep> | null;
  slackTeamId: string | null;
  assignToTaggedUserEnabled: boolean | null;
  slackChannelId: string | null;
  createdAt: string;
  updatedAt: string;
  tags: Array<Tag>;
  slackChannel: SlackChannel | null;
}

Tag

interface Tag {
  id: string;
  name: string;
}

User

interface User {
  id: string;
  name: string;
  email: string;
  slackId: string;
  photo: string;
}

Slack Channel

interface SlackChannel {
  id: string;
  name: string;
  isShared: boolean;
  isPrivate: boolean;
}

Autoresponder Options

interface AutoresponderOptions {
  enabled: boolean;
  condition: "always" | "outside-working-hours";
  message: string;
}

Support Step

interface SupportStep {
  type: "assignment" | "escalation" | "reminder" | "triage";
  assigneeId?: string;
  assigneeType?: "user" | "team";
  minutes: number;
  shouldCycleThroughTeamMembers?: boolean;
  maxCycles?: number;
  cycleMinutes?: number;
}

Outbound

interface Outbound {
  id: string;
  status: "scheduled" | "draft" | "sent" | "failed";
  deliveryMethod: "slack";
  subject: string;
  // See Slack Block Kit documentation for details on the blocks field: https://api.slack.com/block-kit
  blocks: Array<SlackBlock>;
  recipients: {
    type: "customer";
    id: string;
  }[];
  sendAs: {
    // For type == "support-rep" the ID is the user ID to use as a default if there is no support rep for a given customer.
    type: "user" | "support-rep";
    id: string;
  };
  runAt: string;
}

Webhook Subscription

interface WebhookSubscription {
  id: string;
  url: string;
  enabled: boolean;
}

Endpoints

Create Conversation

POST /conversations;

This endpoint will create and return a new conversation. Behavior will differ depending on the type field:

  • For type == "triage", the conversation will be for internal discussion only and there will be no way to communicate with the customer. The triageChannelId field is required.
  • For type == "email", the conversation will be for communication with the customer. Replies in the thread with /unthread send appended will be sent to the customer via email. The emailInboxId field is required. The onBehalfOf field is used to specify the email address and name of the end user to communicate with.

If customerId is provided, the conversation will be linked to the given customer and that customer’s autoresponder and SLA settings will be applied.

Request schema:

interface CreateConversationRequest {
  type: "triage" | "email";
  markdown: string;
  status: "open" | "in_progress" | "on_hold" | "closed";
  assignedToUserId?: string;
  customerId?: string;
  priority?: 3 | 5 | 7 | 9;
  triageChannelId?: string; // Only required if type == "triage"
  notes?: string;
  title?: string;
  excludeAnalytics?: boolean;
  emailInboxId?: string; // Only required if type == "email"
  onBehalfOf?: { // Only required if type == "email". Either email or id must be provided.
    email?: string;
    name?: string;
    id?: string;
  };
}

Attachments:

Files can be attached to the conversation by making a multipart/form-data request instead af a JSON request. The JSON payload should be included as a form field named json and the files should be included as additional form fields with the name attachments and the file as the value. Up to 10 files can be attached to a single conversation and the maximum file size is 20MB. See tho following Javascript example for clarification:

const formData = new FormData();

formData.append('json', JSON.stringify({
  type: 'email',
  markdown: 'Hello, world!',
  status: 'open',
  onBehalfOf: {
    email: '[email protected]',
    name: 'Example User'
  },
  emailInboxId: '12345678-1234-1234-1234-123456789abc'
}));

formData.append('attachments', file1);
formData.append('attachments', file2);

await fetch('https://api.unthread.io/api/conversations', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'Bearer YOUR_API_KEY`
  },
  body: formData
});

Notes:

  • Markdown should follow the Slack formatting guide.
  • For more info on multipart/form-data requests, see the IEFT RFC 7578.

Get Conversation by ID

GET /conversations/:conversationId

This endpoint will return the given conversation.

Update Conversation

PATCH /conversations/:conversationId

This endpoint will return the updated conversation. Fields that can be updated are status, priority, notes, wakeUpAt, and assignedToUserId.

List Conversations

POST /conversations/list

This endpoint will return a list of conversations.

List Messages for Conversation

POST /conversations/:conversationId/messages/list

This endpoint will return a list of messages for a given conversation.

Create Message in Conversation

POST /conversations/:conversationId/messages

This endpoint will post a new message into a Slack thread for a conversation.

Sample request:

interface Block {
  type: "section" | "survey";
  text?: {
    text: string;
    type: "mrkdwn" | "plaintext";
  };
  survey_id?: string;
}

interface CreateMessageRequest {
  markdown?: string;
  blocks?: Block[];
  triageThreadTs?: string;
}

Notes:

  • Either blocks or markdown must be provided
  • If both blocks and markdown are provided, the blocks will be appended after the markdown content
  • Only pass in triageThreadTs if you want the message to be posted to a triage thread for that conversation rather than being sent to the customer

Get Customer by ID

GET /customers/:customerId

This endpoint will return the given customer.

Update Customer

PATCH /customers/:customerId

This endpoint will return the updated customer. Fields that can be updated are: name, slackChannelId, autoresponder, supportSteps, defaultTriageChannelId, disableAutomatedTicketing, primarySupportAssigneeId, primarySupportAssigneeType, secondarySupportAssigneeId, secondarySupportAssigneeType, replyTimeoutHours, botHandling, emailDomains.

Create Customer

POST /customers

This endpoint will return the newly created customer. Fields that can be updated are set on create are: name, slackChannelId, emailDomains

Delete Customer

DELETE /customers/:customerId

This endpoint will delete the given customer.

List Customers

POST /customers/list

This endpoint will return a list of customers.

List Users

POST /users/list

This endpoint will return a list of users who are a member of your Slack workspace and have been synced into Unthread.

Get Tag by ID

GET /tags/:tagId

This endpoint will return the given tag.

Update Tag

PATCH /tags/:tagId

This endpoint will return the updated tag. Fields that can be updated are: name.

Create Tag

POST /tags

This endpoint will return the newly created tag. Fields that can be updated or set on create are: name

Delete Tag

DELETE /tags/:tagId

This endpoint will delete the given tag.

List Tags

POST /tags/list

This endpoint will return a list of tags.

Add Tag to Customers

POST /tags/:tagId/customers/create-links

This endpoint takes an array of customer IDs and will assign the given tag to all the specified customers.

Sample request body: [ '93ba5298-a23f-413b-94fe-73e0ab26a816', '7ab46970-19ec-492e-90fe-47a8abc93fec' ]

Add Tag to Conversations

POST /tags/:tagId/conversations/create-links

This endpoint takes an array of conversation IDs and will assign the given tag to all the specified conversations.

Sample request body: [ 'ad00e2d0-4ea4-45c4-9b5d-8728f93e51a7', 'e82cde04-0d59-4416-b5da-1f405f6d4ddb' ]

Remove Tag from Customers

POST /tags/:tagId/customers/delete-links

This endpoint takes an array of customer IDs and will remove the given tag from all the specified customers.

Remove Tag from Conversations

POST /tags/:tagId/conversations/delete-links

This endpoint takes an array of conversation IDs and will remove the given tag from all the specified conversations.

Create and/or send an outbound message

POST /outbounds

This endpoint will create an outbound and send it, unless the status is set to “draft”. Drafts can be sent later by updating the status to “scheduled”.

Update and/or send an outbound message

PATCH /outbounds/:outboundId

This endpoint will update an outbound. Drafts can be sent by updating the status to “scheduled”.

List outbounds

GET /outbounds;

This endpoint will list all outbounds.

Get outbound by ID

GET /outbounds/:outboundId

This endpoint will return the given outbound.

Create Webhook Subscription

POST /webhook-subscriptions

This endpoint will return the newly created webhook subscription. Fields that can be set on create are: url and enabled.

Update Webhook Subscription

PATCH /webhook-subscriptions/:webhookSubscriptionId

This endpoint will return the updated webhook subscription. Fields that can be updated are: url and enabled.

Delete Webhook Subscription

DELETE /webhook-subscriptions/:webhookSubscriptionId

This endpoint will delete the given webhook subscription.

List Webhook Subscriptions

GET /webhook-subscriptions

This endpoint will return a list of webhook subscriptions.

Reporting

POST /reporting/time-series;

This endpoint will return metrics about your conversations. Requests are made in the following format:

type Metric = "totalCount" | "responseTimeMean" | "responseTimeWorkingMean" | "resolutionTimeMean" | "resolutionTimeWorkingMean" | "conversationsResolved";
type DateDimension = "day" | "week" | "month";
type Dimension = "assigneeId" | "customerId" | "tagId" | "sourceType";

interface ReportingTimeSeriesRequest {
  timezone?: string;
  startDate: string;
  endDate: string;
  dateDimension?: DateDimension;
  metric: Metric;
  dimensions?: Dimension[];
  filters?: {
    [dim: Dimension]: string | string[];
  };
}
  • The timezone field is optional and defaults to “UTC”.
  • starDate and endDate have the following format: “2022-12-31”. The dates are inclusive.
  • dateDimension specifies in what time period the data should be grouped by.
  • All metrics are numbers. Response and resolution times are in seconds.
  • dimensions is an array of fields to group the data by in addition to date.
  • filters allows you to filter the data to specific dimensions. If an array is passed for a given dimension then it is treated as an OR.

Responses are returned in the following format:

type Dimension = "assigneeId" | "customerId" | "tagId" | "sourceType";

type DataSplit = {
  [key: string]: DataPoint;
};
type DataPoint = number | DataSplit;

interface ReportingTimeSeriesResponse {
  data: DataSplit;
  dates: string[];
  lookups: {
    [dim: Dimension]: {
      [key: string]: string;
    };
  };
}
  • If there is no data for a particular day then that day will not be included in the data object, however it will be included in the dates array.
  • If dimensions are included in the request, then the data will be nested for each dimension included. Dimensions are nested in the order they are included in the request. See the example below for clarification.
  • dates is an array of the time periods covered by the query. For grouping on weeks, each value in the array is the first day of the week starting on Monday. For grouping on months, each value in the array is the first day of the month.
  • lookups contains friendly names for the keys contained in data. For example, if grouping by customerId, the lookups would contain a mapping of the customer ID to the customer name.

An example request and response is included below:

// Request
// POST /reporting/time-series
{
  "dateDimension": "week",
	"startDate": "2022-12-01",
	"endDate": "2022-12-31",
	"metric": "totalCount",
	"timezone": "America/New_York",
	"filters": {
		"assigneeId": [
			"63335fa9-ffcc-4103-8905-e4440cc7c7d4",
			"2e8dab53-a7a2-4e1f-b9d9-66018b98365d"
		]
	},
	"dimensions": ["customerId", "sourceType"]
}

// Response
{
   "lookups":{
      "customerId":{
         "9YQjOSTFXeOPF8xNURFd": "Customer 1",
         "AWEpBY0uqxfp73HaTVS4": "Customer 2",
				 "aZHO2af1RFa92QiOq3Y4": "Customer 3"
      },
      "sourceTypes":[]
   },
   "dates":[
      "2022-11-28",
      "2022-12-05",
      "2022-12-12",
      "2022-12-19",
      "2022-12-26"
   ],
   "data":{
      "9YQjOSTFXeOPF8xNURFd":{
         "slack":{
            "2022-11-28":19,
            "2022-12-12":70,
            "2022-12-19":17
         },
				 "widget":{
            "2022-11-28":6,
            "2022-12-12":1
         }
      },
      "AWEpBY0uqxfp73HaTVS4":{
         "email":{
            "2022-12-31":1
         },
				 "slack":{
            "2022-11-28":6,
            "2022-12-12":1
         }
      },
      "aZHO2af1RFa92QiOq3Y4":{
         "slack":{
            "2022-11-28":6,
            "2022-12-12":1
         }
      }
   }
}