Overview
Webhooks let demeterrr push data to your endpoint the moment something happens, instead of you polling the API. Six events are available:new_response- A survey response was submitteddetractor_response- A submitted response scored as a detractornew_review- A new review was synced from Googlebad_review- A new review synced at 3 stars or belownew_contact- A contact was createdstep.completed- A sequence step configured with its own webhook URL finished
POST request with retries, a 10 second timeout, and a durable delivery log you can inspect from the dashboard API.
Getting a Key and Scopes
Create an API key from Settings → API Keys as described in the Quickstart. Webhook management needs one of two scopes depending on the call:
Pass the key as either an
X-API-Key header or an Authorization: Bearer <key> header.
Subscribing
hookUrl adds it to the existing subscription rather than creating a second one. That call returns "secret": null:
hookUrl must be https. Plain http, and any URL that resolves to a private or internal address, is rejected with a 400 error. This check runs again at delivery time against the address actually dialled, so a URL that starts resolving privately later stops receiving deliveries rather than silently reaching an internal host.
The subscription belongs to the API key that created it. Revoking that key from Settings → API Keys deletes the subscriptions it created, along with their delivery history. Deactivating the key instead pauses them: no new deliveries go out while it is off. Reactivating resumes future deliveries only. A retry that came due while the key was off is skipped, and that delivery is finished for good rather than picked up later, so anything sent during the pause is lost. Subscribing again with another key recreates a deleted subscription and issues a new secret.
A subscription is keyed on the URL, so subscribing a URL a sequence step already uses attaches to that step’s existing row rather than creating a second one. See Sequence Step Webhooks for what that shares.
Listing Subscriptions
cURL
webhooks:read. Secrets are never included here:
Unsubscribing
cURL
webhooks:write. Send either hookUrl or the subscription id alongside triggerType:
deleted is true only when that was the last event on the subscription, which deletes the subscription row and its secret along with it.
Event Catalogue
Payloads list only the fields demeterrr actually sends. Treat unlisted fields as not guaranteed.new_response
Fired when a survey response is submitted.detractor_response
Same payload asnew_response, sent as a second, separate delivery whenever isDetractor is true.
new_review
Fired when a review is synced in from Google.bad_review
Same payload asnew_review, sent as a second, separate delivery whenever rating is 3 or below.
new_contact
Fired when a contact is created through the API or dashboard.step.completed
Fired for a sequence step, only if that step has its ownwebhookUrl configured. See Sequence Step Webhooks.
Delivery Format and Headers
The body shape depends on the event, and there are exactly two shapes. For the five subscribable events (new_response, detractor_response, new_review, bad_review, new_contact) the body is a bare JSON array holding one payload object:
step.completed the body is the bare payload object, not wrapped in an array:
Verifying Signatures
X-Demeterrr-Signature is t=<timestamp>,v1=<hex>, where hex is an HMAC-SHA256 digest of ${timestamp}.${rawBody}, keyed with your subscription’s secret. Recompute it over the raw request body bytes, compare with a constant-time comparison, and reject anything where t is more than 300 seconds from the current time.
Node.js
t and v1, reject if t is stale, compute HMAC-SHA256 of ${t}.${rawBody} with your secret, hex-encode it, and compare to v1 with a constant-time string comparison rather than ==.
Retries and Failure Handling
- Up to 5 attempts total per delivery, spaced out by exponential backoff.
- Each attempt has a 10 second timeout.
- Redirects are never followed. Respond 2xx directly from the URL you subscribed.
- Returning
410 Gonedeactivates the subscription immediately and stops further retries and attempts. A deactivated subscription stays off until you act: subscribe the URL again for a Zapier trigger, or save the sequence step again for a step webhook. - The 5th attempt is the last one. It is logged like any other failed attempt, and
GET /api/settings/webhooksmarks it"deadLetter": trueso you can tell a final failure from a retry that is still coming. - A
410is never marked"deadLetter": true, even on the last attempt. It is a terminal answer from your endpoint rather than an exhausted retry budget, so it is recorded as a failed attempt with"httpStatusCode": 410and the subscription switched off. - The subscription is re-read before every attempt. Deactivating it, or revoking the API key that created it, stops the retries that are still queued.
- Use
X-Demeterrr-Deliveryto deduplicate: a retried attempt reuses the same delivery id, so if you already processed that id, it is safe to acknowledge and skip it.
Sequence Step Webhooks
A sequence step can be configured with its own webhook URL, independent of the subscriptions above. When that step completes, demeterrr firesstep.completed to that URL only, signed and delivered the same way as any other event.
Behind the scenes this creates (or reuses) a subscription for that URL, so its secret and delivery history appear in GET /api/settings/webhooks alongside your other subscriptions.
Sharing a row has three consequences worth knowing:
- Whichever consumer created the row owns it. A row created by a subscribe call belongs to that API key and is deleted when the key is revoked, even if a sequence step is also using it. The next step completion recreates the row and issues a new secret, so your step handler must be re-pointed at that secret. A row first created by a step belongs to no key and no revocation removes it.
- Deactivating an API key (setting
isActiveto false) pauses the subscriptions it created. No new deliveries go out while it is off, and reactivating resumes future ones. It does not replay the pause: a retry that came due while the key was off is skipped and that delivery ends there. Revoking deletes; deactivating pauses. - A
410 Gonefrom your endpoint deactivates the whole row. Both the sequence step and any Zapier triggers on that URL stop. Re-subscribing the URL, or saving the step again, switches it back on for both.
webhookUrl, clearing it, or deleting the step or its sequence releases step.completed from the old URL, unless another step in your organization still points at it. Other events on that subscription are untouched, and the row is removed only when step.completed was its last event.
The step’s URL is re-validated (https, not a private or internal address) each time the step fires, so a URL that later resolves internally stops receiving deliveries.
If your endpoint answers 410 Gone, later completions of that step are skipped. Saving the step again, with the same URL or a new one, switches it back on. Firing the step is deliberately not enough, or a retired endpoint would be revived by the next contact who reaches that step.
Viewing Deliveries and Secrets
GET /api/settings/webhooks is a dashboard-session endpoint, not an API-key one. It requires an owner or admin session and returns every subscription for the organization, including its secret and its 20 most recent delivery attempts:
Testing Tips
- Point a subscription at a public request-catcher (a tunnel like ngrok, or a bin service) while you build your handler, so you can inspect raw headers and body before wiring up real verification logic.
- Key your idempotency store off
X-Demeterrr-Delivery, not the event payload. Retries of the same delivery reuse that id, while two genuinely different events never share one. - Confirm your endpoint returns a 2xx before you start doing slow work in the handler. A response after the 10 second timeout counts as a failed attempt even if your endpoint eventually would have returned 200.
Next Steps
API Reference
Explore available endpoints
Quick Start
Make your first API call