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:

  1. Before processing, check whether you've already handled this idempotencyKey.
  2. If yes, ack 200 and stop.
  3. 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 occurredAt if 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 2xx within 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).
The signature check every receiver must do first. The event types and payload contract.

Grand Public API