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:
Guard ordering with ordVersion
Retries are asynchronous, so deliveries can arrive out of order. The classic case: an order settles asSUCCESS (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:
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. Usecrypto.timingSafeEqual,hmac.compare_digest,hmac.Equal,MessageDigest.isEqual,hash_equals, orCryptographicOperations.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 freshtandv1, 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 respondsuccess 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:- Contact Bloobank to schedule the rotation.
- Stage the new secret in your secret manager ahead of the agreed cutover time.
- 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.
- 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.