Skip to main content
A webhook consumer that “works on the happy path” is easy. A consumer that survives retries, duplicates, out-of-order arrival, late replays, and partial failures is the actual goal. This page is the checklist of patterns that get you there. For the delivery contract, see Retry & delivery. For the document shape, see Webhook payload.

The non-negotiables

1. Verify the signature first

Before parsing, before logging, before anything. See Verifying signatures.

2. Deduplicate by (id, etag)

Two deliveries with the same order id and the same etag are the same state snapshot — process it once.

3. Guard ordering with ordVersion

Deliveries can arrive out of order. Ignore any snapshot whose ordVersion is lower than the highest you have processed for that id.

4. Persist, then acknowledge

Respond success only after durable persistence. An acknowledged delivery is never redelivered automatically.

Deduplicate by (id, etag)

Delivery is at-least-once: retries, and much later operator-driven replays, can hand you the same snapshot multiple times. The (id, etag) pair identifies one state snapshot of one order — use an atomic insert on it as the dedupe gate:
If the same snapshot arrives twice concurrently, only one insert succeeds; the other path acknowledges without reprocessing.
Never deduplicate by signature. t and v1 in X-Webhook-Signature are fresh on every attempt — the signature of a retry never matches the original. Only payload fields (id, etag, ordVersion) are stable across attempts.

Guard ordering with ordVersion

Retries are asynchronous, so deliveries can arrive out of order. The classic case: an order settles as SUCCESS (say ordVersion: 3), your endpoint is briefly down, and the order is later REFUNDED (ordVersion: 4). The REFUNDED delivery succeeds immediately, then the retry of the older SUCCESS snapshot arrives afterwards. Without a guard, your ledger would roll the refund back to “paid”. ordVersion is a monotonic snapshot version — it increments on every state change. Track the highest version you have applied per order and refuse to go backwards:
With this guard, the late SUCCESS retry (version 3) is ignored because version 4 is already applied — your local view stays monotonically consistent no matter how scrambled the arrival order is.

Capture the raw body bytes

The signature is computed over the exact bytes on the wire. Any middleware that parses and re-serializes the JSON changes the bytes and breaks verification. Framework pitfalls:

Verify defensively

  • Constant-time comparison, always. A plain == on the signature leaks timing information that lets an attacker forge signatures byte by byte. Use crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal, MessageDigest.isEqual, hash_equals, or CryptographicOperations.FixedTimeEquals.
  • Enforce a freshness window. Reject deliveries where |now − t| exceeds 5 minutes. It bounds replay of captured requests and costs you nothing: every retry and every operator replay carries a fresh t and v1, so a rejected stale delivery is always redelivered with a valid recent signature.
  • Verify before parsing. Any unauthenticated request that reaches your business logic is an injection vector for fake payment confirmations. Verify, then parse, then act.
  • Never authenticate by source IP. The signature is the authentication contract; source IPs are not part of it and are not guaranteed stable. An IP allowlist may complement verification at your edge, but must never replace it.

Persist, then acknowledge

An acknowledged delivery is never redelivered automatically. If you respond success and then crash before persisting, that event is gone from your side. The order of operations inside the 10-second window is therefore fixed:
1

Verify the signature

Reject with 401 on failure — the retry arrives with a fresh signature.
2

Persist durably

Database write or durable queue. If persistence fails, respond 5xx — Bloobank retries.
3

Acknowledge

200, Content-Type: text/plain, body success. Only now is the delivery settled on both sides.
4

Do the real work asynchronously

Downstream calls, notifications, reconciliation — on a background worker, with your own internal retry logic. Bloobank’s retry contract ends at the acknowledgement.

Protect the secret

The webhook secret is symmetric — anyone holding it can forge deliveries that pass your verification. Handle it like a private key:

Rotate the secret in coordination with Bloobank

There is a single active secret per merchant — it signs deliveries to both your cash-in and cash-out endpoints. Rotation is a coordinated cutover scheduled with Bloobank, not a self-service operation, and there is no overlap window where two secrets are valid. Plan the cutover accordingly:
  1. Contact Bloobank to schedule the rotation.
  2. Stage the new secret in your secret manager ahead of the agreed cutover time.
  3. At cutover, switch your verifier to the new secret. Deliveries signed with the old secret that are retried after the cutover are re-signed with the new secret — the freshness window and retry contract make the switch safe.
  4. Watch your verification-failure metric during the window: a sustained burst means one side switched and the other did not.

Observability checklist

Next

Verifying signatures

Worked example, test vector, six reference verifiers.

Retry & delivery

Backoff schedule and operator-driven replay.

Webhook payload

The canonical PaymentOrder document.