Skip to main content

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

caution

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.

info

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'

Creating a webhook target - Step 1

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.

Creating a webhook target - Step 2

info

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 #DelayApproximate time since first attempt
110s10s
230s40s
35m6m
430m36m
51h1.5h
63h4.5h
76h10.5h
812h22.5h
91d2d
101d3d
111d4d
121d5d

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 originated
  • Plain-Webhook-Target-Id: The ID 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.

If you prefer, you can browse the schema using this interactive tool.

FieldTypeDescription
idstringThe ID of the Plain event. It remains the same across all of the delivery attempts
typestringThe Plain event's type
webhookMetadataobjectMetadata associated with the webhook request. See Webhook Metadata for more details
timestampstringThe Plain event's timestamp. In UTC and formatted as ISO8601. E.g. 1989-10-28T17:30:00.000Z
workspaceIdstringThe ID of the workspace where the Plain event originated
payloadobjectThe 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.

FieldTypeDescription
webhookTargetIdstringThe 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
webhookDeliveryAttemptIdstringThe ID of the delivery attempt. It will be different on every delivery attempt
webhookDeliveryAttemptNumberstringThe current delivery attempt number (starts at 1)
webhookDeliveryAttemptTimestampstringThe 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 timeline
  • UPDATED: an existing timeline entry has been updated
  • DELETED: 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 added
  • REMOVED: 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.