Long operations and webhooks

Topics in this section
2 cards

202 Accepted and a status resource

Some operations take minutes: generating a report, transcoding a video, importing a CSV. Holding the HTTP request open that long fails on timeouts in proxies and load balancers, and a retry would start the job twice.

Instead, accept the job and return 202 Accepted at once, with, by convention, a Location header pointing to a status resource: /exports/77. The client polls it (GET /exports/77) and sees status: queued → running → succeeded or failed. When it's done, the status resource links to the result. Tell clients how often to poll (commonly with a Retry-After header) so they don't hammer the server.

POST returns 202 with a status URL; the client polls until the job succeeds and fetches the result
In real life

Cloud providers return an operation id for long jobs such as creating a database; the client polls the operation until it's done.

In code: HTTP
POST /exports
{"from": "2026-01-01", "to": "2026-09-30"}
→ 202 Accepted
Location: /exports/77

GET /exports/77
→ 200 OK
Retry-After: 10
{"id": "77", "status": "running", "progress": 0.4}

GET /exports/77
→ 200 OK
{"id": "77", "status": "succeeded", "result_url": "/exports/77/file"}
  • 202 means 'accepted for processing', not 'done': the job can still fail, and the status resource must say so.
  • Combine with an idempotency key so a retried POST doesn't start a second export.
Check yourself

Generating a yearly report takes about 3 minutes. Which designs work? (Choose two.)

Select 2, then check.

Webhooks done right

A webhook turns polling around: when something happens (payment succeeded, export ready), your server sends an HTTP POST to a URL the client registered. Three things make webhooks reliable.

Signatures: anyone can POST to the client's URL, so sign each delivery, typically an HMAC-SHA256 of a timestamp plus the body with a secret shared with that client; the client recomputes and compares, and rejects old timestamps to stop replays. Retries: if the client doesn't answer 2xx, retry with backoff for hours or days. Duplicates and order: retries mean the same event can arrive twice and out of order, so each event carries a unique event id the client uses to ignore repeats, and clients should fetch the current state rather than trust event order.

In real life

Stripe signs webhooks in a Stripe-Signature header with a timestamp and an HMAC-SHA256 signature, retries failed deliveries for up to three days, and tells receivers to handle duplicate events.

In code: Python
import hmac, hashlib, time

def verify(secret: bytes, body: bytes, ts: str, signature: str) -> bool:
    if abs(time.time() - int(ts)) > 300:          # older than 5 minutes: replay
        return False
    expected = hmac.new(secret, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)  # constant-time compare

seen = set()   # in production: a table of processed event ids
def handle(event):
    if event["id"] in seen:
        return 200                                  # duplicate: acknowledge, do nothing
    ...                                             # do the work, then:
    seen.add(event["id"])                         # record only after success,
    return 200                                      # in production in the same transaction
  • compare_digest avoids leaking the signature through timing differences.
  • Answer 2xx quickly and do the work in the background, or the sender will time out and retry.
Check yourself

You're building the receiving side of a payment provider's webhooks. What should your endpoint do? (Choose three.)

Select 3, then check.