Webhooks
Webhooks allow you to get notified about events happening in your Plain workspace (Plain events). You can react to these events in many ways, such as:
- Assigning customers to workspace users based on business criteria (urgency, customer value, recurrency, etc.)
- Creating an AI-powered auto-responder
- Creating issues for customers based on their messages
- Triggering internal incidents (by identifying patterns in inbound messages)
- Tracking metrics from your customer support team
Receiving events from Plain
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.
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.
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.
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.
Webhook targets can also be managed programmatically. All four CRUD operations are available in our API: createWebhookTarget, webhookTarget, updateWebhookTarget and deleteWebhookTarget.
You can create up to 25 webhook targets per workspace.
Security
Webhook requests are always sent through HTTPS.
Additionally, the webhook target's URL may contain authentication credentials (https://username:password@example.com
)
which will be sent along the webhook request in an Authorization
header:
Authorization: Basic cGxhaW46cm9ja3M=
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 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 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 originatedPlain-Webhook-Target-Id
: The ID of the webhook target this webhook request is being sent toPlain-Webhook-Delivery-Attempt-Id
: The ID of the delivery attempt. It will be different on every delivery attemptPlain-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 typePlain-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.
If you prefer, you can browse the schema using this interactive tool.
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 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. See Plain Events for the list of available events |
Webhook Metadata
All the following fields are also sent as HTTP 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 |
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 |
Plain Events
This is a list of all the available Plain events, including how they are triggered an example webhook request body for each of them.
customer.customer_status_transitioned
🗒 Schema
Triggered when a customer's status has changed.
Example webhook request body
{
"timestamp": "2023-02-21T12:58:51.232Z",
"workspaceId": "w_01GST0W989ZNAW53X6XYHAY87P",
"payload": {
"eventType": "customer.customer_status_transitioned",
"previousCustomer": {
"id": "c_01GST0WAGQ7RSMMEXZGA360MFY",
"email": {
"isVerified": false,
"email": "colby45@example.com",
"verifiedAt": null
},
"externalId": "external_01GST0WAFACKR7NA5BH60W9RWA",
"fullName": "Colby Kunde",
"shortName": "Colby",
"assignedAt": null,
"assignedToUser": null,
"status": "IDLE",
"statusChangedAt": "2023-02-21T12:58:50.775Z",
"createdAt": "2023-02-21T12:58:50.775Z",
"createdBy": {
"actorType": "user",
"userId": "u_01GST0W90RKD7AXS783SDPKJMV"
},
"updatedAt": "2023-02-21T12:58:50.775Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01GST0W90RKD7AXS783SDPKJMV"
}
},
"customer": {
"id": "c_01GST0WAGQ7RSMMEXZGA360MFY",
"email": {
"isVerified": false,
"email": "colby45@example.com",
"verifiedAt": null
},
"externalId": "external_01GST0WAFACKR7NA5BH60W9RWA",
"fullName": "Colby Kunde",
"shortName": "Colby",
"assignedAt": null,
"assignedToUser": null,
"status": "ACTIVE",
"statusChangedAt": "2023-02-21T12:58:51.112Z",
"createdAt": "2023-02-21T12:58:50.775Z",
"createdBy": {
"actorType": "user",
"userId": "u_01GST0W90RKD7AXS783SDPKJMV"
},
"updatedAt": "2023-02-21T12:58:51.112Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01GST0W90RKD7AXS783SDPKJMV"
}
}
},
"id": "pEv_01GST0WAZ0W31PN3BH76M9E2SP",
"webhookMetadata": {
"webhookTargetId": "whTarget_01GST0W9X0WVHBSAPA9HQR7Q2E",
"webhookDeliveryAttemptId": "whAttempt_01GST0WBEHV132SN0W17CHKF6S",
"webhookDeliveryAttemptNumber": 1,
"webhookDeliveryAttemptTimestamp": "2023-02-21T12:58:51.729Z"
},
"type": "customer.customer_status_transitioned"
}
customer.customer_changed
🗒 Schema
Triggered under the following circumstances:
- when a customer has been created
- when a customer's details have been updated
- when a customer's status has changed
- when a customer's assignee has changed
Example webhook request body
{
"timestamp": "2023-02-21T13:27:33.811Z",
"workspaceId": "w_01GST2GVS0BD0X09D1A4DHDDR1",
"payload": {
"changeType": "UPDATED",
"eventType": "customer.customer_changed",
"customer": {
"id": "c_01GST2GWV8YX7141MNS8QKMN5Y",
"email": {
"email": "jasmin.brekke1fvr9@example.com",
"isVerified": false,
"verifiedAt": null
},
"externalId": "external_01GST2GWSYTS81Q11G5W83PJP3",
"fullName": "Jasmin Brekke",
"shortName": "Jasmin",
"assignedAt": null,
"assignedToUser": null,
"status": "ACTIVE",
"statusChangedAt": "2023-02-21T13:27:33.700Z",
"createdAt": "2023-02-21T13:27:33.480Z",
"createdBy": {
"actorType": "user",
"userId": "u_01GST2GVJYJYX4KPYQD6YQ87M1"
},
"updatedAt": "2023-02-21T13:27:33.700Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01GST2GVJYJYX4KPYQD6YQ87M1"
}
},
"previousCustomer": {
"id": "c_01GST2GWV8YX7141MNS8QKMN5Y",
"email": {
"email": "jasmin.brekke1fvr9@example.com",
"isVerified": false,
"verifiedAt": null
},
"externalId": "external_01GST2GWSYTS81Q11G5W83PJP3",
"fullName": "Jasmin Brekke",
"shortName": "Jasmin",
"assignedAt": null,
"assignedToUser": null,
"status": "ACTIVE",
"statusChangedAt": "2023-02-21T13:27:33.480Z",
"createdAt": "2023-02-21T13:27:33.480Z",
"createdBy": {
"actorType": "user",
"userId": "u_01GST2GVJYJYX4KPYQD6YQ87M1"
},
"updatedAt": "2023-02-21T13:27:33.480Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01GST2GVJYJYX4KPYQD6YQ87M1"
}
}
},
"id": "pEv_01GST2GX5KA170WQKG8V0GWMNN",
"webhookMetadata": {
"webhookTargetId": "whTarget_01GST2GW9PXD7FMW924XF13M99",
"webhookDeliveryAttemptId": "whAttempt_01GST2GXPW3RP4TVE8A73Q56AH",
"webhookDeliveryAttemptNumber": 1,
"webhookDeliveryAttemptTimestamp": "2023-02-21T13:27:34.364Z"
},
"type": "customer.customer_changed"
}
timeline.timeline_entry_changed
🗒 Schema
Triggered whenever there is a change on a customer's timeline. In particular:
- new emails (both inbound and outbound) and when they are updated (e.g. when they are marked as sent)
- new notes
- new chat messages (both inbound and outbound)
- new issues
- new custom timeline entries
- when a custom timeline entry is updated
- when the state of an issue changes
- when a customer's status changes
- when a customer's assignee changes
In order to know what kind of change has occurred, you can inspect the entry
field of the timelineEntry
or previousTimelineEntry
object. This field contains a type
field which you can use to determine the type of the timeline entry.
The changeType
field allows you to know what kind of change has occurred. It can be one of the following:
ADDED
: a timeline entry was added to the timelineUPDATED
: an existing timeline entry has been updatedDELETED
: a timeline entry was deleted from the timeline
Below you can see a sample event corresponding to a new outbound email to a customer.
Example webhook request body
{
"timestamp": "2023-02-23T09:21:01.100Z",
"workspaceId": "w_01GSYS6QSK6G4N9V5QJWXSHYV1",
"payload": {
"eventType": "timeline.timeline_entry_changed",
"previousTimelineEntry": null,
"timelineEntry": {
"id": "t_01GSYS6SPDDHWVTG5YHGX4YJDN",
"customerId": "c_01GSYS6RJEVTSDZEE7QTVWRXDZ",
"timestamp": "2023-02-23T09:20:57.549Z",
"actor": {
"actorType": "user",
"userId": "u_01GSYS6Q234DAXCQ6SV1PG9F00"
},
"entry": {
"entryType": "email",
"emailId": "em_01GSYS6SNQ5EEN9KRF3T9XD646",
"to": {
"email": "roel_weimannba3t37@getresolve.dev",
"name": "Roel Weimann",
"emailActor": {
"actorType": "customer",
"customerId": "c_01GSYS6RJEVTSDZEE7QTVWRXDZ"
}
},
"from": {
"email": "elyse.mcdermott.7i7l7c0v@getresolve.dev",
"name": "Elyse McDermott",
"emailActor": {
"actorType": "user",
"userId": "u_01GSYS6Q234DAXCQ6SV1PG9F00"
}
},
"additionalRecipients": [
{
"email": "gerhard26@getresolve.dev",
"name": "Miguel",
"emailActor": {
"actorType": "customer",
"customerId": "c_01GSYS6SNPYMXXEBC30D2GNHNM"
}
}
],
"hiddenRecipients": [
{
"email": "belle3@getresolve.dev",
"name": "Raul",
"emailActor": {
"actorType": "customer",
"customerId": "c_01GSYS6SNPZ3HSC31CVM0JW8PJ"
}
}
],
"subject": "Est inventore culpa corporis.",
"textContent": "Iure eos esse numquam quod exercitationem quod a. Nobis pariatur alias molestiae ad vitae. Nesciunt sed consequuntur ex explicabo hic soluta numquam odio dolore. Dolorem aliquam ipsum quia illo.",
"hasMoreTextContent": false,
"markdownContent": "Iure eos esse numquam quod exercitationem quod a. Nobis pariatur alias molestiae ad vitae. Nesciunt sed consequuntur ex explicabo hic soluta numquam odio dolore. Dolorem aliquam ipsum quia illo. On Thu, 13 Jan 2022 at 17:40, Postmark Tester wrote: > Hello! > > On Thu, 13 Jan 2022 at 17:37, Andrew Blaney wrote: > >> This is an email from gmail without an attachment >> >",
"hasMoreMarkdownContent": false,
"authenticity": "PASS",
"sentAt": null,
"receivedAt": null,
"attachments": [
{
"id": "att_01GSYS6SNNQN624PVKS76H2FEF",
"fileName": "northeast_digitized.a",
"fileSizeBytes": 95559,
"fileMimeType": "application/vnd.iptc.g2.packageitem+xml",
"fileExtension": "a",
"createdAt": "2023-02-23T09:20:57.525Z",
"createdBy": {
"actorType": "user",
"userId": "u_01GSYS6Q234DAXCQ6SV1PG9F00"
},
"updatedAt": "2023-02-23T09:20:57.525Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01GSYS6Q234DAXCQ6SV1PG9F00"
},
"type": "EMAIL",
"emailContentId": "\"JtP!&3-iGw\".E7c\"YEJ"
}
],
"inReplyToEmailId": null,
"isStartOfThread": true
}
},
"changeType": "ADDED"
},
"id": "pEv_01GSYS6X5CFWQQGTAHF2MSTF2S",
"webhookMetadata": {
"webhookTargetId": "whTarget_01GST2GW9PXD7FMW924XF13M99",
"webhookDeliveryAttemptId": "whAttempt_01GST2GXPW3RP4TVE8A73Q56AB",
"webhookDeliveryAttemptNumber": 1,
"webhookDeliveryAttemptTimestamp": "2023-02-23T09:21:01.100Z"
},
"type": "timeline.timeline_entry_changed"
}
customer.customer_group_memberships_changed
🗒 Schema
Triggered whenever there is a change in a customer's groups memberships.
The changeType
field allows you to know what kind of change has occurred. It can be one of the following:
ADDED
: a cutomer group membership was addedREMOVED
: a customer group membership was removed
Below you can see a sample event for when a customer group membership was created for a customer (ADDED
event).
Example webhook request body
{
"timestamp": "2023-02-21T12:58:51.232Z",
"workspaceId": "w_01GST0W989ZNAW53X6XYHAY87P",
"payload": {
"eventType": "customer.customer_groups_memberships_canged",
"changeType": "ADDED",
"previousCustomerGroupMemberships": [],
"customerGroupMemberships": [
{
"customerId": "c_01GST0WAGQ7RSMMEXZGA360MFY",
"customerGroupId": "cg_01GST0W9Z0WVHBSAPA9HQR7Q2E",
"workspaceId": "w_01GST0W989ZNAW53X6XYHAY87P",
"customerGroup": {
"id": "cg_01GST0W9Z0WVHBSAPA9HQR7Q2E",
"name": "Enterprise",
"key": "enterprise",
"color": "#000000",
"createdAt": "2023-02-21T12:58:50.775Z",
"createdBy": {
"actorType": "user",
"userId": "u_01GST0W90RKD7AXS783SDPKJMV"
},
"updatedAt": "2023-02-21T12:58:50.775Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01GST0W90RKD7AXS783SDPKJMV"
}
},
"createdAt": "2023-02-21T12:58:50.775Z",
"createdBy": {
"actorType": "user",
"userId": "u_01GST0W90RKD7AXS783SDPKJMV"
},
"updatedAt": "2023-02-21T12:58:50.775Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01GST0W90RKD7AXS783SDPKJMV"
}
}
],
"customer": {
"id": "c_01GST0WAGQ7RSMMEXZGA360MFY",
"email": {
"isVerified": false,
"email": "colby45@example.com",
"verifiedAt": null
},
"externalId": "external_01GST0WAFACKR7NA5BH60W9RWA",
"fullName": "Colby Kunde",
"shortName": "Colby",
"assignedAt": null,
"assignedToUser": null,
"status": "ACTIVE",
"statusChangedAt": "2023-02-21T12:58:51.112Z",
"createdAt": "2023-02-21T12:58:50.775Z",
"createdBy": {
"actorType": "user",
"userId": "u_01GST0W90RKD7AXS783SDPKJMV"
},
"updatedAt": "2023-02-21T12:58:51.112Z",
"updatedBy": {
"actorType": "user",
"userId": "u_01GST0W90RKD7AXS783SDPKJMV"
}
}
},
"id": "pEv_01GST0WAZ0W31PN3BH76M9E2SP",
"webhookMetadata": {
"webhookTargetId": "whTarget_01GST0W9X0WVHBSAPA9HQR7Q2E",
"webhookDeliveryAttemptId": "whAttempt_01GST0WBEHV132SN0W17CHKF6S",
"webhookDeliveryAttemptNumber": 1,
"webhookDeliveryAttemptTimestamp": "2023-02-21T12:58:51.729Z"
},
"type": "customer.customer_groups_memberships_canged"
}
If you have any problems, please get in touch with us by email on help@plain.com, and we will be happy to help.