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 HTTPS endpoint whenever subscribed data changes. You choose the events and provide the URL. Failed deliveries can be re-sent from the UI or through MCP. Deliveries are signed with HMAC-SHA256 over the raw request body. Every event and its payload shape is listed in the catalog at the end of this page.
Uses
Webhooks let other systems react to CRM changes without polling.
- Push new contacts into an email sequencer.
- Open a ticket in a support tool when a deal changes state.
- Trigger a finance workflow when a deal is marked as closed.
- Sync records into a data warehouse in near real time.
Create a webhook
UI: Company, then Webhooks, then New.
MCP: manage_webhooks with action: "create", plus url, events[], and optional description, secret, enabled.
{
"url": "https://hooks.example.com/customermates",
"events": ["contact.created", "contact.updated", "deal.updated"],
"secret": "use-a-random-string-from-a-password-manager",
"enabled": true
}Use an HTTPS endpoint. An http:// URL is accepted for local testing, but a webhook that carries custom headers must be https://, because headers usually carry a credential.
What you receive
Every delivery is a POST with Content-Type: application/json and the same envelope:
{
"event": "<event-name>",
"data": {
"userId": "<who-triggered-it>",
"companyId": "<workspace>",
"entityId": "<affected-record>",
"payload": { /* event-specific */ }
},
"timestamp": "<iso-8601>"
}userId is the user who caused the write, including a user acting through an API key. For messaging events, which are triggered by inbound provider activity rather than a user action, userId is null.
For *.updated events, payload wraps the full record together with a changes object. For *.created and *.deleted events, payload is the record itself.
Example, contact.updated:
{
"event": "contact.updated",
"data": {
"userId": "u_123",
"companyId": "c_abc",
"entityId": "ct_xyz",
"payload": {
"contact": {
"id": "ct_xyz",
"firstName": "Max",
"lastName": "Mustermann",
"notes": { /* Tiptap JSON */ },
"organizations": [{ "id": "org_1", "name": "Example GmbH" }],
"users": [],
"deals": [{ "id": "deal_1" }, { "id": "deal_2" }],
"customFieldValues": [
{ "columnId": "col_abc", "value": "Won" }
]
},
"changes": {
"organizations": {
"previous": [],
"current": [{ "id": "org_1", "name": "Example GmbH" }]
}
}
}
},
"timestamp": "2026-04-22T10:00:00.000Z"
}The record fields depend on the entity type. Custom columns appear under customFieldValues as columnId and value pairs. Use get_record_schema to see the columns configured for a given entity in your workspace.
The changes object
changes is present only on *.updated events. Each key is a record field whose value moved. previous is the value before the write, current is the value after.
Arrays of related records compare by id. Objects compare by deep equality. Scalar fields compare by value. createdAt and updatedAt are never reported as changes.
If a write does not change any field, the event is suppressed. No-op *.updated events are not delivered.
Custom headers and body templates
Some receivers will not accept the standard delivery. Two optional settings cover that, and a webhook that sets neither delivers exactly as described above.
Custom headers are sent with every delivery, for endpoints that require their own authentication. Set them in the UI one per line as Name: value, or pass a headers object through MCP and the API. Content-Type, Host and X-Webhook-Signature are set by Customermates and cannot be overridden; header names must be valid HTTP tokens, and values must be Latin-1 text without line breaks. Because headers usually carry a credential, a webhook that has them must use an HTTPS endpoint.
Header values are never returned by the REST API or by MCP, which report headerNames instead. The webhook form in Customermates does still show them, so anyone who can manage webhooks in the workspace can read them, the same as the signing secret. An API or MCP update that omits headers keeps the stored values, and one that passes null clears them.
A body template replaces the standard envelope for endpoints that expect a fixed shape. Write a JSON document and reference envelope values with placeholders:
{ "text": "{{event}} for {{data.entityId}}" }Available placeholders are {{event}}, {{timestamp}} and any path under {{data}}, including {{data.entityId}}, {{data.companyId}}, {{data.userId}} and {{data.payload}}. A placeholder must sit inside a JSON string; {"count": {{data.payload.count}}} is rejected. A placeholder that resolves to an object is inserted as a JSON string, and a missing path becomes an empty string. Substituted values are escaped, so record content cannot alter the structure of the document.
The template must produce a JSON object. It is checked when you save the webhook, and a delivery whose template fails to render is recorded as failed rather than sent. The signature is always computed over the body actually sent, so verify it against the rendered body rather than the envelope.
One URL, one configuration. Headers and body templates are matched to a delivery by workspace and URL, not by webhook. If two webhooks in the same workspace point at the same URL and either one carries headers or a body template, every delivery to that URL fails and is not retried, including deliveries from the webhook that has no overrides. Give each subscription its own URL, by adding a path or a query string, before adding headers or a template.
Deliveries are recorded with the event payload rather than the rendered body, so the delivery detail shows the event that fired and not the exact bytes sent.
Sending to a specific service
Each recipe is the URL, the custom headers, and the body template to paste into Company → Webhooks → New. Set a signing secret in every case; a receiver that cannot verify it simply 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.entityId}}" }The endpoint reads only text and ignores every other field, so the template is required; without it the routine starts with no context. Every delivery starts a separate run against your routine allowance, and a retry starts another, so subscribe to the narrowest set of events that does the job.
Slack
Post to a channel with an incoming webhook. The URL carries the authentication, so no custom headers are needed.
Use the incoming webhook URL from Slack → Apps → Incoming Webhooks.
{ "text": "{{event}} for {{data.entityId}}" }Discord
The same shape under a different key.
Use the webhook URL from Channel → Edit Channel → Integrations → Webhooks.
{ "content": "{{event}} for {{data.entityId}}" }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 the node 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 as described below.
Signature verification
If you set 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));
}Retries and failed deliveries
Every delivery is recorded. Review deliveries in the UI (Company, then Webhook Deliveries) or through manage_webhooks with action: "list_deliveries".
A delivery is marked failed if the HTTP status is outside the 2xx range or the request times out. The request timeout is 5 seconds.
Customermates automatically retries failed deliveries: up to 5 retries for timeouts, 5xx responses, and 408, 425, and 429. Permanent 4xx responses (for example 400, 401, 403, 404) are not retried, because a repeat would fail the same way. Retries happen inside the delivery run, so they do not create extra delivery records; each record shows the final outcome.
You can also resend any delivery yourself from the UI or with manage_webhooks and action: "resend_delivery" plus the delivery id. A manual resend creates a new delivery record; the original is unchanged.
Ordering and concurrency
Deliveries are not strictly ordered across events. Two contact.updated events milliseconds apart can arrive out of order. Use the timestamp in the payload to resolve order, or treat deliveries as idempotent messages keyed by entityId.
Notes are JSON, not markdown
The notes field in payloads is Tiptap JSON, the same structure the editor stores. To render it as markdown on your side, run it through a Tiptap-compatible serializer. In MCP, get_records with include: "withNotes" returns notes already serialized to markdown.
Delete payloads
Delete events carry the full record as it was before deletion, under payload, not just the id. The affected record id is also available as data.entityId.
Debugging
- Use webhook.site as a throwaway receiver during integration testing.
- The delivery log (
manage_webhooks,action: "list_deliveries") supportssearchTermon url and event name. - If your receiver returns an error status, Customermates records the response so you can inspect what happened.
Event catalog
Twenty-six events are available to subscribe to: fifteen record events across the five entity types, and eleven messaging events. All use the envelope above.
Record events
| Event | When it fires | 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. userId is null. Payloads reference provider records by id (connectedAccountId, provider, providerMessageId, threadId, and similar) rather than embedding CRM records.
| 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.
- n8n integration: drop webhooks into visual workflows.