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: 03. 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: 30Topic: Transport. Last modified 2026-09-06.