Webhook idempotency keys: why retries need them
A request that times out is indistinguishable from a request that never arrived. Your code doesn't know which one happened — only that it didn't get a response — so the safe move is usually to retry. That's correct for the network call, and wrong for the side effect: if the original request actually did land, the retry creates a second job for the same event.
Where this bites in practice
A serverless function calling POST /v1/ingest/:endpointKey hits a cold-start timeout right as the request completes server-side. The caller sees a timeout and retries. Without anything tying the two calls together, that's two jobs, two deliveries, and — depending on what the destination does with an order-created event — a duplicate charge, a duplicate email, or a duplicate row.
The fix: an idempotency key
POST /v1/ingest/:endpointKey accepts an optional idempotencyKey field alongside payload:
POST /v1/ingest/:endpointKey
{
"targetIdentifier": "customer-123",
"idempotencyKey": "order-456-created",
"payload": { "any": "json" }
}A repeat call to the same endpoint with an already-used idempotencyKey doesn't create a new job — it returns the original one, with idempotent: true so you can tell the difference in logs or tests:
{
"jobId": "9c7e21a4-...",
"status": "DELAYED",
"idempotent": true,
"note": "A job with this idempotencyKey already exists."
}A fresh call still gets 202 with a new job in the DELAYED state:
{
"jobId": "9c7e21a4-...",
"scheduledFor": "2026-08-22T14:32:07.000Z",
"status": "DELAYED"
}Picking a good key
The key needs to be stable across retries of the same logical event and unique across different events. An id you already have — an order id, a database row's primary key, a message id from whatever queue triggered the send — is almost always the right choice. order-456-created works because re-sending the same order produces the same key; a freshly generated UUID on every call would defeat the point, since it would never match a prior call and every retry would look like a new event.
What it doesn't need to be
idempotencyKey is optional — omit it and every call creates a new job, which is correct for events that are genuinely allowed to repeat (a heartbeat, a periodic status ping). Set it specifically on calls where a duplicate delivery would be a real problem: anything that triggers a charge, a state transition, or a notification a customer would notice twice.
Full reference
Every field, response shape, and error case for the ingest call is on Sending a webhook.