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”.

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: number | null;
  initialMessage: Message;
  assignedToUserId: User | null;
  tags: Array<Tag>;
  customer: Customer | null;
  wakeUpAt: string | null;
  summary: string | null;
  snoozedAt: string | null;
  lockedAt: string | null;
  ticketTypeId: string | null;
  ticketTypeFields: Record<string, string>;
  metadata?: Record<string, string>;
  followers?: {
    userId?: string;
    groupId?: string;
    entityId: string;
  }[];
  collaborators?: {
    collaboratorTypeId: string;
    userId?: string;
    groupId?: string;
    entityId: string;
  }[];
  files?: {
    id: string;
    name: string;
    size: number;
    filetype: string;
    mimetype: string;
  }[];
}

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;
  metadata?: {
    eventType?:
      | "autoresponder_replied"
      | "email_received"
      | "email_explanation_sent"
      | "widget_message_received"
      | "microsoft_teams_message_received"
      | "unthread_outbound"
      | "post_close_template_sent"
      | "customer_view_button"
      | "slack_bot_dm_received";
  };
}

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;
}

User Team

interface UserTeam {
  id: string;
  name: string;
  slackId: string;
}

Slack Channel

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

Simple Condition

interface SimpleCondition {
  leftValue?: any;
  leftValueFrom?: string;
  operator: "equals" | "notEquals" | "greaterThan" | "lessThan" | "greaterThanOrEquals" | "lessThanOrEquals" | "contains" | "notContains" | "in" | "notIn";
  rightValue?: any;
  rightValueFrom?: string;
}

For a “simple condition”, the left and right values are compared using the operator. For example, if the operator is “equals”, the left and right values are compared for equality. For left and right values, either a value can be provided directly or a reference to a field in the “context”, through the leftValueFrom and rightValueFrom fields.

The “context” depends on the object and the operation. For example, for ticket type field conditions, the context has the following shape:

{
  "conversation": {
    "ticketTypeFields": {
      "12345678-1234-1234-1234-123456789abc": "value-1",
      "42345678-1234-1234-1234-123456789abd": "value-2"
    }
  }
}

Therefore, a condition can be added on a ticket type field like this:

{
  "leftValueFrom": "conversation.ticketTypeFields['12345678-1234-1234-1234-123456789abc']",
  "operator": "equals",
  "rightValue": "value-1"
}

This will cause the field to be available for a given conversation if the ticket type has field 12345678-1234-1234-1234-123456789abc set to value-1.

Ticket Type Field

interface TicketTypeField {
  id: string;
  label: string;
  type: "short-answer" | "long-answer" | "multi-select" | "single-select" | "checkbox" | "user-select" | "multi-user-select";
  required: boolean;
  public: boolean;
  description: string;
  options?: string[];
  conditions?: {
    type: "simple";
    simple:
      | SimpleCondition
      | {
          and: SimpleCondition[];
        }
      | {
          or: SimpleCondition[];
        };
  };
}

For ticket type field conditions, the only context

Ticket Type

interface TicketType {
  id: string;
  name: string;
  projectId: string;
  description: string;
  createdAt: string;
  updatedAt: string;
  fields: Array<TicketTypeField>;
}

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;
}

Approval Request

interface ApprovalRequest {
  status: "approved" | "rejected" | "pending";
  id: string;
  tenantId: string;
  assignedToGroupId?: string;
  assignedToUserId?: string;
  statusChangedAt?: Date;
  conversationId: string;
  title?: string;
  notes?: string;
  createdAt: Date;
  approverUser?: User;
  approverGroup?: UserTeam;
}

Collaborator Type

interface CollaboratorType {
  id: string;
  name: string;
  key: string;
  description?: string;
}

Endpoints

Create Conversation

POST / conversations;

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

  • For type == "slack", the conversation will be for communicated in Slack. You can choose to post the details either into a DM or into a channel.
  • 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 continued via email replies. 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" | "slack";
  markdown: string;
  status: "open" | "in_progress" | "on_hold" | "closed";
  assignedToUserId?: string;
  customerId?: string;
  channelId?: string; // Only required if type == "slack". If not provided, the conversation will be continued through the Unthread app for Slack via a DM.
  projectId?: 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"
  ticketTypeId?: string;
  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 of 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
  • assignedToUserId
  • wakeUpAt
  • snoozedAt
  • excludeAnalytics
  • title
  • lockedAt
  • customerId
  • ticketTypeId
  • ticketTypeFields
  • submitterUserId
  • metadata

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 {
  body?: {
    type: "html" | "markdown";
    value: string;
  };
  blocks?: Block[];
  markdown?: string; // DEPRECEATED: Use 'blocks' and/or 'body' instead
  triageThreadTs?: string;
  isPrivateNote?: boolean; // Only applicable for email and in-app chat conversations
  isAutoresponse?: boolean; // Set this to true to treat this as a bot auto-response. Useful when responding to a conversation with your own AI bot.
  onBehalfOf?: {
    email?: string;
    name?: string;
    id?: string;
  };
}

Notes:

  • Either body, blocks or markdown must be provided.
  • If both blocks and body or 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.
  • For email and in-app chat conversations, set isPrivateNote to true to mark the message as a private note which will post to the Slack thread for the conversation without sending a response back to the customer.
  • File attachments can also be included. See the section on adding attachments when creating a conversation for more information.
  • If onBehalfOf is provided, the message will be sent as the given user. If not provided, the message will be sent as the Unthread bot.

Download File Attachment

GET /conversations/:conversationId/files/:fileId/full

This endpoint will return the full contents of a file attachment for a given conversation. The :fileId can be found in the files array of the conversation object.

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 and will create a Slack channel automatically if the createChannel flag is set to true.

interface CreateCustomerRequest {
  name: string;
  slackChannelId?: string;
  emailDomains?: string[];
  tagIds?: string[],
  isPrivate?: boolean;
  inviteUserIds?: string[];
  channelTopic?: string; // max 250 characters
  createChannel?: boolean; // to create a channel in Slack for this customer
}

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.

To return external users, pass in the includeExternalUsers field with a value of true.

Get User by ID

GET /users/:userId

This endpoint will return the given user.

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.

Create Approval Request for Conversation

POST /conversations/:conversationId/approval-requests

This endpoint will return the newly created approval request. Fields that can be set on create are: title, assignedToUserId and assignedToGroupId.

Get Approval Requests for Conversation

GET /conversations/:conversationId/approval-requests

This endpoint will return a list of approval requests for the given conversation.

Add Follower to Conversation

POST /conversations/:conversationId/add-follower

This endpoint will add a follower to the given conversation. The request body should contain the entityId of the follower to add and the entityType which is either “user” or “group”.

Remove Follower from Conversation

POST /conversations/:conversationId/remove-follower

This endpoint will remove a follower from the given conversation. The request body should contain the entityId of the follower to remove.

Assign Collaborator to Conversation

PUT /conversations/:conversationId/collaborators/:collaboratorTypeId

This endpoint will add a collaborator with the given collaborator type to the given conversation. The request body should contain the entityId of the collaborator to add and the entityType which is either “user” or “group”.

List Collaborator Types

POST / collaborator - types / list;

This endpoint will return a list of collaborator types.

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