Idempotency and deduplication
Stop a producer's retries from creating duplicate events by telling a source which fields identify a message.
Webhook producers retry. If your endpoint is slow or a network hop drops the response, the same message can arrive twice. Left alone, each copy becomes its own event and its own delivery, and your destination does the work twice.
A source can deduplicate on fields you choose. When a request matches an event that source already stored, SolidHooks answers success and does nothing else.
How it works
Set idempotencyKeys on a source to a list of paths. For each request:
- SolidHooks reads every listed path from the request.
- If all of them resolve, the values are joined and hashed into one key.
- If an event with that key already exists for the same source, the request is a duplicate.
- Otherwise the event is stored with that key and routed as normal.
The check runs only after the request has passed signature verification. An unverified request never claims a key, so an attacker cannot use a forged request to make a later legitimate event look like a duplicate.
Paths
Each entry is a dot-path with one of two prefixes.
| Prefix | Reads | Example |
|---|---|---|
headers. | A request header. One level only, matched case-insensitively. | headers.x-webhook-id |
body. | A field of the parsed JSON body. Nested paths are allowed. | body.data.id |
A path that does not start with headers. or body. is rejected with 400.
curl -s "$API/v1/sources" \
-H "authorization: Bearer $SDHK_KEY" \
-H 'content-type: application/json' \
-d '{
"name": "Orders",
"provider": "generic",
"idempotencyKeys": ["headers.x-webhook-id"]
}'Several paths make one key
The key is built from all the paths you list, in order. If any one of them is missing from a request, no key is built and that request is not deduplicated at all. It is stored as a normal event.
This is why you should list only fields that every request carries. Listing headers.webhook-id and headers.svix-id together does not cover both spellings. Every request has one or the other, never both, so the key would never build and deduplication would silently stop.
Defaults
idempotencyKeys is empty unless you set it, with one exception. When you create a source for a Standard Webhooks provider and send no idempotencyKeys of your own, SolidHooks fills one in. The id header is mandatory for every producer that signs with the scheme, so the default is always present.
provider | Default idempotencyKeys |
|---|---|
openai | ["headers.webhook-id"] |
anthropic | ["headers.webhook-id"] |
resend | ["headers.svix-id"] |
Resend defaults to svix-id because that is the header Svix actually sends. If your Resend account white-labels its headers, set ["headers.webhook-id"] yourself.
Anything you send always wins over the default, including an explicit empty list. "idempotencyKeys": [] turns deduplication off.
What a duplicate looks like
The producer gets a success response, so it stops retrying:
{ "ok": true, "duplicate": true }A first-time request returns {"ok":true,"eventId":"evt_..."}.
A duplicate:
- is not stored. There is no second event to find in
GET /v1/events. - is never counted against your delivery quota.
- is never delivered.
The ingest rate limit still applies to duplicates. They count as requests, because a flood of duplicates would otherwise be free to send.
How long deduplication lasts
Deduplication is scoped to one source. SolidHooks looks the key up among that source's stored events, so a key is remembered for as long as the original event is. There is no separate dedupe window. When the plan's retention period purges the original event, the same key will be accepted as new.
The lookup and the insert are separate steps. Two copies of one message arriving at almost the same instant are not guaranteed to be caught.
Rejected events are never deduplicated
A request that fails verification is stored as a rejected event with no idempotency key. That is deliberate: it can neither match an earlier event nor reserve a key for one.
Email sources
Email sources do not use idempotencyKeys. They deduplicate on the message's Message-ID, and setting idempotencyKeys on an email source is rejected with 400. See Email ingestion.
Next steps
- Signature verification — authenticate requests before they can claim a key
- Email ingestion — how email sources deduplicate
- Sources — the rest of a source's settings
- Replay — re-run a stored event on purpose