# Attachments
How to upload attachments programmatically for messages and events in Plain.
This page outlines how to upload attachments programmatically.
At a high level to upload attachments you:
* Make an API call to get an an upload url and some metadata
* You then upload your file, and metadata to that upload url.
* Use the ID of the attachment you uploaded in other API calls (e.g. create a thread or send an email).
## Step by step guide
To try this, you will need an [API key](/api-reference/graphql/authentication/) with the following permission:
* `attachment:create`
* `fileName` is the name under which the attachment will appear in the timeline, you can use whichever you want
* `fileSizeBytes` is the exact size of the attachment in bytes (specifically, `32318` bytes is the size of Bruce's picture above)
* `c_XXXXXXXXXXXXXXXXXXXXXXXXXX` is the customer id you are uploading the attachment for. **Remember to replace this!**
Which would console log something like this:
The GraphQL mutation to create an attachment upload URL is the following:
In the `AttachmentUploadUrl` we created in the previous step we get back 2 fields which are needed to actually upload our attachment:
* `uploadFormUrl`: The URL to which to upload the file to
* `uploadFormData`: A list of key, value pairs that have to be included in the data we upload along with the actual file data.
With this information we can now upload our actual file to Plain. To do this we need to build a form (`multipart/form-data`) with the data contained in `uploadFormData` and submit it to the `uploadFormUrl`.
Here is some example code showing how you would do this in the Browser and from a Node server:
## Limitations
* On emails and custom timeline entries:
* A maximum of **25 attachments** can be added
* All attachments **combined** can not exceed **6MB** in size
* The following file extensions are not allowed as attachments:
`
bat, bin, chm, com, cpl, crt, exe, hlp, hta, inf, ins, isp, jse, lnk, mdb, msc, msi, msp, mst, pcd, pif, reg, scr, sct, shs, vba, vbe, vbs, wsf, wsh, wsl`
* Attachments uploaded, but never referenced by a Custom Timeline Entry or email, will be
**deleted after 24 hours**.
* Upload URLs are only **valid for 2 hours** after which a new URL needs to be created.
# Customer cards
Live context straight from your own systems when helping customers.
Customer cards are a powerful feature in Plain that let you show information from your own systems while looking at a customer or thread in Plain. This makes sure you always have important context when helping customers.
Customer cards are configured on Plain (see how to [how to create one](/api-reference/customer-cards/create-a-customer-card)) and requested by Plain from your APIs.
## High-level flow
For a more detailed description of the protocol, check out the [full
spec](/api-reference/customer-cards/protocol).
1. A thread is viewed in Plain.
2. Plain fires a POST request to your API with:
* The thread customer's `email`, `id` and, if set, `externalId`
* The thread's `id` and, if set, `externalId`.
* The configured customer card `key`s
3. Your API responds with the JSON for each card
4. Cards are shown to the user in Plain.
Based on your customer card settings, Plain will send a request to your API like the below example:
Your API should then reply with a list of cards matching the requested keys where each card contains the components you want to display:
## UI Components
To define what each customer card should look like, you use the Plain UI components. All the components are documented in the [Plain UI Components](/api-reference/ui-components/) section.
You can find example customer cards and an example API you can check out [team-plain/example-customer-cards](https://github.com/team-plain/example-customer-cards). Feel free to try these out in your workspace!
## Example cards
To demonstrate what you can build with customer cards we've built some examples you can view and which are open source.
[**Customer cards Examples →**](https://github.com/team-plain/example-customer-cards)
## Playground
The UI components playground lets you build and preview the component JSON needed to create a customer card. Use this to prototype a customer card before starting to build your integration.
[**Playground →**](https://app.plain.com/developer/ui-components-playground/)
# Create a customer card
Define the details of the customer card.
To create a customer card head to **Settings** → **Customer cards** and enter the following details:
* **Title**: this will be displayed as the title of the card so even if the card fails to load users know which card is
errored.
* **Key**: the link between this config and your API. A key can only contain alphanumeric, hyphen, and underscore characters (regex: `[a-zA-Z0-9_-]+`)
* **Default time to live (seconds)**: by default how long Plain should cache customer cards. The minimum is 15 seconds, maximum is 1 year in seconds (31536000 seconds).
* **URL**: the URL of your API endpoint that will be built to return customer cards. It must start with `https://`.
* **Headers (optional)**: the headers Plain should pass along when making the request. While this is optional it is
**highly recommended** to add authorization headers or other tokens that authenticate the request as your API may be
returning customer data.
To get you started quickly, we've created a few example customer cards that you can configure and see how they look in your application. All example cards are available in our open-source repository:
[team-plain/example-customer-cards](https://github.com/team-plain/example-customer-cards)
Here is one you can try right now:
* Title: e.g. "Usage"
* Key: `usage`
* Default time to live: `120`
* URL: [https://example-customer-cards.plain.com/](https://example-customer-cards.plain.com/)
# Examples
# Playground
# Protocol
Learn how we request customer cards from your API and how to respond to these requests.
This page is intended for a technical audience that will be implementing a customer card API.
Check out the [customer cards](/customer-cards) page for an overview of customer cards.
Customer cards are not proactively loaded. They are just-in-time and pulled when required.
This means that if your APIs are slow then users of the Support App will see a loading spinner over the card.
The protocol is as follows:
1. When a user in Plain opens up a customer's page the cards are loaded.
2. Plain's backend figures out which cards can be returned from the cache and which cards need to be loaded. On the first
load of the customer this would be all cards.
3. It calculates how many requests it needs to make (see [request deduplication](#request-deduplication) for
details).
4. Your APIs are then called with the customer's details, so you can look up the customer's data in your systems
(see [request](#request) section for details).
5. Your APIs then return customer cards that consist of [Plain UI components](/api-reference/ui-components)
(see [response](#response) section for details).
6. The cards are cached based on either an explicit TTL value in the response or the TTL in the card settings (see [caching](#caching)).
7. Cards are shown to the user in Plain.
8. Users can manually reload the card at any time in which case only that one card will be requested from your API.
A **few limits** to be aware of:
* Your API must **respond within 15 seconds**, or it will time out. See [retry strategy](#retry-strategy) for details on how timed-out requests are retried.
* You can configure a **maximum of 25 customer cards per workspace**.
* **Card keys must be unique within a workspace**. A key can only contain **alphanumeric**, **hyphen**, and **underscore** characters (regex: `[a-zA-Z0-9_-]+`).
## Request
Plain will make the following request to your backend:
* **Method**: `POST`
* **URL:** the URL you configured on customer cards settings page.
* **Headers:**
* All the headers you provided on customer cards settings page. This should typically include authentication headers.
* `Content-Type`: `application/json`
* `Accept`: `application/json`
* `Plain-Workspace-Id`: the ID of the workspace the customer is in. This is useful for logging or request routing.
* `User-Agent`: `Plain/1.0 (plain.com; help@plain.com)`
* `Plain-Request-Signature`: `XXX` (see [request signing](/api-reference/request-signing) for details)
* **Body:**
* `cardKeys`: an array of card keys being requested
* `customer`: an object with the customer's core details
* `id`: the id of the customer in Plain
* `email`: the email of the customer
* [`externalId`](https://www.plain.com/docs/api-reference/graphql/customers/upsert) (optional): string if the customer has an `externalId`, otherwise it is `null`.
* `thread` (optional): an object with the thread's details, if this customer card is being requested in the context of a thread
* `id`: the id of the thread in Plain
* `externalId` (optional): string if the thread has an `externalId`, otherwise it is `null`.
Example request body:
### Request deduplication
If you configure multiple customer cards that have the same API details then Plain will batch them and make only one request.
The request deduplication logic for customer card configs is:
* The following config properties are ignored: Title, Card key, Default TTL
* **API URL:** Leading and trailing whitespaces are trimmed and then compared. **This is case sensitive**.
* For example, these URLs would be considered **different**:
* `https://api.example.com/cards`
* `https://api.example.com/cards/`
* `https://api.example.com/Cards`
* **API Headers:** Order of headers does not matter
* **Header name:** Leading and trailing whitespaces are trimmed and then compared. **This is case insensitive**.
* For example, these header names be considered **the same**:
* `Authorization`
* `AUTHORIZATION`
* ` authorization `
* **Header value:** No processing done, compared as is (be careful with any extra whitespace characters)
* For example, these header values would be considered **as different**:
* `Bearer my-token`
* `bearer my-token`
* ` bearer my-token `
## Response
For each key requested a corresponding card **MUST** be returned in the response, otherwise an integration error will be returned for that card.
Any extra cards in the response will be ignored.
Your API must respond with a **`200` status code** or the response body won't be processed and will be treated as an error.
The response body must be a JSON object with:
* `cards`: an array of cards. Every `cardKey` requested should have a corresponding `key` returned. Any extra returned
cards will be ignored.
* `key`: the requested key
* `timeToLiveSeconds` (optional, nullable): can either be omitted or `null`. If provided it will override the default time to live value. This allows you to control caching on a case-by-case basis.
* `components` (nullable): `null` to indicate that the card has no data or an array of [Plain UI Components](/api-reference/ui-components/).
Example response body for a card cached for 1 hour:
Example response body for a card that has no data and should not be displayed and TTL omitted:
## Caching
We cache the responses we get from your APIs. This cache is controlled via two properties:
1. A time to live value (in seconds) in the customer card's settings. This can be changed under **Settings** → **Customer cards**. Any changes here will only apply to newly loaded customer cards.
2. An explicit time to live value (in seconds) in your API response with the key `timeToLiveSeconds`. This overrides the value from settings and allows your API to dynamically set the TTL using custom logic.
Any card that is past its expiry time will usually be deleted within a few minutes but no later than 48 hours after expiry.
## Retry strategy
Errors are classified into two categories:
1. **Retriable errors**: these are transient issues where retrying once is appropriate
2. **Integration errors**: these are typically programming or configuration errors. These errors won't be retried and cached for 5 minutes.
## Security
Plain supports [request signing](/api-reference/request-signing) and [mTLS](/api-reference/mtls) to verify that the request was made by Plain and not a third party.
### Retriable errors
The following errors are **retried once** after a **1-second delay**:
* HTTP `5xx` response status code
* HTTP `429` Too Many Requests response status code
* The request times out after 15 seconds.
* Plain fails to perform the request for some reason
Retriable errors are not cached, therefore if the cards are requested again via the Support App they will be re-requested.
### Integration errors
The following errors are **not retried**:
* All HTTP 4xx response status codes except for HTTP `429` Too Many Requests response status code
* A card key is missing in the response. For example, if `subscription-details` is requested but the `cards` array in the response doesn't have an element with the key `subscription-details`.
* The response body does not match the expected schema documented in [response](#response).
Integration errors are cached for 5 minutes and usually indicate a programming or configuration error.
Users can manually refresh a card in the UI, in which case the card will be requested again.
# API Explorer
# Authentication
Machine Users can have multiple API Keys to make it easy to rotate keys. Every API key also has fine grained permissions.
Go to **Settings** → **Machine Users** and click "Add Machine User"
A Machine User has two fields:
* **Name:** This is just visible to you and could indicate the usage e.g. "Autoresponder"
* **Public name:** This is the name visible to customers (if the Machine User interacts with customers) e.g. "Mr Robot"
Click "Add API Key" and select the permissions you need. When making API calls, if you have insufficient permissions, the error should tell you which permissions you need.
The relevant documentation will tell you which permissions are required for each feature.
Once you've made an API key you should copy it and put it somewhere safe, as you will not be able to see it again once you navigate away.
That's it! Now that you have an API key you can use this with our SDKs or within any API call as a header:
```text
Authorization: Bearer plainApiKey_xxx
```
# Companies
Within Plain every customer can belong to one company. The company is infered automatically using the customer's email address. For example if their email address ends with "@nike.com" then their company will be automatically set to "Nike".
Companies allow you to prioritise and filter your support requests.
Additionally [tiers and SLAs](/tiers/) can be associated with a company.
# Get companies
You can get all companies you've interacted with in your workspace using the `companies` query. This endpoint supports [Pagination](/api-reference/graphql/pagination).
For this query you need the following permissions:
* `company:read`
# Update customer company
Plain automatically derives a customer's company for you, but you can also update it manually.
The customer in question is identified by their id (ie `c_...`).
With regards to the company, you can either specify an existing company using the ID we've generated (ie `co_...`), or pass the company domain, which we'll use to derive the rest of the company's info.
If you wish to only remove the customer's associated company, then you can pass `null` as the `companyIdentifier`.
For this mutation you need the following permissions:
* `customer:edit`
# Customers
Customers that reach out to you will automatically be created in Plain without requiring any API integration.
However, using our API to manage customers proactively can be helpful when you are optimizing your support workflow.
For example:
* You can [**put customers into groups**](/api-reference/graphql/customers/customer-groups/) to better organize your support queue. For example, you could group customers by pricing tier (e.g. Free Tier, Teams, Enterprise)
* You can [**create customers**](/api-reference/graphql/customers/upsert/) in Plain when they sign-up on your own site so that you can reach out to them proactively without waiting for them to get in touch.
* You can [**save your own customer's ID**](/api-reference/graphql/customers/upsert) for use with [**customer cards**](/customer-cards/).
# Customer groups
Customer groups can be used to group and segment your customers. For example you could organise your customers by their tier "Free", "Growth, "Enterprise" or make use of groups to keep track of customers trialing beta features.
Customers can belong to one or many groups. You can filter customer threads by group, allowing you to quickly focus on a subset of them.
This guide will show you how to add customers to groups using the API. You can also do this with the UI in Plain if you prefer.
This guide assumes you've already created some customer groups in **Settings** → **Customer Groups**.
## Add a customer to groups
A customer can be added to a customer group using the `addCustomerToCustomerGroup` mutation.
Depending on what your customer groups are you may want to call this API at different times. For example if you are grouping them by their pricing tier you will want to do this every time their tier changes.
This operation requires the following permissions:
* `customer:create`
* `customer:edit`
Running the above would console.log:
If you prefer you can also use the customer group id instead of the key. You can do this like so:
## Remove a customer from groups
A customer can be removed from a customer group by using the `removeCustomerFromGroup` mutation.
Which if successful will console.log `null`
If you prefer you can also use the customer group id instead of the key. You can do this like so:
# Delete customers
You can delete customers with the `deleteCustomer` API, you will find this name in both our API and our SDKs.
To delete a customer you will need the customer's ID from within Plain. You can get this ID in the UI by going to a thread from that customer and pressing the 'Copy ID' button from the customer details panel on the right, or via our [fetch API](/api-reference/graphql/customers/get).
Deleting a customer will trigger an asynchronous process which causes all data (such as threads) associated with that customer to be deleted.
This operation requires the following permissions:
* `customer:delete`
# Fetch customers
We provide a number of methods for fetching customers:
1. [Get customers](#get-customers) (To fetch more than one customer at a time)
2. [Get customer by ID](#get-customer-by-id)
3. [Get customer by email](#get-customer-by-email)
All of these endpoints require the following permissions:
* `customer:read`
## Get customers
Our API allows you to fetch customers as a collection using `getCustomers` in our SDKs or the `customers` query in GraphQL. In both cases this endpoint supports [Pagination](/api-reference/graphql/pagination).
This is a very flexible endpoint which supports a variety of options for filtering and sorting, for full details try our [API explorer](https://app.plain.com/developer/api-explorer/) or [Typescript SDK](https://github.com/team-plain/typescript-sdk/).
## Get customer by ID
If you already have the ID of a customer from within Plain or one of our other endpoints you can fetch more details about them using `getCustomerById` in our SDKs or the `customer` query in GraphQL.
## Get customer by email
To fetch a customer by email you can use `getCustomerByEmail` in our SDKs or the `customerByEmail` query in GraphQL.
# Upserting customers
Learn how to create and update customers programmatically.
Creating and updating customers is handled via a single API called `upsertCustomer`. You will find this name in both our API and our SDKs.
When you upsert a customer, you define:
1. The identifier: This is the field you'd like to use to select the customer and is one of
* `emailAddress`: This is the customer's email address. Within Plain email addresses are unique to customers.
* `customerId`: This is Plain's customer ID. Implicitly if you use this as an identifier you will only be updating the customer since the customer can't have an id unless it already exists.
* `externalId`: This is the customer's id in your systems. If you previously set this it can be a powerful way of syncing customer details from your backend with Plain.
2. The customer details you'd like to use if creating the customer.
3. The customer details you'd like to update if the customer already exists.
When upserting a customer you will always get back a customer or an error.
## Upserting a customer
This operation requires the following permissions:
* `customer:create`
* `customer:edit`
* `customerGroup:read` (Typescript SDK only)
* `customerGroupMembership:read` (Typescript SDK only)
This will:
* Find a customer with the email '[donald@example.com](mailto:donald@example.com)' (the identifier).
* If a customer with that email exists will update it (see `onUpdate` below)
* Otherwise, it will create the customer (see `onCreate` below)
Running the above would console.log:
The GraphQL mutation is the following:
The value of the `result` type will be:
* `CREATED`: if a customer didn't exist and was created
* `UPDATED`: if a customer already existed AND the values being updated **were different**.
* `NOOP`: if a customer already existed AND the values being updated **were the same**
# Error codes
If you receive an error code as part of an API call, this is where you can look up what it means
#### `input_validation`
The provided input failed validation. See field errors for details.
#### `forbidden`
Permission denied.
#### `internal`
An internal server error. The request should be retried. If the error persists, please get in touch at [help@plain.com](mailto:help@plain.com)
#### `not_found`
An entity referenced in the request is not found. For example trying to create an issue for a customer that doesn't exist.
#### `not_yet_implemented`
The API is not yet implemented. If you think it should already be implemented please get in touch at [help@plain.com](mailto:help@plain.com)
***
#### `action_not_allowed_in_demo_workspace`
The performed action is not allowed for a demo workspace.
#### `attachment_file_size_too_large`
The attachment being uploaded exceeds the limit (6MB)
#### `attachment_file_type_not_allowed`
The file type is not allowed. Banned file types: `bat`, `bin`, `chm`, `com`, `cpl`, `crt`, `exe`, `hlp`, `hta`, `inf`, `ins`, `isp`, `jse`, `lnk`, `mdb`, `msc`, `msi`, `msp`, `mst`, `pcd`, `pif`, `reg`, `scr`, `sct`, `shs`, `vba`, `vbe`, `vbs`, `wsf`, `wsh`, `wsl`
#### `attachment_not_uploaded`
The attachment ID being referenced was created, but not uploaded. Upload the attachment and try again.
#### `cannot_assign_customer_to_user`
The user that the customer is being assigned to doesn't have a role that is capable of helping the customer. Assign the "Help customers" role to the user and try again.
#### `cannot_remove_only_admin_user`
Can't remove the last user with an admin role. Assign another user the admin role as well and try again.
#### `cannot_reply_to_unsent_email`
The email being replied to has yet to be sent. Wait until the email is sent and try again.
#### `cannot_update_field`
Some Custom Timeline Entry fields can't be updated but only created (such as `timestamp`). Delete the Custom Timeline Entry and recreate it if you want to update these fields.
#### `customer_already_exists_with_email`
A customer with this email already exists in the workspace and can't be created again.
#### `customer_already_exists_with_external_id`
A customer with this external id already exists in the workspace and can't be created again.
#### `customer_already_is_status`
An attempt to change a customer to a status that the customer already is was made.
#### `customer_already_marked_as_spam`
The customer has already been marked as spam and can't be marked as spam again.
#### `customer_card_config_key_already_exists`
A Customer Card config with this key already exists in the workspace and can't be created again.
#### `customer_is_marked_as_spam`
An action was attempted but cannot be performed as the customer is marked as spam.
#### `customer_is_not_marked_as_spam`
An action was attempted but cannot be performed as it requires the customer to be marked as spam.
#### `customer_status_cannot_be_changed_to_idle`
The customer's status cannot be changed to idle, see error for reason.
#### `customer_jwt_expired`
The customer's JWT has expired. Recreate the JWT and try again.
#### `customer_jwt_invalid`
The customer's JWT is in an invalid format. Fix the contents of the JWT and try again.
#### `customer_group_has_memberships`
The customer group has memberships and can't be deleted.
#### `customer_group_key_already_exists`
A customer group with this key already exists in the workspace and can't be created again.
#### `customer_session_challenge_invalid`
The provided customer challenge digits are invalid.
#### `domain_already_taken`
The domain in the support email address is already taken by a different workspace. Currently only one workspace can use a domain. If this is an issue, please contact [help@plain.com](mailto:help@plain.com).
#### `domain_cannot_be_public`
The domain in the support email address is considered public and cannot be used.
#### `insufficient_permissions`
The user doesn't have the required permissions to create an API key that has more permissions than the user itself.
#### `issue_already_open`
Issue is already in an open state and can't be opened.
#### `issue_already_resolved`
Issue is already in a resolved state and can't be resolved.
#### `issue_already_this_issue_type`
Issue is already the provided issue type and can't be changed to it.
#### `issue_already_this_priority`
Issue is already the provided priority and can't be changed to it.
#### `linear_issue_already_linked_to_issue`
Issue is already linked to the provided Linear issue and cannot be linked again.
#### `linear_organisation_cannot_be_authorised`
The Linear OAuth flow failed. Detailed reasoning is included in the error.
#### `mark_as_read_user_must_be_assigned_to_customer`
The user trying to mark the timeline as read isn't the user that is assigned to the customer. Assign the user to the customer and try again.
#### `roles_at_least_one_admin_required`
Can't remove the role for the user as it would leave no users with the admin role in the workspace. Assign another user the admin role as well and try again.
#### `too_many_customer_card_configs`
The maximum number of Customer Card configs has been reached for this workspace.
#### `too_many_webhook_targets`
The maximum number of webhook targets has been reached for this webhook.
#### `user_account_already_exists`
A User Account already exists for the current user.
#### `user_already_this_status`
User is already in this status can can't be changed to it.
#### `user_linear_integration_not_found`
A User does not have a Linear integration setup.
#### `workspace_app_key_not_found`
The workspace app key can't be found.
#### `workspace_app_public_key_required`
A workspace app key must be provided.
#### `workspace_app_required`
A workspace app must be provided.
#### `workspace_chat_not_enabled`
Chat is disabled so chat messages can't be sent.
#### `workspace_email_domain_not_configured`
Email domain settings aren't fully configured yet. Double check the email settings page.
#### `workspace_email_domain_not_set`
A support email is not configured in the email settings. Double check the email settings page.
#### `workspace_email_forwarding_not_configured`
Email forwarding settings aren't fully configured yet. Double check the email settings page.
#### `workspace_email_not_enabled`
Email is not enabled so emails can't be sent.
#### `workspace_invite_already_accepted`
The invite has already been accepted by the user.
#### `workspace_invite_email_already_invited`
The email trying to be invited already has an outstanding invite.
#### `workspace_invite_email_already_member_of_workspace`
The email trying to be invited is already a member of the workspace.
#### `workspace_invite_email_doesnt_match`
The user trying to accept the invite has a different email than the invite is for.
#### `workspace_support_email_address_conflict`
The entered support email address is already taken by a different workspace.
#### `workspace_user_email_already_used_as_support_email`
The provided email is already used as a support email.
#### `you_shall_not_pass`
🧙 User account signup is currently blocked.
# Error handling
GraphQL queries and mutations require different error handling.
This is because we expect:
* … **queries** to generally succeed, as the three most common issues are usually unauthenticated, forbidden, or an internal server error. In the case of unauthenticated and forbidden, the API keys are invalid, while internal server errors should be retried.
* … **mutations** to return errors regularly as part of the normal business flow due to invalid inputs. Errors include rich detail which can be used and displayed to an end user.
## Query errors
Query errors aren't modeled in the GraphQL schema, but rather use [GraphQL's error extensions](https://www.apollographql.com/docs/apollo-server/data/errors/).
If the query returns the value `null`, that typically indicates that the entity is not found (equivalent to an HTTP 404 in a REST API).
The list of error extensions that can be returned by queries:
* `GRAPHQL_PARSE_FAILED`: The GraphQL operation string contains a syntax error. The request should not be retried.
* `GRAPHQL_VALIDATION_FAILED`: The GraphQL operation is not valid against the schema. The request should not be retried.
* `BAD_USER_INPUT`: The GraphQL operation includes an invalid value for a field argument. The request should not be retried.
* `UNAUTHENTICATED`: The API key is invalid. The request should not be retried.
* `FORBIDDEN`: The API key is unauthorized to access the entity being queried. The request should not be retried.
* `INTERNAL_SERVER_ERROR`: An internal error occurred. The request should be retried. If this error persists, please get in touch at [help@plain.com](mailto:help@plain.com) and report the issue.
## Mutation errors
All mutations return with an `Output` type that follow a consistent pattern of having two optional fields,
one for the result and one for the error. If the error is returned then the mutation failed.
```tsx
type Example {
data: String!
}
type ExampleOutput {
# example is the result of the mutation, is only returned if the mutation succeeded
example: Example
# if error is returned then the mutation failed
error: MutationError
}
```
Every `MutationError` has the following fields (assuming you included all these fields in your query):
* **message:** Usually meant to be read by a developer and not an end user.
* **type:** one of `VALIDATION`, `FORBIDDEN`, `INTERNAL`.
* Where `VALIDATION` means input validation failed. See the fields for details on why the input was invalid.
* Where `FORBIDDEN` means the user is not authorized to do this mutation. See `message` for details on which permissions are missing.
* Where `INTERNAL` means an unknown internal server error occurred. Retry in this scenario and contact [help@plain.com](mailto:help@plain.com) if the error persists.
* **code:** a unique error code for each type of error returned. This code can be used to provide a localized or user-friendly error message. You can find the [list of error codes](/api-reference/graphql/error-codes) documented.
* **fields:** an array containing all the fields that errored
* **field:** the name of the input field the error is for.
* **message:** an English technical description of the error. This error is usually meant to be read by a developer and not an end user.
* **type:** one of `VALIDATION`, `REQUIRED`, `NOT_FOUND`.
* Where `VALIDATION` means the field was provided, but didn't pass the requirements of the field. See the `message` on the field for details on why.
* Where `REQUIRED` means the field is required. String inputs may be trimmed and checked for emptiness.
* Where `NOT_FOUND` means the input field referenced an entity that wasn't found. For example, you tried to resolve an issue that doesn't exist/was deleted.
## Typescript SDK
If you are using the Typescript SDK, errors are handled and parsed for you and you don't need to worry about the distinction between queries and mutations.
You can see the [full error types in the code of the Typescript SDK](https://github.com/team-plain/typescript-sdk/blob/main/src/error.ts) (It's open source).
This is how you can access the error when using the SDK:
```tsx
import { PlainClient } from '@team-plain/typescript-sdk';
export async function createCustomer() {
const client = new PlainClient({ apiKey: 'XXX' });
const res = await client.upsertCustomer({
identifier: {
emailAddress: 'jane@gmail.com',
},
onCreate: {
fullName: 'Jane Fargate',
email: {
email: 'jane@gmail.com',
isVerified: true,
},
},
onUpdate: {},
});
if (res.error) {
console.error(res.error);
} else {
console.log(`Created customer with id=${res.data.customer.id}`);
}
}
```
# Events
Log important events to have the full picture of what happened in Plain.
When helping a customer it can be useful to have context about their recent activity in your product. For example, if someone is getting in touch about a 401 error, it could be important to know that they recently deleted an API key in their settings.
Events are created via the Plain API and you have full control of what they look like using Plain's UI components.
There are two types of events
* **[Customer events](/api-reference/graphql/events/create-customer-event)**: these are created in every existing thread for a customer. When a a new thread is created (e.g. by an inbound communication, or by calling the [createThread](/api-reference/graphql/threads/create) endpoint) the **25** most recent events are shown.
* **[Thread events](/api-reference/graphql/events/create-thread-event)**: these events belong to a single thread, and only appear in a single thread's timeline.
## UI Components
To define what each event should look like, you use the Plain UI components. All the components are documented in the [Plain UI Components](/api-reference/ui-components/) section.
### Playground
The UI Components Playground lets you build and preview the component JSON used to create an event. Use this to prototype an event before starting to build your integration.
[**UI Components Playground →**](https://app.plain.com/developer/ui-components-playground/)
# Create a customer event
A customer event will be created in all threads that belong to the provided customer ID. If you
want an event to appear in a specific thread use a [thread
event](/api-reference/graphql/events/create-thread-event).
To create an event you need a customer ID.
You can get this by [upserting a customer](/api-reference/graphql/customers/upsert) in Plain, from data in webhooks or other API calls you made. If you want to test this, press **⌘ + K** on any thread and then "Copy customer ID" to get an ID you can experiment with.
In this example we'll be creating the following event:
![Example event](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/events-api-key-example.png)
For this you'll need an API key with the following permissions:
* `customerEvent:create`
Which would console.log:
For this you'll need an API key with the following permissions:
* `customerEvent:create`
# Create a thread event
A thread event will only be created in the thread ID provided. If you want an event to appear in
all threads for a customer please use a [customer
event](/api-reference/graphql/events/create-customer-event).
To create a thread event you need a thread ID.
You can get this by [creating a thread](/api-reference/graphql/threads/create) in Plain, from data in webhooks or other API calls you made. If you want to test this, press **⌘ + K** on any thread and then "Copy thread ID" to get an ID you can experiment with.
In this example we'll be creating the following event:
![Example event](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/events-api-key-example.png)
For this you'll need an API key with the following permissions:
* `threadEvent:create`
* `threadEvent:read`
* `thread:read`
* `customer:read`
Which would console.log:
For this you'll need an API key with the following permissions:
* `threadEvent:create`
* `threadEvent:read`
# Introduction
An overview of Plain's GraphQL API.
Plain is built with this very GraphQL API we expose to you. This means that there are **no limitations** in what can be done via the API vs the UI.
These docs just highlight the most interesting and most used APIs. If you want to do something beyond what is documented here, please [reach out to us](mailto:help@plain.com) or explore our [schema](/api-reference/graphql/schema) on your own!
## Key details
Our API is compatible with all common GraphQL clients with the following details:
* **API URL:** `https://core-api.uk.plain.com/graphql/v1`
* **Allowed method**: POST
* **Required headers:**
* `Content-Type: application/json`
* `Authorization: Bearer YOUR_TOKEN` where the token is your API key. See [authentication](/api-reference/graphql/authentication/) for more details.
* **JSON body:**
* `query`: the GraphQL query string
* `variables`: a JSON object of variables used in the GraphQL query
* `operationName`: the name of your GraphQL operation (this is just for tracking and has no impact on the API call or result)
If you'd like to use the **GraphQL schema to generate types** for your client code you can fetch the schema
from: `https://core-api.uk.plain.com/graphql/v1/schema.graphql`
## Your first API call
In this example, we're going to get a customer in your workspace by their email address. You can find a customer's email on the right-hand side when looking at one of their threads in Plain.
You will need an API key with the `customer:read` permission. See [authentication](/api-reference/graphql/authentication/) for details on how to get an API key
You'll need to set two shell variables:
* `PLAIN_TOKEN`: The API key
* `PLAIN_CUSTOMER_EMAIL`: The email of the customer you want to fetch
```shell
PLAIN_TOKEN=XXX
PLAIN_CUSTOMER_EMAIL=XXX
curl -X POST https://core-api.uk.plain.com/graphql/v1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLAIN_TOKEN" \
-d '{"query":"query customerByEmail($email: String!) { customerByEmail(email: $email) { id fullName updatedAt { iso8601 } } }","variables":{"email":"'"$PLAIN_CUSTOMER_EMAIL"'"},"operationName":"customerByEmail"}'
```
This assumes you've installed our Typescript SDK.
`npm install @team-plain/typescript-sdk `
Make sure to replace the api key and email in the code:
`node script.js `
# Labels
Labels are a light-weight but powerful way to categorise threads, consisting of label text coupled with an icon. Each thread can have multiple labels.
They can be added manually or programmatically. For example when a contact form is submitted, you could automatically add a label to the corresponding thread with the issue category they selected, so that you know upfront why they are getting in touch.
The available labels you can apply are defined by your label types. Label types can be created and managed in your settings (**⌘ + K** and then search for "Manage labels").
When you want to stop a label being available you can archive a label type. Archived label types are kept on existing threads in order to avoid losing valuable historic data.
Label changes can also be a starting point for integrations [via our webhooks](/api-reference/webhooks/thread-labels-changed). This lets you build workflows triggered by the addition of a label.
# Add labels
You can add multiple labels to a thread with a call to `addLabels`. Label type IDs passed to this endpoint should not be archived, we return a validation error with code `cannot_add_label_using_archived_label_type` for any which are submitted.
If a label type you provide is already added to the thread we will return a validation error with code `label_with_given_type_already_added_to_thread`.
You can retrieve label type IDs in the Plain UI settings by hovering over a label type and selecting 'Copy label ID' from the overflow menu.
This operation requires the following permissions:
* `label:create`
This will output:
# Remove labels
You can remove labels from a thread with a call to `removeLabels`. Label IDs for this call can be retrieved by fetching a thread with the API.
This operation requires the following permissions:
* `label:delete`
Which if successful will console log `null`.
# Messaging
We provide various methods to message your customers with the Plain API. You can use this to reach out proactively, build an autoresponder or even to handle things like waiting list access.
Send a new email in a thread ignoring previous communications.
Use this to reply to an existing inbound email in a thread.
Reply to a thread automatically using the best channel.
# Reply to emails
You can reply to an inbound email with the `replyToEmail` API.
This operation requires the following permissions:
* `email:create`
* `email:read`
* `attachment:download`
This operation requires the following permissions:
* `email:create`
* `email:read`
# Reply to threads
You can reply to a thread using the `replyToThread` mutation, as long as the thread's communication channel is either `EMAIL`, `SLACK` or 'MS\_TEAMS'. This information is available in the thread as the `channel` field.
If it is not possible to reply to a thread, you will get the mutation error code [`cannot_reply_to_thread`](/api-reference/graphql/error-codes#cannot_reply_to_thread) and a message indicating why.
This operation requires the following permission:
* `thread:reply`
Where `res.data` is:
## Impersonation
Impersonation is exclusively available in our `Scale` plan. You can see all available plans in our
[pricing page](https://www.plain.com/pricing).
This feature allows you to bring native messaging between your customers and Plain, straight into [your own product](/headless-support-portal).
With impersonation, you can reply to a thread on behalf of one of your customers: impersonated messages will show up as if they were sent by the customers themselves.
In order to impersonate a customer, provide the `impersonation` parameter in the `replyToThread` mutation, specifying the identifier of the customer you want to impersonate. You can pick any of the available customer identifiers (`emailAddress`, `customerId` or `externalId`)
```graphql
{
"impersonation": {
"asCustomer": {
"customerIdentifier": {
"emailAddress": "blanca@example.com"
}
}
}
}
```
The customer message will be processed differently based on the thread's channel:
* `SLACK`: a new Slack message will be sent to the thread, using the customer's details
* `EMAIL`: an email will be sent to the thread, where the sender will be your customer
When replying to an `EMAIL` thread, you can optionally add 'CC' and 'BCC' recipients by using the `channelSpecificOptions` parameter:
```graphql
{
"channelSpecificOptions": {
"email": {
// For CC'd recipients
"additionalRecipients": [
{
"email": "peter@example.com",
"name": "Peter"
},
],
// For BCC'd recipients
"hiddenRecipients": [
{
"email": "finance@example.com"
}
]
}
}
}
```
This operation requires the following permissions:
* `thread:reply`
* `customer:impersonate`
# Send new emails
As well as creating outbound emails in the UI you can also send them with the `sendNewEmail` API. This is useful for proactively reaching out about issues.
# Pagination
Our GraphQL API follows the [Relay pagination spec](https://relay.dev/graphql/connections.htm).
When fetching collections from our API you can control how much data is returned. We will return 25 records per request by default and the maximum page size is 100 records.
We support two forms of page control arguments:
1. Forward pagination with `after` (cursor) & `first` (numeric count)
2. Reverse pagination with `before` (cursor) & `last` (numeric count)
Note that these must not be mixed, e.g performing a query with values for first & before will result in a validation error.
Endpoints which return paginated results will return a `pageInfo` object along with a `totalCount` field which allows you to make subsequent calls with page controls. Using the `getCustomers` API as an example this would look as follows:
Notice how we use the cursor information from the first page to fetch the second page. The returned `pageInfo` looks as follows:
This will fetch a subsequent page of 50 entries by passing in the `endCursor` from an initial query.
# Schema
If you need the schema programmatically for code generation or if you just want to read the schema you can view the [raw GraphQL schema](https://core-api.uk.plain.com/graphql/v1/schema.graphql).
You can also use the [API Explorer](https://app.plain.com/developer/api-explorer/) to learn about our API schema. This is the easiest way of discovering everything possible with the GraphQL API.
[**View API Explorer →**](https://app.plain.com/developer/api-explorer/)
# Tenants
Tenants allow you to structure your customers in Plain in the same way as they are structured in your product.
For example if within your product customers are organised in a 'team' then you would create one tenant per team in your product. A tenant has an `externalId` so that you can map it back to an entity in your database.
Customers can belong to multiple tenants.
For advanced integrations with Plain you can specify a tenant when creating a thread. This is useful when building a support portal in your product as it allows you to fetch threads specific to a team in your product.
Additionally [tiers and SLAs](/tiers/) can be associated with a tenant.
# Add customers to tenants
You can add a customer to multiple tenants.
When selecting the customer you can chose how to identify them. You can use the customer's email, externalId or id.
For this mutation you need the following permissions:
* `customer:edit`
* `customerTenantMembership:create`
# Get tenants
We provide two of methods for fetching tenants:
1. [Get tenants](#get-tenants) to fetch more than one tenant at a time.
2. [Get tenant by ID](#get-tenant-by-id)
For all of these queries you need the following permissions:
* `tenant:read`
### Get tenants
Our API allows you to fetch teanants as a collection using `getTenants` in our SDKs or the `tenants` query in GraphQL. In both cases this endpoint supports [Pagination](/api-reference/graphql/pagination).
### Get tenant by ID
If you know the tenant's ID in Plain you can use this method to fetch the tenant. Generally speaking it's preferable to use [upsert](./upsert) when you have the full details of the tenant.
# Remove customers to tenants
You can remove customers from multiple tenants in one API call.
When selecting the customer you can chose how to identify them. You can use the customer's email, externalId or id.
For this mutation you need the following permissions:
* `customer:edit`
* `customerTenantMembership:delete`
# Set customer tenants
You can also set all tenants for a customer. Unlike the more specific add or remove mutations this is useful if you are sycing tenants and customers with Plain.
For this mutation you need the following permissions:
* `customer:edit`
* `customerTenantMembership:create`
* `customerTenantMembership:delete`
# Upserting tenants
When upserting a tenant you need to specify an `externalId` which matches the id of the tenant in your own backend.
For example if your product is structured in teams, then when creating a tenant for a team you'd use the team's id as the `externalId`.
To upsert a tenant you need the following permissions:
* `tenant:read`
* `tenant:create`
# Threads
Threads are the core of Plain's data model and equivalent to tickets or conversations in other support platforms. When you use Plain to help a customer you assign yourself to a thread and then mark the thread as `Done` once you're done helping.
Threads are automatically created when a new email is received but can also be created via the API (when a customer submits a contact form for example).
Threads have a status which can be in either `Todo`, `Snoozed` or `Done` and can only be assigned to one person.
Threads belong to one customer but can contain multiple email threads and customers.
An example thread looks like this:
The below is only showing a subset fields a thread has. Since our API is a GraphQL API you decide
which fields you need when you make API requests. Use our [API
explorer](https://app.plain.com/developer/api-explorer) to discover the full schema of threads.
# Assignment
Threads can be assigned to users or machine users. The latter is useful if you want a bot to handle or are building a complex automation of some kind.
### Assigning a thread
To assign threads you need an API key with the following permissions:
* `thread:assign`
* `thread:read`
Where `res.data` is the full thread:
## Unassigning threads
To unassign threads you need an API key with the following permissions:
* `thread:unassign`
* `thread:read`
Where `res.data` is the full thread like with assignment.
# null
Plain provides native [workspace level auto-responses](/auto-responses), however for more complex cases you may want to implement your own custom autoresponder.
To achieve this you can set up endpoint(s) to be notified of one or more [webhooks](/api-reference/webhooks) from Plain. We would typically recommend listening for the [thread created](/api-reference/webhooks/thread-created) webhook as this will allow you the option of responding to any Thread whether it was created via email, Slack or a contact form.
If you want to only reply to emails, you can use the [email received](/api-reference/webhooks/thread-email-received) webhook. This will trigger for all emails, not just the first one in a thread, so you should check the `isStartOfThread` field provided in the webhook payload to ensure you only reply to the first message.
Note that if you subscribe to both `thread.thread_created` and `thread.email_received` you may
receive two events for the same email, since we create a new thread for emails which don't belong
to an existing thread. In order to avoid replying to the same message twice please check the
`isStartOfThread` field in the `thread.email_received` payload.
Once you have received an event and decided how to respond you can use the `replyToThread` mutation to send a reply back to the customer. See our [API explorer](https://app.plain.com/developer/api-explorer/) or [Typescript SDK](https://github.com/team-plain/typescript-sdk/) for more details.
# Create threads
Creating a thread is useful in scenarios where you want to programmatically start a support interaction.
You can do this in many different scenarios but the most common use-cases are when a contact form is submitted or when you want to provide proactive support off the back of some event or error happening in your product.
A thread is created with an initial 'message' composed out of [UI components](/api-reference/ui-components). You have full control over the structure and appearance of the message in Plain.
To create a thread you need a `customerId`. You can get a customer id by [creating the customer](/api-reference/graphql/customers/upsert) in Plain first.
Since the Typescript SDK expands a lot of fields you will need an API key with the following permissions:
* `label:create`
* `label:read`
* `labelType:read`
* `machineUser:read`
* `customer:read`
* `user:read`
* `thread:create`
* `thread:edit`
* `thread:read`
* `threadField:create`
* `threadField:update`
* `threadField:read`
Where `result.data` is:
To create a thread, you need an API key with the following permissions:
* `thread:create`
* `thread:read`
# Changing status
Threads can be in one of 3 statuses:
* `Todo`
* `Snoozed`
* `Done`
When you log into Plain you can filter threads by these statuses.
When threads are created they default to `Todo`.
To change a threads status you need an API key with the following permissions:
* `thread:edit`
* `thread:read`
### Mark thread as `Done`
When any activity happens in a thread, it will move back to `Todo`.
Unlike traditional ticketing software, we expect a ticket to move between `Todo` and `Done` a number of times in the course of helping a customer. This will not break or influence any metrics. `Done` in Plain means "I'm done for now, there is nothing left for me to do".
### Snooze thread
You can snooze threads for a duration of time defined in seconds.
When any activity happens in a thread, it will be automatically unsnoozed and move to `Todo`. Otherwise threads will be unsnoozed when the timer runs out.
### Mark thread as `Todo`
This is useful if you mistakenly marked a thread as `Done` or snoozed a thread and want to unsnooze it. Otherwise just write a message or do what you want to do and the thread will be automatically moved back to do **Todo**.
# Thread fields
Thread fields allow you to extend Plain's thread data model. The thread fields which you want to support have to conform to a schema configured in **Settings** → **Thread fields**.
Thread fields can be nested and be either a boolean, text or a string enum.
Thread fields can be required. When they are required, their value must be set in order for the thread to be marked as done.
For interacting with thread fields via the API, every field has a `key` defined in its schema. Keys make it possible to quickly refer to a thread field without having to know its ID in the schema. For example if you have a field called "Product Area" the key you might choose for the key to be `product_area`.
### Upsert a thread field
To upsert a thread field you need an API key with the following permissions:
* `threadField:create`
* `threadField:update`
### Delete a thread field
To delete a thread field you need an API key with the following permissions:
* `threadField:delete`
# Tiers & SLAs
Within Plain you can organise [companies](/company-support/) and [tenants](/tenant-support/) into Tiers. Tiers should match your pricing tiers (e.g. "Enterprise", "Pro", "Free", etc.).
This allows you to prioritise and filter your support requests by tier.
Tiers also add support for defining [SLAs](/tiers/) so you can enforce a first-response time for different support tiers within your product or pricing.
Typically, tiers are created via the UI in Plain and then tenants and companies are added and removed via the API when this happens in your product so Plain is in sync.
# Add companies and tenants to tiers
You can add multiple tenants and companies to a tier in a single mutation.
Companies and tenants can only be in a single tier.
For this mutation you need the following permissions:
* `tierMembership:read`
* `tierMembership:create`
# Get tiers
For all of these queries you need the following permission:
* `tier:read`
## Get tiers
Our API allows you to fetch tiers as a collection using `getTiers` in our SDKs or the `tiers` query in GraphQL. In both cases this endpoint supports [Pagination](/api-reference/graphql/pagination).
### Get tier by ID
If you know the tiers's ID in Plain you can use this method to fetch the tier.
# Remove companies and tenants to tiers
You can remove companies and tenants from the tiers they are part of manually in the UI or via the API.
For this mutation you need the following permissions:
* `tierMembership:read`
* `tierMembership:delete`
# Update company tier
If you want to explicitly set the tier for a company you can do so using this mutation. If instead you want to add many companies to a tier at once, you can use the [add members mutation](./add-members).
For this mutation you need the following permissions:
* `tierMembership:read`
* `tierMembership:create`
# Update tenant tier
If you want to explicitly set the tier for a tenant you can do so using this mutation. If instead you want to add many companies to a tier at once, you can use the [add members mutation](./add-members).
For this mutation you need the following permissions:
* `tierMembership:read`
* `tierMembership:create`
# Typescript SDK
# mTLS
All outbound requests made to your **webhook targets** and **customer card endpoints** include a client TLS certificate which you can verify to achieve mutual authentication.
This certificate is self-signed. In order to verify it, we provide our CA's certificate (in PEM format), which you will need to add to your server/truststore:
```
-----BEGIN CERTIFICATE-----
MIIDDzCCAfegAwIBAgIUPLCyLvion+WDNw0V8HAZEZL5VjswDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAwwLUGxhaW5NdGxzQ0EwIBcNMjQxMDEwMDkwMzMzWhgPMjEy
NDA5MTYwOTAzMzNaMBYxFDASBgNVBAMMC1BsYWluTXRsc0NBMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvikyF2YpU4zEYUWVYMc5P07CPQgtP6Agoia9
mElydDTReTXW9Rle0apHKNS8OUk8S6qtA5raEh8VT2HOZBUTZb16A1vl54be+LK7
imm7csEsU+FbHbfx9rRbisESu6Mkvf5qklovgcg5UfI4IrmQK3POB6pMBCcmdjyZ
udbx6YSrV5LZLth7Gxq9lcPuwzzpv2DWZTr1GGAQ46UNLXNo4+4IQYtgjThRAl4m
IBbezmiXqpi9N/7ay+P9kb4TZDQohentJu/1+y6Bj8Mxk86kq0KLlYfrEbm86lGp
mJ8s3R5luh98muRT4NdKeoHGf96UAqUq21i00TDJ/PklqardWQIDAQABo1MwUTAd
BgNVHQ4EFgQUlYHkn4D7QBvBudbhtq2M+f8CzpAwHwYDVR0jBBgwFoAUlYHkn4D7
QBvBudbhtq2M+f8CzpAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
AQEAMMLZc8zu7AqP+c2Pms6kRkp9Wr/C6QmXMuhHC98RZL1VcmZhE2P0lg/t644o
prYX8yf7Z2SRZgNb2s8oekPpuI2U2WFC4eam1dK5kS4ux7IgaXZkuB8DyZVSo1WO
KeIb2IYmXZ6hflnFNsTRjhe/Bkb7uVVw5jMaPfxWqPmeHtgUIIoh7nYj+ZnqV5Jz
FQFDb+dZzZDol/Wa3XKm7w96MrX/tanAKTygIkXyjqCrjxTI26latBQV2OPADrRO
uagGFG2G0o56wC8LTJdmceZfWYmVBLawSibj75Av8fwHgXK+XAi05m2GuVOQAfLq
yuMQLHrNDReQDB1tylx13b6meg==
-----END CERTIFICATE-----
```
If you serve your API through AWS API Gateway, you can easily do this by [enabling mTLS and
uploading the
certificate](https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-mutual-tls.html)
above as the truststore.
# Request signing
We sign outbound requests we make to your target URLs with a HMAC signature using a shared secret key. This allows you to verify that the request was made by Plain and not a third party.
## How to verify
Your workspace has a global HMAC secret, this secret can be viewed and (re)generated by workspace admins in **Settings** → **Request signing**.
If you have a HMAC secret set up, when you receive a request from Plain you will see a header `Plain-Request-Signature` with the HMAC signature.
You can verify this signature by hashing the request body with your HMAC secret and comparing it to the signature in the header.
**The signature is a SHA-256 hash of the request body, encoded as a hexadecimal string.**
### Node example
```javascript
const crypto = require('crypto');
// You may need to stringify the request body if you are using a library that parses it to a javascript object
const requestBody = JSON.stringify(request.body);
const incomingSignature = request.headers['Plain-Request-Signature'];
const expectedSignature = crypto
.createHmac('sha-256', '')
.update(requestBody)
.digest('hex');
if (incomingSignature !== expectedSignature) {
return response.status(403).send('Forbidden');
}
```
# UI Components
UI components are a way of describing some UI when creating threads or [events](/events) or building [customer cards](/customer-cards).
For example - this is a button that links to Stripe.
```json
{
"componentLinkButton": {
"linkButtonUrl": "http://stripe.com/",
"linkButtonLabel": "View in Stripe"
}
}
```
and it looks like this:
![Example button linking to stripe](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ui-component-link-button-stripe.png)
In the GraphQL API schema, we have two separate unions for Custom Timeline Entry Components and Customer Card
Components, but both unions share the same types therefore they can be treated as the same.
To see UI components in action you can experiment with them in the [UI components playground](https://app.plain.com/developer/ui-components-playground/)
# Badge
Useful for statuses or when you need to attract attention to something.
![Example badges](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ui-component-badge.png)
A badge has the following properties:
* `badgeLabel`: the text that should be displayed on the badge
* `badgeColor`: one of `GREY`, `GREEN`, `YELLOW`, `RED`, `BLUE`
For example:
# Container
Useful when you need to create a bit of structure.
![Example container](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ui-component-container.png)
A container has the following properties:
* `containerContent` (min 1): an array of components.
Allowed components within a Container are:
* [Badge](/api-reference/ui-components/badge)
* [CopyButton](/api-reference/ui-components/copy-button)
* [Divider](/api-reference/ui-components/divider)
* [LinkButton](/api-reference/ui-components/link-button)
* [Row](/api-reference/ui-components/row)
* [Spacer](/api-reference/ui-components/spacer)
* [Text](/api-reference/ui-components/text)
* [PlainText](/api-reference/ui-components/plain-text)
For example:
# CopyButton
Useful if you have any IDs or other details you want to copy for use in messages or outside of Plain.
![Example copy button](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ui-component-copy-button.png)
A copy button has the following properties:
* `copyButtonTooltipLabel` (optional): the text that should be displayed on hover. Defaults to the value if not
provided.
* `copyButtonValue`: the value that should be copied to the user's clipboard after clicking the button
For example:
# Divider
Useful when you need a bit of structure.
![Example divider](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ui-component-divider.png)
A divider has the following properties:
* `dividerSpacingSize` (optional): the spacing the divider should have before and after the component. One of `XS`, `S`,
`M`, `L`, `XL`. Defaults to `S`.
For example:
# LinkButton
Useful when you want to link somewhere external (e.g. your own admin tool or payment provider)
![Example link button](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ui-component-link-button.png)
A link button has the following properties:
* `linkButtonLabel`: the text of the button
* `linkButtonUrl`: the URL the button should open in a new tab
For example:
# PlainText
Useful when you want to show any text that should not have any formatting (is not Markdown). If you want markdown please use [Text](/api-reference/ui-components/text).
![Example link button](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ui-component-plain-text.png)
The plain text component has the following properties:
* `plainText`: the plain text
* `plainTextSize` (optional): one of `S`, `M`, `L`, defaults to `M`
* `plainTextColor` (optional): one of `NORMAL`, `MUTED`, `SUCCESS`, `WARNING`, `ERROR`, defaults to `NORMAL`
For example:
# Row
Useful when you need to show two things next to each-other.
![Example row](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ui-component-row.png)
The row component has the following properties:
* `rowMainContent` (min 1): an array of row components
* `rowAsideContent` (min 1): an array of row components
The following components can be used in a row:
* [Badge](/api-reference/ui-components/badge)
* [CopyButton](/api-reference/ui-components/copy-button)
* [Divider](/api-reference/ui-components/divider)
* [LinkButton](/api-reference/ui-components/link-button)
* [Spacer](/api-reference/ui-components/spacer)
* [Text](/api-reference/ui-components/text)
* [PlainText](/api-reference/ui-components/plain-text)
For example:
# Spacer
![Example spacer](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ui-component-spacer.png)
A link button has the following properties:
* `spacerSize`: the amount of space the component should take up. One of `XS`, `S`, `M`, `L`, `XL`.
For example:
# Text
![Example text](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ui-component-text.png)
The text component has the following properties:
* `text`: the text. Can include a subset of markdown (bold, italic, and links).
* `textSize` (optional): one of `S`, `M`, `L`, defaults to `M`
* `textColor` (optional): one of `NORMAL`, `MUTED`, `SUCCESS`, `WARNING`, `ERROR`, defaults to `NORMAL`
For example:
# Webhooks
Webhooks allow you to get notified about events happening in your Plain workspace. You can react to these events in many ways, such as:
* Assigning threads to users based on business requirements (urgency, customer value, recurrency, etc.)
* Creating an AI-powered auto-responder
* Categorising threads by adding labels based on the their content
* Triggering internal incidents (by identifying patterns in inbound messages)
* Tracking metrics from your customer support team
## Receiving events from Plain
Events happening in your workspace ('Plain events') are delivered as Webhook requests.
In order to receive webhook requests, you need a **publicly available HTTPS** endpoint. Plain will make
an `HTTP POST` request to this endpoint whenever an event you are interested in occurs.
Once your endpoint is ready, you may create a *webhook target* in Plain. A webhook target tells Plain what events you
are interested in and where to send those events.
You can create it by going to **Settings** → **Webhooks**, then clicking on '+ Add webhook target'
Then, you need to choose a name (e.g. 'Customer notifications'), the URL of your webhook endpoint, the events you want
to receive and whether you want to enable it straight away.
You can create up to **25 webhook targets** per workspace.
Plain events may contain Personally Identifiable Information (PII). If you want to test webhooks
with a production workspace, take the necessary precautions to avoid leaking PII to untrusted
parties.
We have created a repository where you will find instructions on how to create a webhook endpoint
using different programming languages. You can find it
[here](https://github.com/team-plain/webhooks-resources/tree/main/servers).
## Security
Webhook requests are always sent through HTTPS.
If you want, you can include basic authentication credentials in your webhook target's URL (`https://username:password@example.com`) which will then be sent along the webhook request in an `Authorization` header:
```text
Authorization: Basic cGxhaW46cm9ja3M=
```
Plain also supports [request signing](/api-reference/request-signing) and [mTLS](/api-reference/mtls) to verify that the request was made by Plain and not a third party.
## Delivery semantics
Plain guarantees **at-least-once** delivery of webhook requests. As such, you should make sure your webhook endpoint is idempotent. The `id` field in the [webhook request body](#body) can be used as an idempotency key.
## Handling webhook requests
Plain considers a webhook request to be successfully delivered if your endpoint returns a **2xx** HTTP status code. The contents of the response body are ignored.
Any other HTTP status code will be considered a failure, **including redirects**, which are explicitly forbidden.
## Retry policy
When a webhook request fails, Plain will keep retrying it during the **\~5 days** after the first request. The delay between
retries is set by the following table:
| Retry # | Delay | Approximate time since first attempt |
| ------- | ----- | ------------------------------------ |
| 1 | 10s | 10s |
| 2 | 30s | 40s |
| 3 | 5m | 6m |
| 4 | 30m | 36m |
| 5 | 1h | 1.5h |
| 6 | 3h | 4.5h |
| 7 | 6h | 10.5h |
| 8 | 12h | 22.5h |
| 9 | 1d | 2d |
| 10 | 1d | 3d |
| 11 | 1d | 4d |
| 12 | 1d | 5d |
Plain keeps track of all the webhook delivery attempts and their results. Each webhook request
includes [some metadata](#webhook-metadata) that you can use in order to know which delivery attempt it is currently
being processed.
## The webhook request
Webhook requests are sent as an `HTTP POST` request to the webhook target URL.
### Headers
* `Accept`: `application/json`
* `Content-Type`: `application/json`
* `User-Agent`: `Plain-Webhooks/1.0 (plain.com; help@plain.com)`
* `Plain-Workspace-Id`: The ID of the workspace where the Plain event originated
* `Plain-Webhook-Target-Id`: The ID of the webhook target this webhook request is being sent to
* `Plain-Webhook-Target-Version`: The [version](/api-reference/webhooks/versions.mdx) of the webhook target this webhook request is being sent to
* `Plain-Webhook-Delivery-Attempt-Id`: The ID of the delivery attempt. It will be different on every delivery attempt
* `Plain-Webhook-Delivery-Attempt-Number`: The current delivery attempt number (starts at 1)
* `Plain-Webhook-Delivery-Attempt-Timestamp`: The time at which the delivery attempt was made. In UTC and formatted as
ISO8601. E.g. `1989-10-28T17:30:00.000Z`
* `Plain-Event-Type`: The Plain event's type
* `Plain-Event-Id`: The ID of the Plain event. It remains the same across all of the delivery attempts
An additional `Authorization` header is sent if the webhook target URL contains authentication credentials.
### Body
The request body is a `JSON` object with the fields below.
The JSON schema for Plain the webhook request body can be found [here](https://core-api.uk.plain.com/webhooks/schema/latest.json).
| Field | Type | Description |
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `id` | `string` | The ID of the Plain event. It remains the same across all of the delivery attempts |
| `type` | `string` | The Plain event's type |
| `webhookMetadata` | `object` | Metadata associated with the webhook request. See [Webhook Metadata](#webhook-metadata) for more details |
| `timestamp` | `string` | The Plain event's timestamp. In UTC and formatted as ISO8601. E.g. `1989-10-28T17:30:00.000Z` |
| `workspaceId` | `string` | The ID of the workspace where the Plain event originated |
| `payload` | `object` | The Plain event's payload [(Example)](/api-reference/webhooks/thread-created); |
### Webhook Metadata
All the following fields are also sent as [HTTP headers](#headers).
| Field | Type | Description |
| --------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `webhookTargetId` | `string` | The ID of the webhook target this webhook request is being sent to. This is the ID that you will find under **Settings -> Webhooks** in the Support App |
| `webhookTargetVersion` | `string` | The [version](/api-reference/webhooks/versions.mdx) of the webhook target this webhook request is being sent to. |
| `webhookDeliveryAttemptId` | `string` | The ID of the delivery attempt. It will be different on every delivery attempt |
| `webhookDeliveryAttemptNumber` | `string` | The current delivery attempt number (starts at 1) |
| `webhookDeliveryAttemptTimestamp` | `string` | The time at which the delivery attempt was made. In UTC and formatted as ISO8601. E.g. `1989-10-28T17:30:00.000Z` |
# Customer created
This event is fired when a new customer is created in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Customer deleted
This event is fired when a customer is deleted from your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Customer Group Membership Changed Event
This event is fired whenever a customer is added or removed from a customer group.
The `changeType` field allows you to know what kind of change has occurred. It can be one of the following:
* `ADDED`: a customer group membership was added
* `REMOVED`: a customer group membership was removed
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Customer updated
This event is fired when a customer is updated in your workspace. You can expect this event:
* when a customer is marked as spam
* when a customer is un-marked as spam
* when the details of a customer are updated
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread assignment transitioned
This event is fired when the assignee of a thread changes or a thread is unassigned.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Chat sent
This event is fired when a chat message is sent to a customer in a thread.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread created
This event is fired when a new thread is created in your workspace.
You can subscribe to this event if you want to build an [autoresponder](/api-reference/graphql/threads/autoresponders).
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Email received
This event is fired when an email is received in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Email sent
This event is fired when an email is sent in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread Field created
This event is fired when a new thread field is created in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread Field deleted
This event is fired when a thread field is deleted in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread Field updated
This event is fired when a thread field is updated in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread labels changed
This event is fired when labels are added to or removed from a thread.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Note created
This event is fired when a note is created in a thread.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread priority changed
This event is fired when the priority of a thread changes.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
```json
{
"timestamp": "2023-10-19T21:20:07.612Z",
"workspaceId": "w_01GST0W989ZNAW53X6XYHAY87P",
"payload": {
"eventType": "thread.thread_priority_changed",
"previousThread": {
"id": "th_01HD44FHMCDSSWE38N14FSYV6K",
"customer": {
"id": "c_01HD44FHDPG82VQ4QNHDR4N2T0",
"email": {
"email": "peter@example.com",
"isVerified": false,
"verifiedAt": null
},
"externalId": null,
"fullName": "Peter Santos",
"shortName": "Peter",
"markedAsSpamAt": null,
"markedAsSpamBy": null,
"customerGroupMemberships": [],
"createdAt": "2023-10-19T14:12:25.142Z",
"createdBy": {
"actorType": "system",
"system": "email_inbound_handler"
},
"updatedAt": "2023-10-19T21:18:12.863Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01H1V4NA10RMHWFBXB6A1ZBYRA"
}
},
"title": "Unable to tail logs",
"previewText": "Hey, I am currently unable to tail the logs of the service svc-8af1e3",
"priority": 3,
"externalId": null,
"status": "TODO",
"statusChangedAt": "2023-10-19T21:18:12.862Z",
"statusChangedBy": {
"actorType": "user",
"userId": "u_01H1V4NA10RMHWFBXB6A1ZBYRA"
},
"statusDetail": null,
"assignee": null,
"assignedAt": null,
"labels": [],
"firstInboundMessageInfo": {
"timestamp": "2023-10-19T14:12:25.733Z",
"messageSource": "EMAIL"
},
"firstOutboundMessageInfo": null,
"lastInboundMessageInfo": {
"timestamp": "2023-10-19T14:12:25.733Z",
"messageSource": "EMAIL"
},
"lastOutboundMessageInfo": null,
"supportEmailAddresses": ["help@example.com"],
"createdAt": "2023-10-19T14:12:25.266Z",
"createdBy": {
"actorType": "system",
"system": "email_inbound_handler"
},
"updatedAt": "2023-10-19T21:18:12.862Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01H1V4NA10RMHWFBXB6A1ZBYRA"
}
},
"thread": {
"id": "th_01HD44FHMCDSSWE38N14FSYV6K",
"customer": {
"id": "c_01HD44FHDPG82VQ4QNHDR4N2T0",
"email": {
"email": "peter@example.com",
"isVerified": false,
"verifiedAt": null
},
"externalId": null,
"fullName": "Peter Santos",
"shortName": "Peter",
"markedAsSpamAt": null,
"markedAsSpamBy": null,
"customerGroupMemberships": [],
"createdAt": "2023-10-19T14:12:25.142Z",
"createdBy": {
"actorType": "system",
"system": "email_inbound_handler"
},
"updatedAt": "2023-10-19T21:18:12.863Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01H1V4NA10RMHWFBXB6A1ZBYRA"
}
},
"title": "Unable to tail logs",
"previewText": "Hey, I am currently unable to tail the logs of the service svc-8af1e3",
"priority": 1,
"externalId": null,
"status": "TODO",
"statusChangedAt": "2023-10-19T21:18:12.862Z",
"statusChangedBy": {
"actorType": "user",
"userId": "u_01H1V4NA10RMHWFBXB6A1ZBYRA"
},
"statusDetail": null,
"assignee": null,
"assignedAt": null,
"labels": [],
"firstInboundMessageInfo": {
"timestamp": "2023-10-19T14:12:25.733Z",
"messageSource": "EMAIL"
},
"firstOutboundMessageInfo": null,
"lastInboundMessageInfo": {
"timestamp": "2023-10-19T14:12:25.733Z",
"messageSource": "EMAIL"
},
"lastOutboundMessageInfo": null,
"supportEmailAddresses": ["help@example.com"],
"createdAt": "2023-10-19T14:12:25.266Z",
"createdBy": {
"actorType": "system",
"system": "email_inbound_handler"
},
"updatedAt": "2023-10-19T21:20:07.612Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01H1V4NA10RMHWFBXB6A1ZBYRA"
}
}
},
"id": "pEv_01HD4WYPDWSGNHMETTVVGYHDQY",
"webhookMetadata": {
"webhookTargetId": "whTarget_01HD4400VTDJQ646V6RY37SR7K",
"webhookDeliveryAttemptId": "whAttempt_01HD4XAYFTYXRYMHJ0HGR2FN3D",
"webhookDeliveryAttemptNumber": 4,
"webhookDeliveryAttemptTimestamp": "2023-10-19T21:26:49.082Z"
},
"type": "thread.thread_priority_changed"
}
```
# Thread SLA status transitioned
This event is fired when the status of an SLA linked to a thread changes.
As part of the `serviceLevelAgreementStatusDetail` field threads can have a status with the following values:
| Status | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PENDING` | When the timer on the SLA is counting down but has not met the `IMMINENT_BREACH` threshold |
| `IMMINENT_BREACH` | For SLAs where an alert has been set up to notify the team before it breaches. The SLA will be in this status after the alert period and before the SLA breaches |
| `BREACHING` | Applies to SLAs while their conditions are not met e.g if a thread with a first response time (FRT) SLA has not been replied to after the time period specified |
| `ACHIEVED` | A thread where the SLA conditions were met e.g a thread was replied to within the FRT SLA period |
| `BREACHED` | A thread where the SLA conditions were not met (and so entered `BREACHING`) but action has been taken that would have resolved the SLA e.g a thread breached the FRT SLA, but then first reply was sent |
| `CANCELLED` | An SLA which no longer applies e.g if a thread is marked as done with no reply the SLA is cancelled since we don't want it to affect metrics |
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Slack message received
This event is fired when a Slack message is received in your workspace.
If the message is edited in Slack, this webhook will not fire again.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Slack message sent
This event is fired when a Slack message is sent in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread status transitioned
This event is fired when the status of a thread changes.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Webhooks and Typescript
Our [TypeScript SDK](https://github.com/team-plain/typescript-sdk) provide utilities to [verify the webhook signature](https://www.plain.com/docs/api-reference/request-signing#request-signing) and parse the webhook body into a typed object.
```typescript
import express from 'express';
import {
PlainWebhookSignatureVerificationError,
PlainWebhookVersionMismatchError,
verifyPlainWebhook,
} from '@team-plain/typescript-sdk';
// Plain HMAC Secret. You can find this in Plain's settings.
const PLAIN_SIGNATURE_SECRET = process.env.PLAIN_SIGNATURE_SECRET;
if (!PLAIN_SIGNATURE_SECRET) {
throw new Error('PLAIN_SIGNATURE_SECRET environment variable is required');
}
const app = express();
app.use(express.text());
app.post('/handler', function (req: Express.Request, res: Express.Response) {
// Please note that you must pass the raw request body, exactly as received from Plain,
const payload = req.body;
// Plain's computed signature for the request.
const signature = req.get('Plain-Request-Signature');
const webhookResult = verifyPlainWebhook(payload, signature, secret);
if (webhookResult.error instanceof PlainWebhookSignatureVerificationError) {
res.status(401).send('Failed to verify the webhook signature');
return;
}
if (webhookResult.error instanceof PlainWebhookVersionMismatchError) {
// The SDK is not compatible with the received webhook version.
// This can happen if you upgrade the SDK but not the webhook target, or vice versa.
// We recommend setting up alerts to notify you when this happens.
// Consult https://plain.com/docs/api-reference/webhooks/versions for more information.
console.error('Webhook version mismatch:', webhookResult.error.message);
// Respond with a non-2XX status code to trigger a retry from Plain.
res.status(400).send('Webhook version mismatch');
return;
}
if (webhookResult.error) {
// Unexpected error. Most likely due to an error in Plain's webhook server or a bug in the SDK.
// Treat this as a 500 response from Plain.
console.error('Unexpected error:', webhookResult.error.message);
res.status(500).send('Unexpected error');
return;
}
// webhookResult.data is now a typed object.
const webhookBody = webhookResult.data;
// You can use the eventType to filter down to a specific event type
if (webhookBody.payload.eventType === 'thread.thread_created') {
console.log(`Thread created: ${webhookBody.payload.thread.id}`);
}
// Respond with a 200 status code.
res.status(200).send('Webhook received');
});
```
We strongly recommend verifying the webhook signature before processing the webhook body. This ensures that the webhook was sent by Plain and not a malicious third party. However, if you want to skip the verification, you can use the [`parsePlainWebhook` function](https://plain-typescript-sdk-docs.vercel.app/functions/parsePlainWebhook.html) instead.
# Webhook Versions
Every [webhook target](https://www.plain.com/docs/api-reference/webhooks#receiving-events-from-plain) in Plain is associated with a specific version. The webhook version defines the schema of the payload that Plain sends to your endpoint. By specifying a version, you ensure that the payload format remains consistent, even as Plain evolves and introduces changes to the webhook schema.
**Benefits of Versioning**:
* **Consistency**: Your endpoint always receives payloads in the same format.
* **Control**: You decide when to adopt new schema changes.
* **Stability**: Prevents unexpected breaking changes due to schema updates.
## Available Versions
We recommend always using the latest version of the webhook payload schema to benefit from new features and improvements. Below are the currently available versions:
### `2024-09-18` (Latest)
Our first official versioned webhook payload schema.
* **What's New**:
* Introduction of webhook versioning.
* Improved forward-compatibility schema definitions for payloads.
* Microsoft Teams events.
* New thread status details.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2024-09-18.json)
* **TypeScript SDK Compatibility**: Version `>= 5.0.0`
### `unversioned`
The legacy webhook payload schema before versioning was implemented.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/unversioned.json)
* **TypeScript SDK Compatibility**: Versions `>= 4.8.0` and `< 5.0.0`
## How to Upgrade to the Latest Version
Upgrading to the latest webhook version involves updating your code to handle the new schema and changing your webhook target settings in Plain.
### Step 1: Update Your Code
Modify your code to handle both the old and new webhook payload versions during the transition period. This ensures uninterrupted processing of events.
**Handle Version Mismatch (TypeScript SDK Users):**
If you're using our TypeScript SDK, you can update your code to throw an error when receiving an old version. This will cause Plain to retry the request later, ideally after you've updated the webhook target version in the Plain dashboard. For more details, refer to our [retry policy](https://www.plain.com/docs/api-reference/webhooks#retry-policy).
See the [TypeScript SDK Example](#typescript-sdk-example) for implementation details.
Deploy this updated code, and fast follow with Step 2.
### Step 2: Update the Webhook Target in Plain
After deploying your updated code, change the version of your webhook target in Plain to the new version. This ensures that all future webhook events are sent using the latest schema.
### Step 3: Revert Temporary Code Changes
Once you have confirmed that your application is successfully processing events with the new version, you can remove the code that handles both old and new versions. Your code can now exclusively handle the latest webhook payload schema.
Ensure that your webhook handling code is **idempotent** and can gracefully handle **duplicate
events**. Plain's webhook delivery is **at least once**, meaning the same event might be delivered
multiple times. Refer to our [delivery
semantics](https://www.plain.com/docs/api-reference/webhooks#delivery-semantics) for more
information.
## TypeScript SDK Example
Below is an example of how to handle webhook version mismatches using our TypeScript SDK:
```typescript
import { verifyPlainWebhook, PlainWebhookVersionMismatchError } from '@team-plain/typescript-sdk';
// The same approach works for `parsePlainWebhook`
const webhookResult = verifyPlainWebhook(payload, signature, secret);
if (webhookResult.error instanceof PlainWebhookVersionMismatchError) {
// Received a webhook with an old version
// Return a non-2XX response to trigger a retry
throw new Error('Skipping older version');
}
// proceed with the rest of your error handling logic
```
**Explanation**:
* **Version Mismatch Handling**: By checking if the error is an instance of `PlainWebhookVersionMismatchError`, you can determine if the payload is on the old version. This could happen if an event is sent during the time between your code deployment and the webhook target version update in Plain.
* **Triggering a Retry**: Throwing an error or returning a non-2XX HTTP response tells Plain to retry the webhook delivery later. After you update the webhook target version in Plain, the failed event will be resent with the new schema.
## Identifying the Webhook Version in Received Payloads
If you receive a webhook payload and are unsure which version it is using, you can identify the version by checking:
* **Headers**: The `Plain-Webhook-Target-Version` header indicates the version of the webhook target for which this request is intended.
* **Payload Metadata**: Within the [webhook metadata](https://www.plain.com/docs/api-reference/webhooks#webhook-metadata) in the payload body, the `webhookTargetVersion` field specifies the version of the webhook target for this request.
This information helps you determine how to parse and handle the webhook payload according to its schema version.
## Best Practices and Recommendations
* **Monitor Logs**: After upgrading, monitor your logs and error tracking systems for any issues related to webhook processing.
* **Stay Informed**: Keep an eye on our documentation and [change log](https://www.plain.com/changelog) for future updates or changes to the webhook schema.
If you have any questions or need assistance, please reach out to us at **[help@plain.com](mailto:help@plain.com)**.
# Assignment
![Assignment within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/assignment-introduction.png)
Threads can be assigned to any user in your workspace.
When you start typing a message, Plain will automatically assign you, so you typically don't have to think about assigning yourself manually.
If you want to assign a thread manually, you can use the shortcut **A** or use **⌘ + K**.
By default in Plain, once you are assigned to a thread, you will never be automatically unassigned. This ensures that if you pick up a support request, you can see it through. We call this behavior "sticky assignment".
If you'd like to change this behavior, you can do so in **Settings** → **Workflow**. Here you can choose to be automatically unassigned when a thread is marked as `Done` or `Snoozed`.
We recommend turning sticky assignment off if your team has a shared support rota. That way, whoever is on-call can pick up any threads you didn't finish.
If you have sticky assignment on but are going on holiday you can turn on Away Mode in the top left menu within Plain. This will unassign you from any threads that come back to Todo.
If you are interested in building fully automated bots using Plain, you can also programmatically assign threads to machine users as a starting point. If this sounds like an interesting use-case to you, please reach out to us via [help@plain.com](mailto:help@plain.com).
**Multiple Assignees**
You can add multiple assignees (lead and co-assignees) to threads. Having the ability to add multiple assignees improves your ability to collaborate internally on customer requests, and speeds up your team's response times.
# Auto-responses
To set up an auto-response go to **Settings** → **Auto-responses**
An auto-response can be useful in situations of acute support load, such as during incidents, or to confirm you received a support request and manage expectations with customers.
Auto-response messages are best kept short and personable without too many links or formatting. Messages which are too long and scripted will often just be ignored as they are too obviously automated.
When you configure an auto-responses, you can choose which channels it will be used in:
* `Slack`: threads started from Slack
* `Api`: threads that you create programmatically
* `Email`: threads created from an inbound email
An auto-response will trigger when a new thread is created. Plain adds a little delay (approximately one minute) before sending the message to give your users a chance to reply.
Auto-responses can also be configured to only trigger for specific **tiers** and inside or outside **business hours**. If multiple auto-responses are configured for the same channel, tier and business hours, we will pick the **first one** according to the order they show up under settings.
You can also leverage Plain's powerful API and webhooks to build custom interactive auto-responses, see [Thread autoresponders](/api-reference/graphql/threads/autoresponders) for more details.
# Broadcasts
Plain broadcasts is a free standalone app provided by Plain which lets you send updates to differentiated groups of customers in Slack.
![Plain broadcasts](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/broadcasts.png)
## Getting started
### Define an audience
Audiences let you group your customers so that you can control who messages get sent to.
In order to add a channel to an audience you need to add the Plain app to it in Slack first. If you can't see a channel you expect to, try hitting the refresh icon next to the search box to refresh the available channels.
### Create a broadcast
Broadcasts are the messages and updates which you send out to customers. You can construct your broadcast out of all the blocks you would expect such as:
* text
* headings
* lists
* images
* code blocks
* and more!
To insert emojis try using your system emoji picker e.g `ctrl + cmd + space` on Mac or `Windows key + ;` on Windows.
# Chat
*Chat is currently in Beta. This will not effect the reliability of this feature but we are heavily iterating and improving on the featureset.*
Our Chat widget lets you embed a live chat interface on your website or app, allowing your customers to reach out to you without leaving your site. You can then respond to these messages directly from Plain.
You and the customer can communicate in real-time and provide high fidelity support including syntax highlighting and rich-text formatting.
Here’s how to get started:
*At the moment you can only create a single Chat app configuration per workspace. In future we
will allow multiple configurations.* - In Plain, navigate to **`Settings`** > **`Chat`**. -
Press **`Create Chat App`**.
After creating your Chat app, you will be provided with a snippet of code that you can embed in
your website or app. This will add the chat widget to your site. We recommend adding this to all
pages of your site.
You can customize some aspects of the Chat widget by providing additional information to the
`Plain.init` function. These are the available options:
You can add conditional auto-responses to chat by heading to the "auto-responses" section of
your workplace settings. Set conditions based on tier or business hours, and let your customers
know when they'll receive a response.
Each new conversation in the Chat widget creates a new thread in Plain. You can view and respond
to these threads in the Plain app.
## Content Security Policy (CSP)
If you are using a Content Security Policy on your website, you will need to add the following to your CSP:
```text
script-src https://chat.cdn-plain.com;
connect-src https://chat.uk.plain.com;
style-src https://fonts.googleapis.com;
```
# Authentication
## Providing customer details
By default, customers chatting with you will be anonymous. You can pass customer details, if you know them, in the `Plain.init` function call:
```typescript
Plain.init({
// ... Other options
customerDetails: {
fullName: 'John Doe', // Optional
shortName: 'John', // Optional
chatAvatarUrl: 'https://picsum.photos/32/32', // Optional
},
});
```
These details will be shown to you in the Plain app when you are chatting with the customer but they will not be matched to an existing customer even if they have the same details.
## Matching chat users to existing customers
If you want to match the customer to an existing customer in your workspace, you will need to pass their email. To avoid security issues around impersonation you will also need to
provide the email address hashed using a shared secret. You can generate this secret in the Chat settings page in the Plain app.
Once you have this secret, you can calculate the hash and pass it to the `Plain.init` function:
```typescript
import * as crypto from 'node:crypto';
const secret = '';
const email = 'johndoe@example.com';
const hmac = crypto.createHmac('sha256', secret);
hmac.update(email);
const hash = hmac.digest('hex');
Plain.init({
customerDetails: {
email: email,
emailHash: hash,
// If you pass other customer details (e.g. fullName), this will also update their customer details within Plain
// If you don't pass any other customer details, we will use their existing details within Plain
},
});
```
# Companies
![Company support within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/companies-introduction.png)
In Plain you can see what company a customer belongs to, so you have more context when providing support.
The company is automatically set based on the email domain of the customer and is visible throughout the support app. For example if a customer has the email with the domain **@nike.com** then their company will be **Nike**.
You can browse threads by company in Plain as well as associate a company with a tier to manage their SLAs.
Companies can also be listed and updated [via the API](/api-reference/graphql/companies/).
# Contact forms
The best way to offer support at scale.
With contact forms, you can pre-triage every support request so that you know exactly what to prioritize in Plain.
Plain **does not** provide any UI components or a drop-in script tag. Instead, you use your own UI components and then use Plain's API.
When a contact form is submitted, you first [create the customer in Plain](/api-reference/graphql/customers/upsert) and then [create a thread](/api-reference/graphql/threads/create) in Plain.
Depending on your desired behavior, you can also do other things as part of the form submission, such as [add the customer to a customer group](/api-reference/graphql/customers/customer-groups), [set labels](/labels), and add a priority to a thread. Contact forms can take any shape and be very specific to your product and customers.
Here are just a few examples as starting points for you to customize:
### Example floating contact form
This shows how you can build a very simple floating contact form (bottom right) in NextJS.
[**View demo ↗**](https://example-nextjs-floating-form.vercel.app/) | [View source on Github↗](https://github.com/team-plain/example-nextjs-floating-form)
![Screenshot of the floating contact form](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/contact-form-floating.png)
### Example advanced contact form
This shows a more advanced contact form in NextJS with structured inputs depending on the topic you are getting in touch about and categorization.
[**View demo ↗**](https://example-nextjs-advanced-contact-form.vercel.app/) | [View source on Github↗](https://github.com/team-plain/example-nextjs-advanced-contact-form)
![Screenshot of the advanced contact form](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/contact-form-advanced.png)
# Customer cards
Live context straight from your own systems when helping customers.
Customer cards are a powerful feature in Plain that allows you to show information from your own systems while looking at a customer or thread in Plain. This ensures that you always have important context at your fingertips when helping customers, without having to jump through different tabs and admin tools.
![Customer cards within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/customer-cards-introduction.png)
Customer cards are:
* Pulled from your backend, so you do not have to sync your customer data to Plain.
* Short-lived, so no data is permanently stored in Plain beyond the time frame you set.
* Defined using a simple JSON schema, so you don't have to worry about styling them.
* Automatically reloaded if a user is viewing a customer and the data expires.
[**Jump to documentation →**](/api-reference/customer-cards)
# Customer groups
Organize and segment your customers.
![Customer groups within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/groups-introduction.png)
Groups let you organise customers when company, tenant or tiers don't make sense. A good example of this is when someone is beta tester or an investor trying your tool.
You can configure the groups in your workspace by going to **Settings** → **Customer Groups**, where you can set a name and color per group.
You can then either manually add a customer to a group from any thread, or you can programmatically [via the API](/api-reference/graphql/customers/customer-groups).
You can also filter threads by customer groups, so you can, for example, prioritize threads belonging to beta testers.
# Data model
A quick overview of how Plain is structured.
Whether you are using Plain's API or not, this page is a quick way to understand how Plain works.
Within Plain, all data belongs to a **Workspace**. Within a workspace, you have Users, Customers, Threads, Companies and more.
## Workspace
Everything within Plain happens in a workspace. Typically, you will have one workspace like you would in Slack or Discord. You can create multiple workspaces if you want to match different environments, e.g. "Acme Staging" and "Acme Production".
## Users
When you use Plain, you are a user. As a user, you have one or more roles that define what you are allowed to do within Plain.
## Customers
Within Plain, you help customers. Each customer has a name ("Grace Hopper"), short name ("Grace"), and one email address. Email addresses are unique across all customers. Customers can also have an `externalId` which lets you correlate them to customers in your own systems, you can provide this when creating or updating them via the API.
Customers are created automatically by Plain when receiving a new support request or can be created programmatically using our API. Customers can belong to a company and multiple tenants (more on that below).
## Threads
Threads are the core of Plain's data model and are equivalent to tickets or conversations in other support platforms.
Each thread belongs to one customer and has a status which is one of `Todo`, `Done`, or `Snoozed`. When you use Plain to help a customer, you assign yourself to a thread and then mark the thread as done once you're done helping.
Threads are created automatically by inbound communications or programmatically via the API, for example, when a contact form is submitted.
Each thread has a timeline. The timeline contains all relevant communication, events and other updates such as assignment changes.
Threads can be extended with custom attributes called thread fields.
## Labels
Labels are a lightweight way of categorizing threads by topic (e.g., bugs, feature requests, demo request, etc.). A thread can have one or more labels, and each label has a name and an icon that you choose in your settings.
You can filter threads by label in any queue.
## Events
Events belong to a customer or thread and allow you to log important actions that happen outside of Plain within Plain. Events provide you with additional context of the customer's actions when you are helping them.
For example, if you log an event when a customer deletes an API key in your systems, then if they reach out reporting 401 errors - you immediately know why.
## Companies
Each customer can belong to one company. The company is automatically set by Plain based on the customer's email. For example if a customer gets in touch with the email [jeff@nike.com](mailto:jeff@nike.com),then their company will be set to Nike. The company of a customer can be changed manually and via the API.
You can filter and organise your support by company.
## Tenants
Tenants allow you to organise your customers in the same way they are organised in your product. For example, if your product's users are organised into teams, then every team would be one tenant within Plain.
Tenants can only be created programmatically and are useful for advanced integrations or larger support teams.
## Tiers
Tiers allow you to organise companies and tenants into groups that match your product's pricing. For example "Enterprise", "Pro", "Free" etc. Within tiers you can also define SLAs so that you can stay on top of your queue and prioritise.
# Digests
![Digests daily update](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/digests-update.png)
Digests are a way to get a summary of activity in your Plain workspace. They are currently only available via [Slack Workspace Notifications](./notifications#workspace-notifications)
Digests are configured in settings, and you can specify a time (in UTC) when you would like to receive them. They are **only sent on weekdays**.
## Available Digests
### Daily Standup
The Daily Standup Digest aims to give you a brief, cohesive overview of your outstanding threads in “Todo”, and tags the members of your team that have threads assigned to them.
This Digest is tailored towards the members of your team who are actively responding to support requests – we recommend using it in the mornings so you get a quick overview first thing about the threads that need to be actioned.
### Daily Summary
The Daily Summary Digest is aimed at giving you a high-level view into the health of your support queue. Threads that need high priority actions will be listed here – e.g. threads that have breached or are about to breach their SLAs, and threads that have been waiting for action for a long time.
This Digest will be useful for Heads of Support, or team members who mightn’t be in the support weeds every day, but need to know about the high-level state of your support queue. We recommend turning it on at the end of the day so you get an idea of how your support team has performed throughout the day.
## Setup
* Navigate to **Settings → Notifications → Slack**
* Setup or choose an existing Slack integration
* Choose the Digest you want to turn on and specify the time (in UTC) that you’d like it to be delivered
* Turn on the toggle
## What if I want to send different Digests to different Slack channels?
To set up a digest to send to a different Slack channel (e.g. if you want the Daily Summary to be sent to #support-leads, but the Daily Standup Digest to send to #support) – just repeat the steps above and add a new Slack channel integration in your Notifications settings.
# Email
Setting up email requires you to complete two steps:
If you follow this guide, bear in mind that:
* All emails sent to that email address will be received by Plain (not your email provider's inbox)
* You can only use one domain per Plain workspace but can use multiple emails for the same domain.
If you need to receive a copy of each email sent to the support email address (for instance, to keep a copy in your
company's email inbox), there are ways to achieve this. Please get in touch with us by email
at [help@plain.com](mailto:help@plain.com), and we will be happy to help.
## What you will need
* Your Plain workspace's inbound email address (which ends with
`@inbound.postmark.app`). We will provide this during the email setup process.
* Admin access to your domain's DNS settings.
* Admin access to your company's email provider.
* If you use Google for email, you will need admin access to the Google Workspace Admin console.
## What makes a good support email address?
The only requirement is that your support email address uses your company's domain. Email addresses using `@gmail.com`
, `@yahoo.com`, `@icloud.com`, `@hotmail.com` or any other public email provider **are not supported**.
We recommend you choose an email address that is easy to remember and clear that it represents a company. Good
examples could be `help@`, `support@`, `contact@` or `hello@`. Generally, we'd advise against a personal one
like `sonia@` or `peter@` but they might also be fine, depending on your needs.
# Alternate addresses
Within Plain you can configure up to 10 alternate email addresses. Alternate email addresses allow you to receive and send emails from Plain just like your main support email address.
You can configure these at the bottom of **Settings** → **Email** page in Plain.
Alternate email addresses must:
* ...have the same domain as the main support email. For example, if your main support email is [help@acme.com](mailto:help@acme.com) then you could have [hello@acme.com](mailto:hello@acme.com) as an alternate addresses but not [sales@tnt.com](mailto:sales@tnt.com).
* ...be forwarded in the same way as your main help email to the email address Plain provides you when setting up your email.
Once alternate emails are set up you can choose to reply from them when writing an email in Plain. By default Plain will always try to reply with the same email used by the customer.
# Email avatars
When it comes to customizing the avatar or logo that appears alongside the emails you send from Plain, it’s important to note that you don't "send" an avatar with the email itself. Instead, the avatar your customer sees is determined by the recipient's email client, which controls how inboxes are rendered.
Here's a breakdown of the options available for displaying custom avatars in your emails:
1. **BIMI**\
BIMI (Brand Indicators for Message Identification) allows you to display a custom logo next to your emails by adding a DNS record that points to an SVG image of your logo. Some [email clients support BIMI](https://support.google.com/a/answer/10911320), but there are a few considerations:
* Gmail, amongst others, will only display your logo if it’s verified by a certificate authority. This process involves purchasing a Verified Mark Certificate (VMC), which costs around \~\$1200 per year.
* Without the certification, some clients may still show your logo, but support varies across different platforms.
2. **Gravatar**\
Some email clients use [**Gravatar**](https://gravatar.com/) to display avatars. You can create a Gravatar profile **for free** (using your workspace's email address) and if the recipient’s client supports it, your Gravatar's profile picture will show in the emails you send.
3. **Gmail**
In the abscense of BIMI, Gmail will pull the avatar from the profile picture you use in your Gmail-managed email. You can do this from your Gmail inbox or follow [these instructions](https://support.google.com/mail/answer/35529)
Ultimately, the avatar that appears next to your emails depends entirely on the recipient's email client and its rendering choices.
# Receiving emails
To receive emails, you need to set up email forwarding from your company's support email address (e.g. `help@yourcompany.com`)
to your Plain workspace's inbound email address. Your workspace's inbound email address ends with `@inbound.postmark.app` and
can be found in under **Settings** → **Email**.
This assumes you use Google Workspace (formerly called Google Apps) to manage your domain's emails.
If your email provider is not Google, you can still set up email forwarding in different ways, such as with your domain registrar (e.g. [DNSimple](https://support.dnsimple.com/articles/email-forwarding/)) or your email provider (e.g. [Microsoft 365](https://learn.microsoft.com/en-us/microsoft-365/admin/email/configure-email-forwarding)).
You can find this here: [https://admin.google.com/u/0/ac/apps/gmail/defaultrouting](https://admin.google.com/u/0/ac/apps/gmail/defaultrouting)
Under "Default routing" click on "CONFIGURE" or "ADD ANOTHER RULE"
![Add new rule](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/email-setup-0.png)
In the dropdown, select "Single recipient" and write your **support email address** under "Email address"
![Add your support email address](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/email-setup-1.png)
Choose "Replace recipient" and paste the inbound email address (`@inbound.postmarkapp.com`)
![Replace recipient](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/email-setup-2.png)
Scroll further down, and choose "Perform this action on non-recognised and recognised addresses"
![Apply to all addresses](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/email-setup-3.png)
...and That's it! 💅
If you have existing email routing rules they can sometimes conflict with ones you add for Plain.
For optimal reliability always reorder the Plain ones to apply first.
# Sending emails
To be able to send emails from Plain using your support email address, you will need to add a couple of DNS records to your domain.
While setting up your email, you will see DNS settings that need to be
configured for your domain:
![DNS settings](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/email-setup-4.png)
The first record is the "DKIM". It is part of the available mechanisms that exist to authenticate
emails: verifying that you're the actual sender of the email.
The second record sets the return path for emails sent from Plain using your domain.
Return paths are used if an email bounces (fails to reach an inbox).
Both of them make sure that your emails reach your users' email inboxes
and do not end up in the spam folder.
Adding these to your DNS records varies slightly among different
providers. Here is how to do so for some common hosting providers:
1. Go to [https://dnsimple.com/dashboard](https://dnsimple.com/dashboard).
2. Click on the domain you need to update. If your support address is
`help@example.com` you must pick `example.com`.
3. On the left menu, click on "DNS".
4. Under "DNS records" click "Manage".
5. Click "Add record" Choose TXT. Add the following:
* **Name**: paste "Hostname"
* **Type**: TXT
* **Content**: paste "Value"
6. Click "Add record" to save.
7. Click "Add record" again. Choose "CNAME" Add the following:
* **Name**: paste "Hostname"
* **Type**: CNAME
* **Content**: paste "Value"
8. Click "Add record" to save.
1. Go to [https://domains.google.com/registrar/](https://domains.google.com/registrar/).
2. Choose the domain you need to update. If your support address is `help@example.com`
you must pick `example.com`. Click on "Manage".
3. On the left menu, click on "DNS".
4. Click on "Manage custom records" Scroll down and click on "Create
new record".
5. Add the following:
* **Host name**: paste "Hostname"
* **Type**: TXT
* **Data**: paste "Value"
6. Click on "Create new record" again.
7. Add the following:
* **Host name**: paste "Hostname"
* **Type**: CNAME
* **Data**: paste "Value"
8. Click "Save".
1. Go to [https://ap.www.namecheap.com/domains/list/](https://ap.www.namecheap.com/domains/list/).
2. Pick your domain and click on "Manage".
3. Click on the "Advanced DNS" tab.
4. Under "Host records" click on "Add new record" and set the following:
* **Type**: TXT
* **Host**: paste "Hostname"
* **Value**: paste "Value"
5. Click on "Add new record" again and add the following:
* **Type**: CNAME
* **Host**: paste "Hostname"
* **Value**: paste "Value"
6. Click on "Save all changes" to save.
1. Go to [https://account.godaddy.com/products](https://account.godaddy.com/products)
2. Pick your domain and click on "DNS".
3. Click on "Add" and add the following:
* **Name**: paste "Hostname"
* **Type**: TXT
* **Content**: paste "Value"
4. Click on "Add entry" Confirm details and wait.
5. Click on "Add" again and add the following:
* **Name**: paste "Hostname"
* **Type**: CNAME
* **Content**: paste "Value"
6. Click on "Add entry" to save and wait.
Remember that in some cases, changes to DNS records may take some time
to propagate. For modern providers this should be less than 10 minutes,
but in extreme scenarios it may take 24-48 hours.
# Events
Get the full picture of what happened and why straight in Plain.
![Screenshot of the Events](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/events-introduction.png)
Events are a powerful API feature of Plain that allows you to log key events to Plain, providing more context while you are helping a customer.
When you create an event in Plain, you can specify the content of the event using a simple GraphQL request and our [UI Components](/api-reference/ui-components).
When a customer gets in touch, you will see the events you created on the thread's timeline, along with all other messages and activity.
Some examples of use-cases for events include:
* Keeping track of key account activity, such as upgrading plans, inviting team members, or changing sensitive settings in your product.
* Logging errors or potential issues that your customers encounter (e.g., someone experiences an unexpected error while using your product).
* Logging key events in your own systems, such as changes to feature flags or releases.
[**Jump to documentation →**](/api-reference/graphql/events)
# Headless Support Portal
Give your customers visibility over their support requests.
Headless Portal is available on the [Grow pricing plan](https://www.plain.com/pricing) and above.
If you are interested in trying this feature, please reach out to us at [sales@plain.com](mailto:sales@plain.com) or via our shared Slack channel.
When building a headless support portal, you will have access to our engineering team, who can
advise and help you. These docs are primarily intended to give you a high-level overview of how
the process works. They do not aim to be exhaustive or self-serve.
Customer support portals allow your customers to view, create, and reply to support requests directly from your product.
Plain provides a set of powerful APIs to do this, and you build the UI.
This means you can build a support portal tailored to your product, look 100% white-label, and not require separate login credentials from your product.
### Security & architecture
To build a support portal, you will make API calls to [Plain's GraphQL API using an API key](/api-reference/graphql/authentication). This API key needs fairly broad permissions to be able to read threads and customer details as well as perform other actions such as send emails.
For this reason it's **very very important to never make API calls to Plain directly from the client**. Doing so means you will be leaking the API key which is a big security risk.
You must always make Plain API calls from an API you control. This allows you to implement access controls so that for example, a customer can only access their support requests etc.
If you have leaked an API key by mistake, immediately delete it from your workspace settings and don't hesitate to reach out to us if we can help with any mitigation/investigations.
### How does it work
It's very helpful to first read our [data model documentation](/data-model) to get your bearings.
Details can vary depending on the experience you'd like to offer but in essence to build a portal you have 4 separate piece of work:
1. Creating a tenant for each of your customers in Plain
2. Fetching threads for a customer's tenant
3. Allow customers to submit new threads
4. Allowing customer to reply within a thread.
When a customer signs up to your product you first have to create a tenant for that customer. You can [read more about tenants here](/api-reference/graphql/tenants), but in essence each tenant in Plain should 1:1 map to your own workspace/team/org concept in your product.
This is an essential first step so that you can make sure that when someone access the support portal they only see *their* team's support requests. Without this it would be difficult/impossible to know which support requests belong to the customer's workspace/team/org.
To create tenants in Plain [you can upsert them](/api-reference/graphql/tenants/upsert). After that you can [add individual customers to that tenant](/api-reference/graphql/tenants/add-customers).
You can, if you are prototyping this, skip this step and instead just fetch support requests for the customer that is logged in. This means that if John and Lucy are both part of the same team in your product, John will only see his own tickets vs also being able to see Lucy's latest support requests. Depending on your product this might work fine but for most B2B SaaS this is not the ideal experience.
Once you have set tenants & threads for all support requests you can fetch them via our API and show them to the customer
To display a list of support requests you will need to fetch the appropriate threads from your workspace. The recommended way to do this is to filter by [tenant](/api-reference/graphql/tenants).
```Javascript
const threads = await plainClient.getThreads({
filters: {
tenantIdentifiers: [{ externalId: tenantExternalId }],
statuses: [ThreadStatus.Todo, ThreadStatus.Snoozed],
},
});
```
If in Step 1 you opted for not using Tenants then you can also filter by customerId here in order to fetch just the threads belonging to a specific customer.
This will give you back a list of threads but not give you back the content of a thread. To do so you must use this query. This will fetch all relevant message timeline entries. This example is showing how to do this via GraphQL directly but this can also be done with our SDK using the `rawRequest` method.
To submit new support requests from your portal you can create a contact form which, when submitted, create a thread within Plain.
What is important is that when you create the thread you pre-fill the customer's tenant id. This ensures that the support request they create is then visible in the support portal.
You can read more on [how to create a thread here](/api-reference/graphql/threads/create).
One thing to note is that you are not constrained at all here by the data you want to gather. Although a simple form with just a message and a title does work, it's typically much more useful and powerful to ask structured questions such as what the support request is about, how urgent it is and much more.
You can [read more on contact forms, as well as see some real examples here](/contact-forms).
To reply to threads directly from your support portal is probably the most difficult part of building a good support portal from the UI side of things.
In terms of the interaction with Plain, however, it's as simple as calling the [`replyToThread` mutation](/api-reference/graphql/messaging/reply-to-thread).
Depending on the channels you support your UI here might vary. For example if you want to not support email communication here you don't need to allow for specifying Cc and Bcc addresses.
### Example implementation
To show you how this works we've built an example using NextJS which shows how you might set up a headless support portal.
**[Check out the example on Github →](https://github.com/team-plain/example-headless-portal)**
***
There is a lot of more nuance and detail that these docs do not cover such as rich formatting, attachments etc. We can help you plan and scope out the necessary work depending on your exact requirements and stack. We can also debug technical issues as well as actually help with the implementation in your code base.
If you are interested in building a headless support portal based on this high-level overview, please reach out to us via Plain or on [help@plain.com](mailto:help@plain.com).
# Insights
![Insights](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/insights-0.png)
**Insights** give you an overview of your support workload allowing you to analyze trends, identify improvement areas and answer questions like:
* Are we prioritizing support requests correctly?
* What product areas are creating most of our support volume?
* What companies are we speaking with most?
* Are we becoming more efficient?
* And lots more
## Using Insights
You can access Insights from the left sidebar of the Plain app or by hovering over a Label, Tier, Group or Company tag on a thread.
Insights give you the following metrics and much more:
1. **Support volume**: The number amount of threads in your queue at any given point in time.
2. **First response time**: How quickly your customers receive a first reply from you.
3. **Resolution time**: How long it takes you to resolve support requests.
Each metric is broken down to display the median and 90th percentile for either the current day, last 7 days or last 28 days. You will also see a volume breakdown of each metric by Company, Tier, or Label and whether the metric is trending upward or downward.
### Get a birds-eye view
At the top of the Insights page you will see the overall health of your support. How is your queue looking? Are you keeping up with your response time targets? Are you resolving your customers requests quickly?
![Insights overall health](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/insights-1.png)
### Get into the details
In each section of Insights, you will see a breakdown of your support by Channel, Company, Group, Label, Priority and Tier. This lets you see which attributes of a support request might be contributing to slower response times or increased volume.
![Insights details](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/insights-2.png)
### Check in from anywhere in Plain
Hover over any Company, Group, Label or Tier in Plain to instantly see a view of recent support volume. This is a great way to proactively identify trends in your data and improve your workflow.
![Insights around Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/insights-3.png)
If you’d like to see additional metrics that aren’t displayed in our Insights page or to receive an export of the data to pipe into your own BI tool, please send us an email at [help@plain.com](mailto:help@plain.com).
## The Metrics
Now, the nerdy bit - this is a breakdown of the exact metrics we show in Insights and how they’re calculated. **Note -** All metrics can also be broken down by Channel, Company, Group, Label, Priority and Tier.
**Support volume**
| Metric | Definition |
| ----------------------------- | ---------------------------------------------------------------------------- |
| Queue size | A snapshot of the number of threads in todo over time, updating hourly. |
| New threads created per day | A count of new threads being created. |
| Threads re-opened per day | A count of threads that transition back to Todo from either Snoozed or Done. |
| Threads moved to Done per day | A count of threads transitioned to Done. |
**First response time**
| Metric | Definition |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| First response time - Median | The median time it takes for your team to send the first reply. |
| First response time - 90th percentile | The 90th percentile time it takes for your team to send the first reply for support requests that start off slower. |
**Resolution time**
| Metric | Definition |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Resolution time - Median | The median time between the first inbound message and the last outbound message for threads marked as Done. |
| Resolution time - 90th percentile | The 90th percentile time between the first inbound message and the last outbound message for threads marked as Done. |
# Discourse Integration
With Plain you can connect your Discourse community and automatically keep track of support requests via Plain.
This works by deploying a small node service which uses Plain and Discourse's APIs. You can either self-host this little node service or we can deploy this for you.
**[Check out the example on Github →](https://github.com/team-plain/example-discourse-integration)**
This integration can be customised depending on the structure of your Discourse community and ideal support workflow. For example you could customise the priority, labels and assignee of support requests from Discourse based on their category, author and content.
If you'd like help getting this set up, just reach out to us via Plain or on [help@plain.com](mailto:help@plain.com).
# Help Scout Sync
To connect your Help Scout account, head to **Settings** → **Help Scout importer** and follow the instructions.
You can also sync multiple Help Scout mailboxes to Plain if you need to.
When you connect your Help Scout account with Plain, we will import:
* all your end users as Plain [**customers**](/api-reference/graphql/customers)
* all your conversations as Plain [**threads**](/api-reference/graphql/threads). We will import all conversation threads as messages, and all internal notes. We'll map every HelpScout conversation status to an equivalent Plain thread status.
* all the conversation tags as Plain [**labels**](/api-reference/graphql/labels)
* all attachments that were previously added to conversations in Help Scout, will be added to threads in Plain
After you connect Help Scout for the first time, we will import all your existing data.
From that point, any new new end user, conversation, thread, note, or tag created in your
Help Scout account, will be automatically synced into Plain and kept up-to-date.
To stop syncing data from Help Scout, click on **Settings** → **Help Scout importer** -> **Disconnect**.
The data created through the Help Scout importer will not trigger any webhooks or auto-responders.
The Help Scout importer is available on [Scale tier](https://www.plain.com/pricing) only – please [reach out for a demo](https://38j36lhg2hq.typeform.com/to/zRvsFJPO) if your interested.
We will only create new records in Plain, we will never update existing ones:
* once an end user is synced into Plain, further changes to that end user in Help Scout (e.g. changing their name) will not be synced into Plain.
* if a customer or label is modified on Plain after syncing them from Help Scout, further syncs from Help Scout will not override your changes on Plain.
* the same applies to threads and notes.
# HubSpot Sync
HubSpot Importer is only available in our [Scale tier](https://plain.com/pricing). If you are
interested in using this feature please reach out to use on
([sales@plain.com](mailto:sales@plain.com))\[mailto:[plain@sales.com](mailto:plain@sales.com)].
To connect your HubSpot account, head to **Settings** → **HubSpot importer** and follow the instructions. It will take a couple of clicks.
When you connect your HubSpot account with Plain we will import:
* all your contacts as Plain **customers**
* the companies linked to those contacts as Plain [**tenants**](/tenant-support)
After you connect for the first time, we will import all your existing data. From that point, any new connection or account that you create in your HubSpot account, will be automatically synced into Plain.
The data created through the HubSpot importer will not trigger any webhooks or auto-responders.
To stop syncing data from HubSpot, click on *Disconnect* in **Settings** → **HubSpot importer**.
We will only **create** new records in Plain, we will **never update** existing ones:
* once a contact is synced into Plain, further changes to that contact in HubSpot (e.g. changing their name) will not be synced into Plain
* if a customer or tenant is modified **on Plain** after syncing them from HubSpot, further syncs from HubSpot will not override your changes on Plain
# Jira integration
To easily keep track of feature requests and bugs reported by your customers in Plain, you can connect your Jira workspace to Plain.
This lets you easily link a Jira issue to a thread, and ensures you're always prompted to reply to a customer when their bug is fixed or feature request is shipped.
To do this, head to Settings → Jira integration (under the Integrations section) and connect your workspace.
Once you've linked your Jira workspace to Plain, you can create a Jira issue (or link an existing Jira issue) directly to a customer request in the sidebar on the right hand side of a thread – or by pressing the shortcut: `i`.
Once a Jira issue is linked, the thread will move back to Close the loop when the Jira issue is completed, canceled or deleted.
**Coming soon:** you'll be able to get a ranked view of your top customer requests from Jira directly in Plain. You'll also be able to dig into each request, to understand which customers requested each feature, the tier breakdown of these customers and when the request was initially made. Please reach out to us if you'd like a demo of this feature.
# Linear integration
![Screenshot of the floating contact form](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/linear-introduction.png)
To easily keep track of feature requests and bugs reported in Plain, you can connect your Linear workspace with Plain.
This lets you easily link a Linear issue to a thread – and ensures you're always prompted to reply to a customer when their bug is fixed or feature request is shipped.
To do this, head to **Settings** → **Linear integration** and connect your workspace.
Once you've linked your Linear workspace to Plain, you can create a Linear issue (or link an existing Linear issue) directly to a customer request in the sidebar on the right hand side of a thread – or by pressing the shortcut:`i`.
Once a Linear issue is linked, the thread will move back to `Close the loop` when the Linear issue is completed or canceled.
Once you connect your Linear account and start linking issues to customer requests, you'll get access to a new Linear issues page in Plain – where you'll be able to get a ranked view of your top customer requests from Linear directly in Plain. You can also dig into each request, to understand which customers requested each feature, the tier breakdown of these customers and when the request was initially made.
# Salesforce importer
To connect your Salesforce account, head to **Settings** → **Salesforce importer** and follow the instructions. It will take a couple of clicks.
When you connect your Salesforce account with Plain, we will import:
* all your contacts as Plain **customers**
* the accounts linked to those contacts as Plain [**tenants**](/tenant-support)
After you connect for the first time, we will import all your existing data. From that point, any new connection or account that you create in your Salesforce account, will be automatically synced into Plain.
The data created through the Salesforce importer will not trigger any webhooks or auto-responders.
To stop syncing data from Salesforce, click on *Disconnect* in **Settings** → **Salesforce importer**.
We will only **create** new records in Plain, we will **never update** existing ones:
* once a contact or account is synced into Plain, further changes to that contact in Salesforce (e.g. changing their name) will not be synced into Plain
* if a customer or tenant is modified **on Plain** after syncing them from Salesforce, further syncs from Salesforce will not override your changes on Plain
# Zendesk importer
To connect your Zendesk account, head to **Settings** → **Zendesk importer** and follow the instructions. You will need your *Zendesk subdomain*, e.g. `plain` in `https://plain.zendesk.com/`
When you connect your Zendesk account with Plain, we will import:
* all your end users as Plain [**customers**](/api-reference/graphql/customers)
* all your tickets as Plain [**threads**](/api-reference/graphql/threads). We will import all messages in the tickets, including internal ones, which are imported as Plain **notes**. Tickets in status *closed* or *solved* will be imported as *DONE* threads. All others will be imported as *TODO* threads.
* all the ticket tags as Plain [**labels**](/api-reference/graphql/labels)
After you connect for the first time, we will import all your existing data. From that point, any new end user, ticket, message or tag created in your Zendesk account, will be automatically synced into Plain.
The data created through the Zendesk importer will not trigger any webhooks or auto-responders.
To stop syncing data from Zendesk, click on *Disconnect* in **Settings** → **Zendesk importer**.
We will only **create** new records in Plain, we will **never update** existing ones:
* once an end user is synced into Plain, further changes to that end user in Zendesk (e.g. changing their name) will not be synced into Plain
* if a customer or label is modified **on Plain** after syncing them from Zendesk, further syncs from Zendesk will not override your changes on Plain
# Can I forward emails to Plain?
No. If you receive an email to your personal email address and would like to hand it off to support what you can do is **CC** your support email.
For example, if [help@acme.com](mailto:help@acme.com) is your support email, your email exchange could look like this:
***
**From**: [jane@gmail.com](mailto:jane@gmail.com)
**To**: You
> Can you help me with A, B & C.
***
**From**: You
**To**: [jane@gmail.com](mailto:jane@gmail.com)
**Cc**: [help@acme.com](mailto:help@acme.com) 👈
> Hey Jane, sure thing. Just looping in our support team here (CCd) who can help you out!
***
# Can I connect my personal email address to Plain?
Technically you can connect any email to Plain if you control the domain (e.g. not @hotmail.com) but we don't recommend using a personal email. The reason for this is that emails are visible to all users of Plain in your workspace.
Visibility aside, we are focused on providing support via email and so Plain is not well suited for personal email address workflows.
# How long does it take to set up Plain? Do I need to have someone technical do it for me?
If you are setting up Plain to handle your Slack support it only takes a few minutes. You have to add a Slack app to your workspace and then invite Plain to the channels you want to handle. For detailed instructions [read the docs](/slack).
The Plain App will walk you throught this process or we can help you onboard in a quick 15 minute call.
If you want to set-up Plain via email you need a bit more time and need to be able to add DNS records to your domain. For more info on how to set up email, [read the docs](/email).
# Are you SOC2 certified?
Yes. We are SOC2 Type II compliant. On request we can share our lates SOC2 report as well as our latest pen test and other associated documentation.
We are also GDPR compliant. You can read more high-level information on our security approach [here](/security).
# Knowledge Base
**Beta**: We're just in the process of building out our knowledge base. A lot more coming soon
### Setup
* [How long does it take to set up Plain? Do I need to have someone technical do it for me?](./kb-setup-time)
### Email
* [Can I connect my personal email address to Plain?](./kb-personal-email)
* [Can I forward emails to Plain?](./kb-forwarding-emails)
### Security
* [Are you SOC2 certified?](./kb-soc2)
# Labels
![Example labels](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/labels-introduction.png)
Labels are a lightweight and powerful way to categorize threads in Plain.
We recommend categorizing threads into topics so you immediately know, at a glance, what people are getting in touch about.
For example:
* "Bug report"
* "Sales" or "Demo request"
* "Feedback" or "Feature requests"
* "API question"
* "Security"
* "Hiring"
* etc.
In combination with setting thread priorities, labels can unlock powerful workflows and let you quickly triage your threads to work on what matters most, first.
To configure your labels, press ⌘ + K and search for "Manage labels" or go to **Settings** → **Labels**. Here you can define the icon and name your labels.
A thread can have multiple labels. They are displayed throughout the Plain UI wherever threads are visible.
In many cases, it's also useful to add labels to threads programmatically.
[**Jump to documentation →**](/api-reference/graphql/labels)
# Microsoft Teams
**Never miss another message from a customer in Microsoft Teams**
Microsoft Teams is available on the [Scale pricing plan](https://www.plain.com/pricing).
If you are interested in trying this feature, please reach out to us at [sales@plain.com](mailto:sales@plain.com) or via our shared Slack channel.
Microsoft Teams support is something we recently shipped. As you'd be among our early adopters, we'd love for you to [book a demo with us](https://38j36lhg2hq.typeform.com/to/zRvsFJPO?typeform-source=www.plain.com) so we can show you around.
Our Microsoft Teams integration lets you sync messages from selected Teams channels to Plain and respond directly to customers from the platform.
To get started you need to:
* Create a team and a channel in Microsoft Teams
* Install the Plain app in Microsoft Teams
* Invite guests
Once this is set up, the integration allows you to:
* Respond to teams messages in Plain
* Sync messages from selected Teams channels to Plain
* Reply as your Teams user from Plain
Here's a quick demo of the Plain Microsoft Teams integration in action, to get started, see the [installation guide](./microsoft-teams/installation).
# Installation
Microsoft Teams support is only available in our [Scale tier](https://www.plain.com/pricing). If
you'd like to use our Teams integration to consolidate your support stack – [you can book a demo
now](https://38j36lhg2hq.typeform.com/to/zRvsFJPO?typeform-source=www.plain.com). We'd love to
show you around.
Our Microsoft Teams integration lets you sync messages from selected Teams channels to Plain and respond directly to customers from the platform.
### Prerequisites
For the integration you'll need:
* A Teams account with at least one seat
* The installation of the app for Teams can only be performed by an admin as you may need to change settings to:
* [Allow external apps to be installed](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/prepare-your-o365-tenant#enable-custom-teams-apps-and-turn-on-custom-app-uploading)
* [Allow external users to be added as guests to be invited to Teams](https://learn.microsoft.com/en-us/microsoft-365/solutions/collaborate-as-team)
## Installation
* In Plain, navigate to **`Settings`** > **`Microsoft Teams`**.
* Press **`Connect to Teams`**.
* Follow the instructions in the pop-up window to sign in to your Microsoft account, make sure you've selected the correct
Microsoft account if you have multiple.
* Once the authorization is complete, you can download the Plain app for Teams (used in the next step).
* Optional: Authorize replies from your Teams profile to respond directly from Plain as yourself.
The method of installation here will depend on how Teams is set up for your organisation. If you are having trouble
with setting this up, please [get in touch so we can help](mailto:help@plain.com)!
#### Option 1 - Install via Teams App
* Navigate to the Apps tab
* Click **Manage your apps**
* Click **Upload an app**
* As a Teams admin, you should be able to **Upload an app to your organization's app catalogue** and upload the .zip file you downloaded in the previous step
#### Option 2 - Install via the [Teams Admin Dashboard](https://admin.teams.microsoft.com/dashboard)
* In the left sidebar, navigate to **Teams apps → Manage Apps**
* Under Actions, click **Upload new app**
* Upload the .zip file you downloaded in the previous step
## Using the Plain app in Microsoft Teams
The Plain app must be added to a Team and channel that allows guests where you can provide customer support. To start helping customers, you need to do a few things:
If the app was installed via the Teams admin dashboard, it can take upto 24 hours to appear in the
Teams client
* Create a new Team in your Teams instance
* Create a new channel in the Team
* Add the Plain app to the Channel (see below)
* [Invite guests to the channel as external guests](https://support.microsoft.com/en-gb/office/add-guests-to-a-team-in-microsoft-teams-fccb4fa6-f864-4508-bdde-256e7384a14f)
You must be the owner of the channel you want to add the Plain app to. Customers you are providing support to will be able to access this channel in your workspace. Currently, shared channels and B2B Direct connect channels are not supported.
Setting up the app:
## How threads are created from Teams
You should now have a channel with the Plain app installed, with *users* and *guests*. The integration works as follows
* Only posts **created by guests** will create a new thread in Plain
* Reply messages to threads **created by guests** will be ingested by both **users and guests**
## Responding to customers
To respond to customers, you can do so directly from the Plain app in Teams.
If the user has a personal Teams integration (see below) then the message will be sent as that user:
![User Reply](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ms-teams-int-user-reply.png)
If the user does not have a personal Teams integration, we will send the message as the bot, impersonating the user, which will look like the below:
![Impersonated Reply](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/ms-teams-int-impersonated-reply.png)
## Authorize Teams users in Plain
Its recommended that anyone with a Teams seat follows the below steps to allow Plain to reply as their teams user. It also allows attributing any messages they send in the Teams channel to be associated with their Plain user.
To link a Teams account with a Plain user account, navigate to **`Settings`** > **`Microsoft Teams`** and click, **`Authorise replies`** to start the authorization flow.
# Notifications
![Notifications within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/notifications-introduction.png)
Plain supports Discord, Slack and Email notifications. Notifications are also divided into **Workspace Notifications** and **Personal Notifications**.
### Workspace notifications
Workspace notifications are global to your workspace via Slack and/or Discord. They are typically used to notify you as a team of a new support request or other activity.
We recommend setting up a shared channel like #support and then having all workspace notifications go there.
You can configure workspace notifications in **Settings** → **Workspace**.
### Personal notifications
Personal notifications are via Slack or Email. We recommend setting up notifications here to be notified of threads you are assigned to.
You can configure personal notifications in **Settings** → **Workspace**.
When you use personal Slack notifications they will be delivered in the Plain Slack App direct message channel with you.
# Plain AI
Quickly categorize and label new threads without lifting a finger. To turn on Plain’s AI features, go to **Settings → Plain AI.**
All AI features in Plain are opt-in. More information on our use of OpenAI and our data processing policies can be found in our [DPA](https://www.plain.com/legal/dpa).
![Plain AI](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/plain-ai.png)
**Auto-labelling**
When a thread is created, Plain AI will add 1-2 labels automatically to a thread based on its content. If a thread was created with existing labels, no labels will be added. To best leverage Plain AI, make sure you’ve already set up [Labels](/labels) (**Settings → Labels**).
**Thread titles**
When enabled, this will automatically give each thread a short, descriptive title based on the content of the support request. This is particularly useful in Slack and other channels which, unlike email, do not have a subject line you can rely on.
**Thread summarization**
As a thread grows over time, a summary of the thread will be added and kept up-to-date so you know what has happened without having to read the full timeline and every message. You can see the summary on the top right of any thread and in Slack notifications.
# Priorities
![Priorities within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/priority-introduction.png)
Every thread has a priority. Priorities help you manage your support queue and identify which threads need attention first.
The available priorities are:
* **Urgent**
* **High**
* **Medium**
* **Low**
When you are viewing a thread you can use the shortcut **P** to set the priority or use **⌘ + K**.
When looking at your thread queue you can also filter and sort your threads by priority.
Priorities can be useful in combination with contact forms. By asking questions such as "is this preventing you from using X?" you can determine whether bug reports or questions are high priority.
Equally, you can automatically set higher priorities for certain topics like security reports, if your contact form exposes a topic picker.
# Quickstart
Everything you need to know to start supporting your customers with Plain.
![Plain.com](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/quickstart.png)
Plain is the fastest, most powerful support platform for technical support teams. It's got everything you need to help your customers in a modern, consolidated, and collaborative platform.
This quickstart guide will walk you through how to set up your workspace and start helping your customers.
### The basics
Everything in Plain happens in your team's workspace. To create a workspace for your team, sign up at [https://app.plain.com](https://app.plain.com) and click **Create a workspace**. Please note: you'll need to jump on a quick onboarding call with us before you can create a workspace.
To start answering customer requests in Plain, you should begin by connecting your Slack, email, or Chat:
**Slack** - Our Slack integration lets you sync messages from selected Slack channels to Plain and respond directly to customers from the platform. [Learn more about setting up Slack with Plain](/slack).
**Email** – You can link your support email and start receiving customer request in minutes. For more information on how to connect your email addresses to Plain, [check out the docs](/email/)
**Chat** – Our Chat widget lets you embed a live chat interface on your website or app, allowing your customers to reach out to you without leaving your site. Please note: Chat is currently in beta and we're shipping new updates regularly. [Learn how to set up Chat.](/chat)
You can also build a totally on-brand [support portal](/headless-support-portal/) and [in-app forms](/contact-forms/) to match your branding and UI, and structure your support queries.
To add team members to your Plain workspace and assign roles, navigate to **Settings** →
**Members** in your workspace. You can add your entire team to Plain for free - we'll only charge
you for users that actively send messages to customers.
To make sure your team gets notified of new support requests, [set up notifications](/notifications).
We recommend, as a minimum, setting up one shared Slack or Discord channel where new support requests are posted. We also recommend setting up personal notifications so you can be notified of new replies and activity on threads you are assigned to.
[Connecting your Linear workspace](/integrations/linear) to Plain will let you quickly and seamlessly log bugs and feature requests to Linear without leaving Plain and then close the loop with the customer once they're completed.
You can connect to Linear by selecting your workspace name in the top left hand side of your Plain workspace, and selecting **Settings** → **Linear**.
### Speed up your workflow
Labels are a lightweight but powerful way to categorize threads in Plain. You can configure the labels that make sense for you in **Settings** → **Labels**.
[**Add labels**](/labels/)
Snippets are templated messages that allow you to pull common language to message customers more
quickly. You can configure them in **Settings** → **Snippets**
[**Set up snippets**](/snippets/)
An autoresponder can be useful in situations of acute support load, such as during incidents, or to confirm you received a support request and manage expectations with customers.
[**Set up auto-responses**](/auto-responses/)
We make it easy to fly through your workflow without ever needing to use a mouse. Learn about our
keyboard shortcuts here. We've also added hints throughout the app on which keyboard shortcuts to
use.
[**View all shortcuts**](/shortcuts/)
Use workflow rules to help you automate common actions your team takes. With customizable conditions and actions, you can automate repetitive tasks and streamline your team’s processes.
[**Learn about workflow rules**](/workflow-rules/)
### Organize your workspace
In Plain you can see what company a customer belongs to, so you have more context when providing support.
[**Learn more about companies**](/company-support/)
You can organize your customers to mirror how your product is structured. For example, if in your product all of your customers belong to a team/org/account/workspace then you would create a tenant per team/org/account/workspace.
[**Learn more about tenants**](/tenant-support/)
Tiers add support for defining SLAs so you can enforce a first-response time for different support tiers within your product or pricing.
[**Learn more about tiers**](/data-model/)
When configuring an SLA you can set when you want to be warned of a breach. For example if your first response time SLA is 4 hours, you might want to be notified 30 minutes before a breach so you can still reply in time. They can be set for first response time and next response time.
[**Learn more about SLAs**](/tiers#slas/)
By default, SLAs apply at all times. They can be configured to only count working hours by toggling on Only during business hours. To configure business hours go to Settings → Business hours.
[**Learn more about business hours**](/tiers#business-hours/)
### Adding context
To provide support more quickly, get more context into Plain from your own systems.
Customer cards let you show live information from your own systems in Plain. This lets you bring important, business-specific context to Plain and makes it even easier to help customers without jumping through different tabs.
[**Set up customer cards**](/customer-cards/)
Events let you log important customer actions, errors, releases, and other key events to Plain. This gives you the full picture in the context of a support request as to what happened and why.
[**Set up events**](/events/)
To be able to keep track of additional information related to a support request, you can configure additional custom fields you want to store on a thread (like product area, needs followup, Github issue etc.).
[**Learn more about thread fields**](/thread-fields/)
Plain is built API first so that you can build countless other use cases into your support stack.
[Learn more about our API](/api-reference/graphql/) and make your first API call.
### Feedback & questions
If you have any feedback on our docs, we'd love to hear it! Open an issue straight in [Github](https://github.com/team-plain/docs) or drop us an email
at [help@plain.com](mailto:help@plain.com) and an engineer on the team will help you.
# Roles & Permissions
Below you'll find the roles available within your Plain workspace. Set your team's roles depending on the level of access and ownership they need.
* `Owner`: Full access to everything, including billing, deleting workspaces, managing workspace settings and API keys.
* `Admin`: Access to everything in Plain except for billing and deleting workspaces.
* `Support`: Can message customers and use all in-app features.
* `Viewer`: Can view all support requests and participate in internal discussions, but cannot send messages to customers via Plain. Viewer seats are completely free, and unlimited.
**Note**: Owner, Admin, and Support seats are paid seats in Plain, while viewer seats are free. [View our full pricing breakdown here](https://www.plain.com/pricing)
You can view and update existing user's roles or add new users from **Settings → Members**.
You can also manage billing directly from Plain. Navigate to **Settings → Billing** to view, or make changes to your billing settings.
# Security
If you or your team have specific questions about how Plain is built, our processes or how we store and handle data please get in touch at [help@plain.com](mailto:help@plain.com).
We are very happy to answer any questions you have.
## SOC 2 Type II
Plain has completed a [SOC 2 Type II](https://www.aicpa-cima.com/topic/audit-assurance/audit-and-assurance-greater-than-soc-2) certification.
Achieving SOC 2 compliance means that Plain has implemented procedures, policies and controls necessary to meet AICPA's trust services criteria for security, availability, and confidentiality and that these processes and controls have been tested to ensure that they are operating effectively.
Obtain a copy of the report by emailing us at [help@plain.com](mailto:help@plain.com).
## Data security
* We use Amazon Web Services to host Plain
* All data is stored in Amazon Web Services `eu-west-2` (London) region
* All data is encrypted in transit and at rest
* All data is backed up regularly and encrypted at rest
* We apply the following security best practices:
* All changes to our infrastructure, permissions, and code happen via code reviews
* We grant the least amount of privileges to IAM roles, systems, and engineers to perform their duties
* Administrator privileges are only used in the case of serious incidents, for routine maintenance tasks we provision IAM roles with fine-grained permissions.
* We carefully evaluate 3rd party vendors before using them, regularly review them and the data they can access. Please see the [Data Processing Addendum](https://www.plain.com/legal/dpa/) for the full list of vendors we use.
## Request signing
Outbound requests we make to your target urls provide a HMAC signature with a shared secret key. Please see the [Request signing](/api-reference/request-signing) documentation for more information.
### Reporting an issue
If you think you found a security issue or have any questions related to security please email us at **[security@plain.com](mailto:security@plain.com)**.
Please keep your report concise, add steps to reproduce, and include a proof of concept if possible.
We will acknowledge valid reports within 48 hours of receipt. Please avoid following up more than once every 72 hours to allow our team to focus on fixing any issues.
### Guidance
We reward a bounty to security researchers who have adhered to this policy and found a confirmed high-severity vulnerability on a case-by-case basis.
You must not:
* Break any applicable law or regulation
* Access unnecessary, excessive or significant amounts of data
* Modify data in Plain systems or services
* Use high-intensity invasive or destructive scanning tools to find vulnerabilities
* Attempt or report any form of denial of service, for example; overwhelming a service with a high volume of requests
* Disrupt the Plain services or systems
* Submit reports detailing non-exploitable vulnerabilities, or reports indicating that the services do not fully align with “best practice”, for example missing security headers
* Submit reports detailing TLS configuration weaknesses, for example “weak” cipher suite support or the presence of TLS1.0 support
* Communicate any vulnerabilities or associated details other than by means described in this policy
* Social engineer, 'phish' or physically attack Plain staff or infrastructure
* Demand financial compensation in order to disclose any vulnerabilities, **or threaten the public disclosure of a vulnerability unless payment is made**
You must:
* Always comply with data protection rules and must not violate the privacy of any data Plain holds. You must not, for example, share, redistribute or fail to properly secure data retrieved from the systems or services
* Securely delete all data retrieved during your research as soon as it is no longer required or within 1 month of the vulnerability being resolved, whichever occurs first (or as otherwise required by data protection law).
If you follow these guidelines when reporting an issue to us, we commit to:
* Not pursuing or supporting any legal action related to your research
* Working with you to understand and resolve the issue quickly (including an initial confirmation of your report within 48 hours of submission)
# Shortcuts
Keyboard shortcuts within Plain make it really fast to help customers and stay in the flow.
This is the list of available keyboard shortcuts in Plain:
#### Global shortcuts
* **⌘** + **K** - Open the command palette
* **?** - The full list of available shortcuts
#### On thread queues
* **F** - Add filter
* **/** - Search for customers or threads
#### When viewing a thread
* **A** - Assign the thread
* **P** - Change thread priority
* **N** - Add a note to a thread
* **E** - Mark a thread as `Done`
* **Z** - Snooze a thread
* **!** - Mark customer as spam
* **⌘ + .** - Show/hide queue
* **⌘ + /** - Show/hide customer sidebar
* **R** - Focus composer to reply
* **J**/**K** - Next/Previous thread
* **Esc** - Go back to queue
* **L** - Add label
* **I** - Add linear issue
#### When the composer is focused
* **⌘ + Enter** - Send message
* **⌘ + Shift + Enter** - Send message and mark thread as done
* **\[** - Insert snippet
* **Esc** - Unfocus/blur composer
# Slack
**Never miss another message from a customer in Slack**
Our Slack integration lets you sync messages from selected Slack channels to Plain and respond directly to customers from the platform.
You will also be able to [add context to Slack messages with customer cards](/customer-cards), [set labels](/labels) and priorities, [create Linear tickets](/integrations/linear), and much more.
Here’s how to get started:
* In Plain, navigate to **`Settings`** > **`Slack`**.
* Press **`Connect to Slack`**.
* Follow Slack’s instructions to make sure the correct Slack workspace is selected.
* Authorize replies from your Slack profile to respond directly from Plain.
Once you’ve set up the integration in Plain, go to the Slack channels you want to track and invite the Plain app. You can do this by typing **`/invite`**, selecting **`Add apps to this channel`** and then choosing Plain.
When Plain is added to a channel, we automatically classify it as a Customer channel if it's a *Slack Connect* channel or one for [internal discussions](/thread-discussions) otherwise.
For customer channels, we treat messages from non-Plain users as support requests, which are ingested as threads into Plain.
In discussion channels, on the other hand, we only ingest messages related to an on-going discussion.
Slack messages from the authorized channels will begin appearing as threads in Plain. To respond, press `R` and begin typing in Plain. Your responses will appear in Slack as if you are responding directly in the app.
Emojis ( **`:`** to search for the right one), syntax highlighting, and attachments are all supported in Plain.
**Note -** Slack messages sent by a Slack user that does **not** have an account in Plain (i.e. a customer) will create threads in Plain.
# Data Retention
When you add Plain to a Slack channel, Plain stores information from that Slack channel. This is to enable you to provide support on Slack through Plain.
Plain only stores information from Slack channels you add the Plain bot to, so you are in control of what gets added to Plain.
When the Plain app is added to a channel, Plain will begin to store the following Slack data:
* Message contents
* Message metadata (e.g. timestamps, ids, reactions etc.)
* Channel members (used for @ mentioning and showing the author of messages)
* Channel metadata (e.g. name, id, connected team ids etc.)
Any additional data that Plain receives from Slack's APIs but that is not required for the operation of the service is automatically deleted after a 7 day period. The 7 day period allows us to recover messages in the case of an incident.
When a message is deleted within Slack, Plain will delete the corresponding message content in Plain. Plain will keep a record that there was a message and who wrote it, but not its contents. This ensures that there remains an audit trail for every support request, even if a message is deleted.
# Ingestion Modes
There are multiple ingestion modes you can choose from for each of your Slack workspaces. Ingestion modes control how and which messages in Slack are added to Plain.
You can configure them by going into **`Settings`** > **`Slack`**, choosing any of your Slack integrations and selecting one of the ingestion modes we have available.
Here's a brief description on what they are and how they differ.
## Time-Based (Default)
Plain ingests and groups Slack messages automatically for you based on the time between them.
If two messages are sent less than two hours apart by the same user, they are grouped into the same Plain thread.
On the contrary, if messages are sent by different users, they will be grouped if they are at most one hour part.
## AI-Based
If you're looking for a more intelligent grouping logic, you can enable AI Grouping.
When messages come in, they will be triaged by OpenAI's GPT-4 model for similarity.
If messages are determined to be part of the same conversation, they will be grouped into the same thread.
Otherwise, new threads will be created for each of the messages.
## One-to-one
If you want to always associate a Plain thread to a single Slack thread, you can enable One-to-one mode.
Every new message from a customer on a channel will start a new Plain thread. Any subsequent messages within the Slack thread will be appended to the Plain thread.
## Manual
There are use-cases where certain channels are too noisy and you don't want all the messages to go into Plain.
Or perhaps you want more granular control over which Slack threads end up in Plain.
For that purpose, you have the option to enable **Manual Ingestion Mode**. When enabled, Plain will only pick up the Slack threads you specify by adding an emoji reaction.
Some considerations to bear in mind:
* A message will be added to Plain only if the reaction is by a Plain workspace user
* You need to add the reaction on the channel message, rather than any of the messages in the Slack thread
* Removing the reaction will not delete the thread in Plain. Furthermore, adding it multiple times will not create multiple threads.
# Smart AI Replies
This feature is currently in Beta and is not available to all customers. If you are interested in
trying this feature, please reach out to us at [help@plain.com](mailto:help@plain.com) or via our
shared Slack channel.
Smart AI Replies allow you to generate replies to support requests based on your documentation and the contents of a thread.
![Smart AI Replies](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/smart-ai-replies.png)
Replies are not automatically sent, the composer will be populated with the suggested reply, which you can either send as is, or edit as you wish.
To enable Smart AI Replies go to **Settings** → **Plain AI** and toggle the **Smart AI Replies** setting.
All AI features in Plain are opt-in. More information on our use of OpenAI and our data processing
policies can be found in our DPA.
### Indexing documentation for use with Smart AI Replies
Documentation can be indexed by using the [Plain CLI](https://github.com/team-plain/cli). With it you can index:
* a single URL
* a sitemap URL (each entry in the sitemap is indexed)
For installation and usage instructions, please see the [Plain CLI docs](https://github.com/team-plain/cli).
### Automation
To keep indexed documents up to date, you can automate the indexing process the Plain CLI on a schedule.
#### Github Actions example
Here is an example Github Action to re-index the documents using the [Plain CLI](https://github.com/team-plain/cli) run on a schedule.
* The action runs every 3 hours
* The action uses the `PLAIN_API_KEY` secret to authenticate with the Plain API
* The action indexes all URLs in the sitemap
The `PLAIN_API_KEY` requires the following permissions:
* `indexedDocument:create`
```yaml
name: Index docs
on:
schedule:
- cron: '0 */3 * * *'
jobs:
lint:
name: Index Documents
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install CLI
run: npm install -g @team-plain/cli@latest
- name: Index Documents
run: plain index-sitemap https://www.plain.com/docs/sitemap.xml
env:
PLAIN_API_KEY: ${{ secrets.PLAIN_API_KEY }}`
```
# Snippets
![Snippets within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/snippets-introduction.png)
Snippets are templated messages in Plain that you can quickly combine to help customers more quickly.
You can configure your snippets in **Settings** → **Snippets**.
You can then press **\[** to insert a snippet when writing in a thread.
Snippets can either be entire messages or short re-usable bits.
For example:
* Common links such as to your docs, status page, calendar booking links etc.
* Re-usable apologies and greetings such as "So sorry to hear about, let me look into that." or "Is there anything else I can help you with?"
* Common instructions and how-to's to your product
Within snippets you can also use some variables to include dynamic data. The following variables can be used:
* `{{ customer.email }}`: The customer's email address.
* `{{ customer.fullName }}`: The first and last name of the customer.
* `{{ customer.shortName }}`: If set, the short/first name of the customer.
* `{{ user.publicName }}`: Your public name (useful for signatures).
# null
In Plain all support requests are organised in threads, and every thread has a status.
This page explains each status, its purpose and best practices for how each fits into your workflow.
## Needs First Response
![Todo / Needs First Response](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/statuses/todo-needs-first-response.png)
When a thread is first created it will be in this status. This means that the customer has never received a reply. Any [First Response Time SLAs](/tiers#slas) you have set up apply to these threads.
## Needs Next Response
![Todo / Needs Next Response](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/statuses/todo-needs-next-response.png)
When a customer replies to you, threads will automatically move to this status. Any [Next Response Time SLAs](/tiers#slas) you have set up apply to these threads.
## Investigating
![Todo / Investigating](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/statuses/todo-investigating.png)
When you have a support request that requires further\*\* \*\*work on your side, you can choose to set a thread to this status.
This is particularly useful when you have support request that requires longer technical investigation or research before getting back to the customer or marking it as done.
## Close the Loop
![Todo / Close the Loop](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/statuses/todo-close-the-loop.png)
If you use our [Linear Integration](/integrations/linear) then threads will move to this status when there was an update to a linked Linear issue.
If you use [Discussions](/thread-discussions), then threads will move to this status when a discussion is resolved.
## Waiting for Customer
![Snoozed / Waiting for Customer](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/statuses/snoozed-waiting-for-customer.png)
Threads in this status are waiting for the customer to reply. By default, threads will be automatically moved to this status when you reply to them. You can control this behaviour by going to **Settings** → **Workflow**
When a customer replies, threads in this status will be moved to Needs Next Response.
## Paused for Later
![Snoozed / Paused for Later](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/statuses/snoozed-paused-for-later.png)
When you need to pick up a thread later, you can pause it for any amount of time. Once paused, threads will have this status.
When the pause duration you set runs out, or if the customer sends a new message, the thread will be moved back to its previous status.
## Done
![Done](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/statuses/done.png)
When a customer issue is resolved, you can mark a thread as done. If a customer sends a new message the thread will move back to Needs Next Response.
## Ignored
![Ignored](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/statuses/ignored.png)
When a thread in Plain is not a support request, you can ignore it with this status. This will silence further notifications and status changes.
This is useful when there are non-support conversations (e.g. sales, social) happening in Slack channels where you provide support.
# Syntax highlighting
![Syntax highlighting within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/syntax-highlighting-introduction.png)
Within Plain, you can quickly and easily share code with customers using full syntax highlighting for all common languages.
In a message use 3 backticks ` ``` ` to start a code block and then press **Shift** and **Enter** (or just **Enter** 3 times) to exit the code block.
The language will be automatically inferred but if you want to override this you can manually specify the syntax after the initial 3 back-ticks. For example:
````
```json
{ "status": 200}
```
````
will render:
```json
{ "status": 200 }
```
Syntax highlighting is **currently** only supported in email.
# Tenants
![Tenants within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/tenants-introduction.png){' '}
In Plain, in addition to [company support](/company-support), you can also organise your customers to mirror how your product is structured.
For example, if in your product all of your customers belong to a team/org/account/workspace then you would create a tenant per team/org/account/workspace.
Tenants are primarily useful if you are a larger support team building a headless support portal or if you have support SLAs tied to different tiers of customers.
Tenants are created and set [via the API](/api-reference/graphql/tenants/).
# Thread Discussions
![Example Thread Discussion](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/discussions-introduction.png)
Thread Discussions are available on the [Grow pricing plan](https://www.plain.com/pricing) and above.
If you are interested in trying this feature, please reach out to us at [sales@plain.com](mailto:sales@plain.com) or via our shared Slack channel.
Thread discussions allow you to lead conversations with your team about support requests in Slack.
To start a discussion you can either do it from within the thread in Plain or simply copy & paste the thread link to that channel.
We will then unfurl the link for you and populate the Plain thread context for your wider team to be aware of, without leaving Slack.
For your convenience, the discussion will also be visible in the Plain thread.
Any messages you or your team sends on the slack thread will show up in Plain, along with their reactions, mentions and attachments. You can respond either via Slack or directly through Plain.
When a discussion is started, Plain will post a message with a "Resolve discussion" button. That can be used by anyone to signal that the discussion is concluded and the Plain thread will be moved back to Todo.
To enable this functionality, you will need to setup your [Slack integration](/slack) and add the Plain bot to your channel.
When Plain is added to a channel, we automatically classify it as a [Customer channel](/slack) if it's a *Slack Connect* channel or one for internal discussions otherwise.
For customer channels, we treat messages from non-Plain users as support requests, which are ingested as threads into Plain.
In discussion channels, on the other hand, we only ingest messages related to an on-going discussion.
For good data hygiene, we don't start discussions on all links to a Plain thread.
We will not start a discussion if:
* the link is posted by someone who is not a user in Plain.
* the link is posted in a private channel Plain doesn't have access to.
* the link is posted in a customer channel (typically *Slack Connect*), although you can always adjust this from your Slack settings.
If you want to opt out completely, you can disable thread discussions and link unfurling in your Slack integration settings.
# Thread Fields
Extend and customise Plain's data model.
![Thread fields within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/thread-fields-introduction.png)
To be able to keep track of additional information related to a support request, you can configure additional custom fields you want to store on a thread.
To configure the schema for thread fields go to **Settings** → **Thread fields**
Thread fields can either be:
* A boolean (true or false)
* Text
* A dropdown (single select from a predetermined list)
Thread fields can also be nested and be conditional on answers from other fields.
Fields can also be marked as required. This is useful when there is some information you want to collect on every support request before it is marked as done.
Thread fields can also be **auto-filled via AI**. To enable this behaviour in the settings for any thread field enable "Autofill via AI". The description you provide for each field will be used to guide suggestions.
# Tiers & SLAs
![Tiers and SLAs within Plain](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/tiers-introduction.png)
Tiers & SLAs are available on the [Grow pricing plan](https://www.plain.com/pricing) and above.
If you are interested in trying this feature, please reach out to us at [sales@plain.com](mailto:sales@plain.com) or via our shared Slack channel.
Within Plain you can organize companies and tenants to match your pricing tiers (e.g. Enterprise, Pro, Free, etc.).
To manage your tiers go to **Settings** → **Tiers**.
Tiers can be managed manually or [via the API](/api-reference/graphql/tiers/).
### Priorities
Tiers can have a default priority. This allows you to, for example, set all Enterprise support requests to have a priority of "High". This means you can encourage prioritisation by tier (e.g. "Enterprise first, then Pro, then everyone else").
### Tier assignment
When a thread is created Plain will automatically assign a tier, with varying logic depending on the channel.
#### Threads created via API
When a threads is created via the API for a specific tenant, this is how the tier is selected:
* If the tenant is a member of a single tier we will use that tier
* If the tenant is a member of multiple tiers, we will use the tier with the highest default priority
* If the tenant is a member of no tiers we will use the default tier if one exists, or the thread will have no tier
If a thread is created with no tenant we will fall back to the logic below.
#### Email & Slack
For all email and Slack threads, the tier is applied based on the customer the thread belongs to. We look at all tenants and company the customer is part of, to determine the tier.
* If there is a single tier which is possible given the customer, that tier is used
* If there are multiple tiers we pick the one with the highest default priority
* If there are no tiers we will use the default tier if one exists, or the thread will have no tier
### SLAs
We currently support SLAs for:
* **First response time** - applied when a first customer communication is created within a thread
* **Next response time** - applied for subsequent customer communications (note: a first response time SLA must be configured to create a next response time SLA)
When configuring an SLA you can set when you want to be warned of a breach. For example if your first response time SLA is 4 hours, you might want to be notified 30 minutes before a breach so you can still reply in time.
SLAs can also be driven by priority of threads. This allows you to have a different SLAs for "Urgent" vs "Normal" priority threads. If you have configured multiple SLAs for the same priority then only the first SLA will apply.
If you want to create a 'default' SLA then you should first create a tier and make that the default in your settings. Then you can configure SLAs like you would on any other tier.
When an SLA is about to breach or is breaching, you will be notified via the mechanism you configure in your workspace notification settings.
We also provide the [Thread SLA status transitioned](/api-reference/webhooks/thread-service-level-agreement-status-transitioned/) webhook so that you can be notified programatically. This event will be fired when the status of an SLA linked to a thread changes (e.g. about to breach, breaching, achieved, etc.)
#### Business hours
By default, SLAs apply at all times. They can be configured to only count working hours by toggling on **Only during business hours**.
To configure business hours go to **Settings** → **Business hours**
When an SLA is configured to use business hours the timer on the SLA will be paused outside of business hours.
For example, if:
* Your business hours are Monday - Friday, 9am - 5pm
* An enterprise customer sends in a support email on Friday at 4.30pm
* Your enterprise First Response Time SLA is 1 hour
... then the SLA would breach if you don't reply by 9.30am on Monday.
This is because there are 30 minutes of business hours on Friday afternoon, after which your SLA is paused over the weekend. This is followed by a remaining 30 minutes of business hours from 9 - 9.30 am on Monday.
# Workflow Rules
![Workflow Rules](https://mintlify.s3-us-west-1.amazonaws.com/plain/public/images/workflow-rules.png)
Workflow Rules are available on the [Grow pricing plan](https://www.plain.com/pricing) and above.
If you are interested in trying this feature, please reach out to us at [sales@plain.com](mailto:sales@plain.com) or via our shared Slack channel.
You can use workflow rules to help you automate your team's processes.
Some common use-cases include:
* Assign someone to handle all support requests from a given company or tier (e.g. Enterprise)
* Set a threads' priority if it includes certain key phrases
* Assign threads based on the used support email (e.g. Give all security@ emails to Jane)
To manage your workflow rules go to **Settings** → **Workflow Rules**.
When building a new rule, you can choose among a set of **conditions** which result in one or more **actions**.
There are a couple of conditions you can choose from, based on the thread's attributes:
* a given label is applied
* is from a given company
* is from a given slack channel
* is from a specific support channel (Slack, MS Teams, chat, API)
* has a specific tier (ie Enterprise)
* is for a specific support address (ie [sales@yourcompany.com](mailto:sales@yourcompany.com))
* contains a certain keyword (ie vulnerability)
When the chosen condition matches a thread, you can perform any of the following actions:
* assign someone to the thread
* set the thread priority
* apply a given label
* add a customer to a customer group
* add a note to a thread