Module 3 · API design

Lesson 17 — Webhooks and third-party APIs

Retries, timeouts and signatures: the payment-gateway webhook that confirms the ticket.

Published
In this lesson
  1. Exercise 1 — Defenses
  2. Exercise 2 — ACK + queue
  3. Exercise 3 — Out of order
  4. Exercise 4 — Outgoing
  5. Professor's summary

Exercise 1 — Defenses

  1. With the naive endpoint, a curl with {"status": "SUCCEEDED"} marks other people's payments: hacked in 30 seconds. The signature is the barrier.
  2. HMAC over the RAW body (the exact bytes): if you verify over a re-serialized request.data, a spacing/key-order change invalidates the signature (or worse, accepts variants). compare_digest is constant-time: == short-circuits and allows theoretical timing attacks.
  3. Signature of another body → the HMAC doesn't match (the timestamp is inside the signed material): 401. Old timestamp → 400: an intercepted payload is worthless after the window (5 min: the balance between unsynchronized clocks and the attack window; the provider signs the timestamp so you can't move it).

Exercise 2 — ACK + queue

  1. Synchronous: 3s+ response. With the provider's 5s timeout you're at the limit; if your DB slows down, the provider marks failure and retries while you're still processing → duplicates and storm.
  2. ACK in <100ms: validate + enqueue (or a thread spawn in simulation). The real processing lives in the worker with its own idempotency.
  3. exists(gateway_event_id) before creating: 1 record, 3×200. The duplicate is NOT an error: it is expected (at-least-once).

Exercise 3 — Out of order

  1. If your handler blindly applied status = payload["status"], the confirmed payment would go back to PENDING: the state machine (00b) prevents it — SUCCEEDED → PENDING is not a legal transition and raises InvalidTransition.
  2. To the legal event: 200 (processed). To the illegal one: 200 with "ignored" (log the rejected transition) — answering 4xx/5xx would trigger endless provider retries for an event that simply arrived late and must not be applied. Exception: an UNKNOWN event (a new format you don't recognize) does deserve 4xx so the provider flags it... or 200 with a log: it depends on the provider; you document it in their checklist (§4).

Exercise 4 — Outgoing

  1. A deliveries table with attempts, next_retry_at and backoff 1m/5m/30m/2h/6h; the worker (or celery beat) queries "pending with next_retry <= now()".
  2. 503 → failed attempt → attempts+1 and next_retry scheduled; after 5, FAILED (and alert/panel: the organizer isn't receiving, which is a support incident, not silence).
  3. 410 Gone = the subscription is dead by the receiver's decision: deactivate the endpoint (flag) and notify the organizer through another channel. Retrying forever against 410 burns your queue — that is why you distinguish 5xx (retry) from 410 (don't retry) and auth 4xx (don't retry; check credentials).

Professor's summary

  • Zero trust: signature over raw body + anti-replay window + idempotency by event_id.
  • Fast ACK + worker: the provider retries with a timeout; your slowness becomes a storm.
  • The state machine absorbs disorder; illegal events get ignored with 200 and a log.
  • What you demand on receive, you deliver on send: signature, backoff retries, and knowing when NOT to retry (410).

The APIs module closes. Next: Lesson 18 — Sessions vs tokens (the Security module).