Delivery, retries & idempotency
How Grand delivers webhooks, what's expected of your endpoint, and how to build a receiver that stays correct under retries and duplicates.
Responding
Return a 2xx status as soon as you've accepted the event. Anything else — a non-2xx, a
timeout, or a connection error — is treated as a failed delivery and retried.
Acknowledge fast, process asynchronously. Verify the signature, enqueue the event (or record
the idempotencyKey), and return 200 immediately. Do the profile re-fetch and downstream work
off the request path so slow processing can't cause timeouts and needless retries.
Your endpoint must:
- Be reachable over HTTPS.
- Accept a JSON
POST. - Respond within a few seconds.
Retries
Failed deliveries are retried automatically with increasing back-off. A retry carries the same
idempotencyKey as the original, so your endpoint may see the same event more than once.
Delivery is therefore at-least-once: aim for a receiver that's correct whether an event arrives once or several times.
Idempotency
Every event carries a stable idempotencyKey that identifies the underlying change. Use it to
de-duplicate:
- Before processing, check whether you've already handled this
idempotencyKey. - If yes, ack
200and stop. - If no, process, then record the key.
if (await seen(event.idempotencyKey)) return res.sendStatus(200); // duplicate
await handle(event);
await markSeen(event.idempotencyKey);
res.sendStatus(200);
Dedupe on the body's idempotencyKey, not on any header. Transport headers such as the
delivery layer's event id (x-grand-eventid / idempotency-key) are not Grand's idempotency
key and can differ across retries of the same change.
Ordering
Delivery order is not guaranteed — under retries a newer event can arrive before an older one. Two things make this a non-issue in practice:
- The payload never carries values — you always re-fetch the current profile, so you can't act on stale data.
- Each event includes
occurredAtif you need to reason about when a change happened.
Transport headers
Deliveries carry metadata headers added by the delivery layer (e.g. x-grand-attempt-count,
x-grand-eventid, and tracing headers like sentry-trace / baggage). These are informational
only and not part of Grand's contract — don't depend on them. The one header you rely on is
x-grand-signature.
Receiver checklist
- Endpoint is HTTPS and returns
2xxwithin a few seconds. - Signature verified over the raw body before processing.
- Duplicate events dropped by
idempotencyKey. - Profile re-fetched via
GET /v1/profile/{companyNumber}for current data. - Heavy work done asynchronously, off the request path.
- Unknown JSON fields ignored (the payload is additive).