Receive CRM events through webhooks
Subscribe to CRM events, verify signatures, inspect failed deliveries, and read the full event catalog in one place.
Customermates sends a JSON POST to your endpoint whenever subscribed data changes. You choose the events and the URL. With a secret, every delivery is signed with HMAC-SHA256 over the raw request body. Failed deliveries are retried automatically and can be resent by hand. The event catalog is at the end of this page.
What are webhooks for?
Webhooks let other systems react to CRM changes without polling, for example to push new contacts into an email sequencer, open a support ticket when a deal changes state, start a finance workflow when a deal closes, or sync records into a data warehouse in near real time.
How do I create a webhook?
Open Webhooks, click Add, fill in the Webhook dialog, enter the URL and click Save.
| Field | What it does |
|---|---|
| URL | Required. An HTTP or HTTPS endpoint with a valid host and no username or password receives a POST for every subscribed event. A URL without a scheme is saved with https:// in front. International hostnames and paths are saved in their ASCII URL form. Other URL schemes, such as mailto: and tel:, are refused. |
| Description | Optional, up to 500 characters. Shown in the list. |
| Events | Required. One or more events from the catalog. |
| Secret | Optional, 1 to 256 characters. Signs every delivery (see How do I verify the signature?). |
| Custom headers | Optional. One Name: value per line (see Custom headers and body templates). |
| Body template | Optional JSON document that replaces the standard envelope. |
| Enabled | On by default. A disabled webhook receives nothing and keeps its configuration, so you can pause it without deleting it. |
To change a webhook, click its row, edit and click Save. To remove it, click Delete at the top of the dialog and confirm; its past deliveries stay on the Deliveries page. To see the deliveries of one webhook, choose Show deliveries in the row's More actions menu: Deliveries opens filtered to that webhook's URL, and you can remove the filter like any other. The list shows URL, Description, Events, Status (Enabled or Disabled), Created at and Updated at. In Filter, searching matches the URL and filters cover the creation and update dates.
Use an HTTPS endpoint. Plain http:// is accepted for any host only while the webhook has no custom headers. With custom headers the URL must use https://, or http:// on localhost, 127.0.0.1 or ::1, because headers usually carry a credential. This is checked when you save and again before every delivery.
Through MCP, use manage_webhooks with action: "create", plus url, events[] and optional description, secret, headers, bodyTemplate, enabled. Over REST, POST /api/v1/webhooks creates one with the same fields and GET /api/v1/webhooks lists them; GET, PATCH and DELETE /api/v1/webhooks/{id} read, update and delete one, and an update changes only the fields you send. An id that does not exist or belongs to another workspace answers 404 with the structured not_found error.
Which records trigger a webhook, and whose access does a delivery use?
When you pick a record event, the dialog shows the record source: choose the list under Records from (or All accessible types), then optionally Changed fields, Search records and Record filters on fields and relationships. Deliveries use the record access of the Record access owner, by default the member who creates the webhook; only an administrator can choose another owner. Older migrated webhooks can keep several sources, shown as chips.
Through MCP, record subscriptions in manage_webhooks also need expectedSchemaRevision from get_record_model. recordTrigger holds a query with stable type and field IDs and optional changedFieldIds; null selects all accessible types. recordOwnerUserId optionally sets the delivery owner. On update, omit these settings to keep them; changing the trigger or owner requires the current schema revision. A webhook that follows several lists uses recordSources instead of recordTrigger: up to 50 sources, each with its own query, changedFieldIds and events; the two cannot be combined, and null clears the sources. The REST API takes the same settings.
{
"url": "https://hooks.example.com/customermates",
"events": ["record.created", "record.updated"],
"expectedSchemaRevision": 3,
"recordTrigger": {
"query": { "typeId": "<project-type-id>" },
"changedFieldIds": []
},
"secret": "use-a-random-string-from-a-password-manager",
"enabled": true
}Who can see and change webhooks?
Access comes from the API & Webhooks row of a role in Roles.
| Role setting on API & Webhooks | What members with that role can do |
|---|---|
| Read access None (the default for a new role) | Webhooks are hidden; opening the page directly leads to the Dashboard. |
| Read access All, no checkbox ticked | Open Webhooks and its deliveries, open every webhook read-only and open Event Details for every delivery. A saved Secret and each Custom headers value appear as ********; header names stay visible. |
| Read access All plus Create | Add webhooks and resend deliveries. |
| Read access All plus Edit | Edit webhooks and see saved Secret and Custom headers values. |
| Read access All plus Delete | Delete webhooks. |
The same row also controls API keys, so a role that may create or delete its own API keys may also create or delete every webhook in the workspace. The Admin role can do everything. Webhooks have no plan restriction: they work on every Cloud plan and when self-hosted. The same permissions apply to manage_webhooks through MCP and in Mate, where deleting a webhook or resending a delivery asks for your approval first.
What does the payload look like?
Every delivery is a POST with Content-Type: application/json and the same envelope:
{
"event": "<event-name>",
"id": "<event-id>",
"timestamp": "<iso-8601>",
"companyId": "<workspace>",
"actorId": "<who-triggered-it>",
"data": {
/* event-specific */
}
}id identifies the event: a resend and every retry carry the same id, so use it to deduplicate. actorId is the user who caused the write, including a user acting through an API key or a routine run by its owner. Messaging events come from inbound provider activity, so their actorId is null.
For record events, data.record holds the record reference (typeId and recordId, unambiguous even after a rename) and the changes, data.cause what caused the write, and data.causeId is shared by every event of the same write.
Example, record.updated for a contact, with unrelated fields omitted:
{
"event": "record.updated",
"id": "<event-id>",
"timestamp": "2026-04-22T10:00:00.000Z",
"companyId": "<workspace-id>",
"actorId": "<actor-user-id>",
"data": {
"causeId": "<cause-id>",
"cause": { "kind": "mutation" },
"record": {
"ref": { "typeId": "<contact-type-id>", "recordId": "<record-id>" },
"schemaRevision": 3,
"beforeVersion": 1,
"afterVersion": 2,
"fields": [
{
"fieldId": "<first-name-field-id>",
"before": { "value": { "state": "value", "value": { "kind": "text", "value": "Max" } } },
"after": { "value": { "state": "value", "value": { "kind": "text", "value": "Maximilian" } } }
}
]
}
}
}A real record change also carries assignments, identities, link changes and related records. Field entries include display metadata and may show missing, restricted or error instead of a value. Find the list with POST /api/v1/model/discover and resolve its stable field IDs with POST /api/v1/model/read, or use the get_record_model MCP tool.
Changed fields in data.record.fields
data.record.fields lists changed fields with before and after values; data.record.links, data.record.assignments and data.record.identities describe other changed connections. data.record.beforeVersion and data.record.afterVersion identify the record versions. A missing or restricted field is not the same as zero.
Updates that change nothing produce no record.updated event. Data model changes may recalculate stored values on many records; those configuration backfills remain in record history but do not produce per-record webhook deliveries or trigger event routines. A later user or API edit to a record produces its usual event. The payload is redacted by the webhook owner's current record and calculated-field access at delivery time.
Custom headers and body templates
Two optional settings cover receivers that do not accept the standard delivery. A webhook that sets neither sends the standard envelope.
Custom headers are sent with every delivery, for endpoints with their own authentication. Enter them one per line as Name: value (a line without a colon is ignored, an empty field removes all headers), or pass a headers object through MCP and the API. Seven names are reserved and rejected: Connection, Content-Length, Content-Type, Host, Transfer-Encoding, X-Webhook-Signature and X-Customermates-Delivery-Id. Names must be valid HTTP tokens, values Latin-1 text without line breaks. A webhook may carry up to 20 headers, names up to 128 characters, values up to 2,048 characters, 8,192 characters in total. A webhook with headers must use HTTPS; plain HTTP is accepted only for localhost, 127.0.0.1 and ::1.
The REST API and MCP never return header values or the secret; they report headerNames and hasSecret. The dialog shows the values only to the Admin role and roles with Edit on API & Webhooks, with the secret masked until you click the eye icon. An API or MCP update that omits headers keeps the stored values; null clears them.
A body template replaces the standard envelope for endpoints that expect a fixed shape. Write a JSON document with placeholders:
{ "text": "{{event}} for {{data.record.ref.recordId}}" }Placeholders are {{event}}, {{id}}, {{timestamp}}, {{companyId}}, {{actorId}} and any path under {{data}}, such as {{data.record.ref.recordId}} for a record event or {{data.entityId}} for a messaging event. A placeholder must sit inside a JSON string; {"count": {{data.count}}} is rejected. An object value is inserted as a JSON string, a missing path as an empty string. Substituted values are escaped, so record content cannot change the document structure.
The template may be up to 8,192 characters, must produce a JSON object and is checked when you save. A delivery whose template fails to render, or whose rendered body exceeds 131,072 characters, is recorded as failed without being sent and is not retried. The signature is computed over the body actually sent, so verify it against the rendered body.
Removing a secret or a template. The dialog rejects saving Secret or Body template empty once set. Update the webhook through manage_webhooks or PATCH /api/v1/webhooks/{id} with secret: null or bodyTemplate: null, or delete and recreate it.
Deliveries are recorded with the event payload, not the rendered body, so Event Details shows the event that fired rather than the exact bytes sent.
How do I send to Slack, Discord or n8n?
Each recipe gives the URL, custom headers and body template to enter after Add in Webhooks. Set a secret in every case; a receiver that cannot verify it ignores the header.
Claude Code routines
Start a Claude Code routine when a record changes. Create the routine at claude.ai/code/routines first.
Add an API trigger
Open the routine, click Add another trigger, choose API, then Generate token. Copy the URL and the token; the token is shown once and cannot be retrieved later.
Create the webhook
Paste the URL, select your events, and add the headers and template below.Reference the payload in the routine prompt
The text arrives wrapped in a
routine-fire-payloadblock that the routine treats as untrusted data. The routine's own prompt has to refer to it, for example "act on the event described in the routine-fire-payload block", or the text is ignored.
The URL is https://api.anthropic.com/v1/claude_code/routines/<routine-id>/fire, and the routine id is prefixed trig_.
Authorization: Bearer <routine-token>
anthropic-beta: experimental-cc-routine-2026-04-01
anthropic-version: 2023-06-01{ "text": "{{event}} for {{data.record.ref.recordId}}" }The endpoint reads only text, so the template is required; without it the routine starts with no context. Every delivery, and every retry, starts a separate run against your routine allowance, so subscribe to as few events as the job needs.
Slack
Post to a channel with an incoming webhook from Slack → Apps → Incoming Webhooks. The URL carries the authentication, so no custom headers are needed.
{ "text": "{{event}} for {{data.record.ref.recordId}}" }Discord
Use the webhook URL from Channel → Edit Channel → Integrations → Webhooks. Same shape, different key:
{ "content": "{{event}} for {{data.record.ref.recordId}}" }n8n
An n8n Webhook node accepts the standard envelope, so leave both settings empty unless the node uses header authentication. If it does, add the header it expects and leave the template empty:
X-N8N-Header-Auth: <value>See n8n for the full pattern.
Anything else
A receiver that needs neither its own authentication nor a fixed body needs neither setting. Leave both empty and verify the signature.
How do I verify the signature?
If the webhook has a Secret, every request includes:
X-Webhook-Signature: <hex><hex> is HMAC-SHA256(secret, rawRequestBody), lowercase hex, no prefix. Recompute it on your side and compare in constant time.
Node.js example:
import crypto from "crypto";
function verify(rawBody: string, received: string, secret: string) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}How do retries and failed deliveries work?
A delivery fails when the HTTP status is outside 2xx, the request times out (after 5 seconds) or no response arrives. Customermates retries timeouts, network errors, 5xx responses and 408, 425 and 429 automatically, up to 10 attempts in total. The first retry follows 10 seconds after the failed attempt and each pause doubles (20 seconds, 40 seconds and so on, capped at one hour), so the last attempt runs about 1.5 hours after the first; after that the delivery stays Failed, and you can resend it by hand. Other 4xx responses, such as 400, 401, 403, 404 and 422, are not retried, because a repeat would fail the same way. A delivery also fails before any request is sent, with status code 422, when headers would go to an address that does not allow them or the body template cannot be rendered.
Retries run inside the same delivery, so they add no records: each record shows the latest outcome and can show Failed while retries are pending, then change to Delivered when one succeeds.
Where do I see past deliveries?
Every delivery is recorded on the Deliveries page, newest first, with URL, Event, Element (the affected record), Status, Status Code and Created at. In Filter, searching matches the event and the URL, and filters cover the event, the URL and the creation date. Through MCP, use manage_webhooks with action: "list_deliveries"; over REST, GET /api/v1/webhooks/deliveries lists the deliveries of every webhook and GET /api/v1/webhooks/{id}/deliveries those of one webhook, newest first. Delivery records are kept as long as the workspace exists.
Click a row to open Event Details: the status (Pending, Sending, Delivered or Failed), URL, Event, Element, Created At, Status Code, Response Message and the Event payload. A messaging event's payload is shown as it was sent. A record event's payload is read again with your current access: values you cannot read appear as restricted, and the payload stays empty when you cannot read the record.
How do I resend a delivery?
To resend, click Resend at the top of Event Details. It appears once the delivery is Delivered or Failed, for roles with Create on API & Webhooks; through MCP, use manage_webhooks with action: "resend_delivery" and the delivery id, and over REST POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/resend. A resend creates a new delivery record with the webhook's current URL, secret, headers and template; the original stays unchanged. A messaging event is resent from its stored event. A record event is read again with the webhook owner's current access, only that owner or an administrator can resend it, and the resend is refused when the webhook's record sources changed since the original delivery. The webhook must still exist, be enabled and subscribe to the event; deliveries of retired type-specific events cannot be resent. The Webhooks and Deliveries pages show such an event with (legacy), for example Contact created (legacy).
Are deliveries ordered, and do they arrive in the right order?
No. Two record.updated events milliseconds apart can arrive out of order. Use the timestamp and record version to resolve order, or treat deliveries as idempotent messages keyed by typeId, recordId and event ID.
Why is formatted text Tiptap JSON in payloads?
The value of a Formatted text field in payloads is a richText value whose documentJson is Tiptap JSON, the structure the editor stores. Run it through a Tiptap-compatible serializer to get markdown. The REST record routes return the same documentJson. MCP record tools return Formatted text as Markdown instead, marked as untrusted content; see MCP.
What do delete payloads contain?
record.deleted fires when a record moves to Trash. data.record carries the qualified ref, the authorized before-values and the relationships it leaves; afterVersion is null. Source filters match immediately before the move, and the owner's current access is checked again before delivery. record.restored fires when it comes back from Trash and carries the after-values like record.created.
record.deletedPermanently fires when a record is deleted permanently, by hand or 30 days after it went to Trash. Its data.record carries only the ref: the values are erased, and the record's earlier events lose their field values too, keeping who changed it and when. It reaches every webhook of the record's list that selected it and whose owner can read that list, without source filters.
How do I debug a delivery?
- Find the delivery under Deliveries and open it to see Event Details.
- Use webhook.site as a throwaway receiver while testing.
manage_webhookswithaction: "list_deliveries",GET /api/v1/webhooks/deliveriesandGET /api/v1/webhooks/{id}/deliveriessupportsearchTermon URL and event name.- Customermates stores only the HTTP status code and status text your receiver returned (for example
404andNot Found), shown as Status Code and Response Message in Event Details. The response body is not stored, so log it on the receiving side. Without a response, Status Code shows a dash (the list column stays empty) and Response Message names the cause, such as the timeout.
Which events exist?
A webhook can subscribe to 16 events: 5 record events and 11 messaging events. For a CRM webhook, select Record created, Record updated, Record moved to Trash, Record restored or Record deleted permanently (record.created, record.updated, record.deleted, record.restored, record.deletedPermanently) and set its record source; this covers lists you created yourself too. All use the standard envelope. Routines can be triggered by the same events except Record deleted permanently (record.deletedPermanently), Email Deleted (messaging.email.deleted) and Chat Deleted (messaging.chat.deleted); Message Deleted (messaging.message.deleted) does trigger routines. See Routines.
Record events
| Event | When it fires | data |
|---|---|---|
record.created | A record of a configured source type is created | the record change with its after-values |
record.updated | A field, assignment, identity, or link changes on a configured source type | the record change with before/after values |
record.deleted | A record of a configured source type moves to Trash | the record change with its before-values |
record.restored | A record of a configured source type is restored from Trash | the record change with its after-values |
record.deletedPermanently | A record of the list is deleted permanently | the ref only; values are erased |
Fifteen type-specific identifiers are retired. They stay only so older webhooks and delivery records remain readable: they cannot be selected for a webhook or routine, and nothing emits them. Replace them with a generic event and a record source.
| Legacy event | Historical meaning | Legacy payload |
|---|---|---|
contact.created | A contact is created via UI, API, MCP, or import | full contact |
contact.updated | Any contact field changes | contact, changes |
contact.deleted | A contact is deleted | full contact |
organization.created | An organization is created | full organization |
organization.updated | Any organization field changes | organization, changes |
organization.deleted | An organization is deleted | full organization |
deal.created | A deal is created | full deal |
deal.updated | Any deal field changes | deal, changes |
deal.deleted | A deal is deleted | full deal |
service.created | A service is created | full service |
service.updated | Any service field changes | service, changes |
service.deleted | A service is deleted | full service |
task.created | A task is created | full task |
task.updated | Any task field changes | task, changes |
task.deleted | A task is deleted | full task |
Messaging events
Messaging events fire on inbound provider activity on connected accounts. Events offers them in every workspace, but they fire only where Channels are connected, which requires Cloud and a plan that includes messaging. actorId is null. data references provider records by id (entityId for the affected message, thread, calendar, calendar event or relation, plus connectedAccountId, provider, providerMessageId, threadId and similar) rather than embedding CRM records. A messaging event is kept only while a webhook or an event routine subscribes to it.
| Event | When it fires |
|---|---|
messaging.message.received | A chat message or email is ingested |
messaging.message.updated | An ingested message is updated |
messaging.message.deleted | An ingested message is deleted |
messaging.message.reaction | A reaction is added to a message |
messaging.email.received | An email is ingested |
messaging.email.deleted | An ingested email is deleted |
messaging.chat.updated | A chat thread is updated |
messaging.chat.deleted | A chat thread is deleted |
messaging.calendar.changed | A connected calendar changes |
messaging.calendar_event.changed | A calendar event changes |
messaging.relation.created | A new provider relation is created |
Next
- MCP tool catalog: programmatic webhook management.
- API reference: the REST webhook endpoints and every event payload.
- n8n integration: drop webhooks into visual workflows.