Every webhook delivery is signed with HMAC-SHA256 in the Standard Webhooks style. You don't have to implement verification yourself — clone a starter repo with a vendored verifier and a working handler for Modal or Railway.
import os
from fastapi import FastAPI, Request, Response
# verify_webhook ships vendored in each starter repo (explaining_markets/),
# so there's nothing to install — clone a starter and it's already there.
from explaining_markets import verify_webhook, WebhookVerificationError
app = FastAPI()
SECRET = os.environ["EM_WEBHOOK_SECRET"] # whsec_...
@app.post("/competition/webhook")
async def hook(request: Request) -> Response:
raw_body = await request.body() # raw bytes — DO NOT use request.json()
try:
event = verify_webhook(
raw_body=raw_body,
headers=request.headers,
secret=SECRET,
)
except WebhookVerificationError:
return Response(status_code=401)
# event is the parsed JSON. Use event["id"] as your idempotency key.
process(event)
return Response(status_code=200)
| Header | Description |
|---|---|
Webhook-Id | Stable per logical event across retries. Use as your idempotency key. |
Webhook-Timestamp | Unix seconds, refreshed on each delivery attempt. |
Webhook-Signature | One or more space-delimited v1,<base64> signatures. Multiple appear during signing-secret rotation — accept the delivery if any signature verifies. |
json.dumps(json.loads(body)) reorders keys and adds spaces; verification fails.request.json() instead of request.body(). Same issue — the parsed dict is no longer the original byte string.tolerance_seconds= if your clocks drift.Webhook-Id. We retry on 5xx and timeout. Dedupe in your app or expect to process the same event more than once.Frozen test vectors let you confirm your verifier is wired up correctly without waiting for a real event delivery — both our server-side signer and the vendored verifier pin to these values. They cover a single-signature delivery and a rotation-overlap delivery (two space-delimited signatures, accept if either verifies). Fetch them at /test_vectors.json, or find them in each starter repo at tests/test_vectors.json.