HTTP APIs

larsggu.me › Examples

A webhook receiver, end to end

verify → record → acknowledge → process

Four steps, in the order they must happen. The code is written as plain pseudocode against the HTTP specification so it can be read in any language; the point is the ordering rather than the syntax.

1. Accept the request and read the exact bytes

The signature is computed over the body as it arrived. A framework that parses JSON and hands back a structure has already discarded the byte sequence the signature covers, because re-serialising a structure does not reliably reproduce the original ordering or whitespace. The receiver therefore reads the raw body first and parses it only after the signature has been checked.

The timestamp header is checked at the same time. A delivery whose timestamp is far outside a narrow window is refused regardless of whether the signature is valid, which bounds how long a captured request remains useful.

Accept the request and read the exact bytes

POST /hooks/inbound HTTP/1.1
Content-Type: application/json
Event-Id: ev_8fd21c
Delivery-Id: dl_41ab90
Timestamp: 2026-09-06T09:14:22Z
Signature: sha256=4c1b...9ae0

# read the body as bytes, before any parsing
raw = request.body_bytes
expected = hmac_sha256(secret, raw)
if not constant_time_equal(expected, header_signature): return 401
if abs(now - header_timestamp) > tolerance: return 401

2. Record the event, then acknowledge

The Event-Id is stable across redeliveries, so it is the value the receiver stores. Writing it with a uniqueness constraint makes the duplicate case a database refusal rather than a race the application has to reason about, and the refusal is answered with the same success status as a first delivery.

The acknowledgement is returned as soon as that write commits. Nothing downstream has happened yet, and nothing downstream should be allowed to delay this response.

Record the event, then acknowledge

try:
    store.insert(event_id=header_event_id, body=raw, state='received')
except UniqueViolation:
    return 202          # already seen; acknowledge again

return 202              # committed, not yet processed

HTTP/1.1 202 Accepted
Content-Length: 0

3. Process out of band

The stored event is picked up by a worker running on its own schedule. Because the acknowledgement has already been sent, a slow or failing downstream dependency now delays only this receiver’s own processing, and cannot cause the sender to retry.

The worker moves the event through explicit states so that an interrupted run resumes rather than restarts, and so that an operator looking at the store can see what is stuck.

Process out of band

for event in store.claim(state='received', limit=50):
    try:
        apply(event)
        store.mark(event.id, 'processed')
    except Retryable:
        store.mark(event.id, 'received', attempts=event.attempts + 1)
    except Permanent as exc:
        store.mark(event.id, 'failed', reason=str(exc))

4. Answer failures honestly

A body the receiver cannot parse is answered with a 400 rather than a 200. Answering successfully to hide the error removes the only signal the sending side has that the integration is broken, and the events are then lost silently.

A receiver that is temporarily unable to store events answers 503 with a Retry-After value. That is the response the sender's retry schedule is written for, and it produces a delay rather than a discarded event.

Answer failures honestly

# unparseable body
HTTP/1.1 400 Bad Request
{"error":{"type":"invalid_body","message":"Body is not valid JSON."}}

# receiver cannot store right now
HTTP/1.1 503 Service Unavailable
Retry-After: 30

Topic: Transport. Last modified 2026-09-06.