Skip to main content

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 submitted
  • detractor_response - A submitted response scored as a detractor
  • new_review - A new review was synced from Google
  • bad_review - A new review synced at 3 stars or below
  • new_contact - A contact was created
  • step.completed - A sequence step configured with its own webhook URL finished
Every delivery is a signed 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

The call that creates the subscription returns its signing secret. The secret is 64 hexadecimal characters with no prefix:
Subscribing another trigger for the same hookUrl adds it to the existing subscription rather than creating a second one. That call returns "secret": null:
Store the secret when the subscription is created. It is returned exactly once, because handing it back to every caller holding a webhooks:write key would make this endpoint a way to read secrets. If you lose it, read it from GET /api/settings/webhooks, which needs an owner or admin dashboard session.
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
Requires webhooks:read. Secrets are never included here:

Unsubscribing

cURL
Requires 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 as new_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 as new_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 own webhookUrl 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:
For step.completed the body is the bare payload object, not wrapped in an array:
The two differ because they always have: sequence-step webhooks have sent an object since before subscriptions existed, and the Zapier trigger events have always sent an array. The signature is computed over whatever bytes are sent, so verification is identical either way. Every delivery carries these headers:

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
In any other language: split the header on commas into 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 ==.
Verify against the raw bytes of the request body, before any JSON parsing or re-serialization. Re-encoding the body first (different key order, whitespace) produces a different digest and every delivery will fail verification.

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 Gone deactivates 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/webhooks marks it "deadLetter": true so you can tell a final failure from a retry that is still coming.
  • A 410 is 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": 410 and 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-Delivery to 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 fires step.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.
There is one subscription row per URL per organization, and everything on it is shared. If a sequence step and a Zapier trigger both point at the same URL, they are the same subscription: one secret, one active flag, one delivery history. Give each consumer its own URL path (/webhooks/demeterrr/steps and /webhooks/demeterrr/zaps) if you want them to be independent.
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 isActive to 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 Gone from 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.
Changing a step’s 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:
This is the endpoint to use if you lost a secret or need to see why a delivery is failing, rather than re-subscribing to get a fresh one.

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