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 — The defense-less webhook (broken on purpose)
  2. Exercise 2 — Fast ACK + deferred processing
  3. Exercise 3 — Out of order
  4. Exercise 4 — Your outgoing webhook
  5. Exercise 6 — The incoming webhook's fortress
  6. Submit

Gateway simulator included (your own script signs and sends). Do not look at solutions.md before submitting.

Exercise 1 — The defense-less webhook (broken on purpose)

  1. Implement POST /api/payments/webhook/ trusting the body ({"event_id", "payment_id", "status", "timestamp"}) and mark the payment SUCCEEDED. Test it with curl. How long did it take to hack yourself? (anyone with your URL just charged).
  2. Add HMAC-SHA256 signing: X-Signature = hex(hmac(secret, timestamp + "." + raw_body)). Verify with hmac.compare_digest (why not ==?).
  3. Attacks to try: valid body without signature → 401; a signature of ANOTHER body → 401; the same body+signature with a timestamp from 1 hour ago → 400 (replay). Add the 5-minute window.

Exercise 2 — Fast ACK + deferred processing

  1. Simulate the queue: process_gateway_event(payload) that takes 3s (sleep) + creates the record. First synchronous: measure the webhook's response time with curl (how much? what would the gateway do with a 5s timeout?).
  2. Wrap it in transaction.on_commit + a simulated queue (background thread or delay() if you already set up Celery). Answer 200 in <100ms and process just the same.
  3. Demonstrate idempotency: the same event_id 3 times → 1 record created, 2×200 responses.

Exercise 3 — Out of order

  1. Send payment.succeeded and THEN payment.pending for the same payment. Does your handler regress the payment to PENDING? (if yes: a state-machine bug — 00b's confirm()/transitions must reject the illegal transition).
  2. What status do you answer each one? (the illegal one: 400/409/200-ignored? Justify against the provider's retry policy).

Exercise 4 — Your outgoing webhook

  1. Create notify_organizer(event_id, payload): sign it just like you verify incoming, POST to a test URL (requestbin or your own test endpoint), retries with backoff 1m/5m/30m/2h/6h (a outgoing_webhook_delivery(attempts, next_retry, status) table).
  2. Simulate the URL being down (503): verify it retries per the schedule and that after 5 attempts it is marked FAILED for manual review.
  3. What do you do with a URL answering 410 Gone permanently? (hint: don't retry forever — deactivate the subscription and notify).

Exercise 6 — The incoming webhook's fortress

  1. Implement §6's checklist on your fake-gateway endpoint: all 7 pieces, each with its test (verification, window, dedup, ACK+queue, rate limit, secret filtering in logs, invalid-signature metric).
  2. The replay attack: capture a valid webhook and forward it as-is after 10 minutes: does the window reject it? And after 2 minutes with a different event_id: does dedup accept it or duplicate it? Document both limits.
  3. The outbox for outgoing: migrate Lesson 17's outgoing webhooks to the outbox→poller mechanism (25): an atomicity test (failed transaction → zero enqueued webhooks) and a retry test (the other gateway down → backoff → convergence).

Submit

Paste code, curl outputs and the retry table. Module 3 closes; next Lesson 18 — Sessions vs tokens.