Five minutes to register an outbound webhook, receive a signed event, and verify its signature.
ragctl webhooks demo # sign + deliver a sample event in-process
ragctl webhooks demo --fail 2 # watch the retry path (2 failures, then success)It prints the delivery status, the X-AgentContextOS-Signature header, and
whether the signature verifies — no gateway, no network.
uv run uvicorn rag_gateway.app:app --port 8000curl -sX POST localhost:8000/v1/webhooks/subscriptions \
-H 'x-tenant-id: acme' -H 'x-principal-id: svc' \
-d '{"url":"https://your-app.example/webhooks","event_types":["ingest.completed"]}'The response includes "secret": "whsec_…" — copy it now; every later read
masks it. event_types: [] subscribes to all events.
curl -sX POST localhost:8000/v1/webhooks/subscriptions/whsub_…/test \
-H 'x-tenant-id: acme' -H 'x-principal-id: svc'Your endpoint receives a POST with the signed envelope. A real
ingest.completed fires automatically whenever a document finishes ingesting
via POST /v1/ingest/document.
from rag_webhooks import verify # or re-implement the scheme in your language
@app.post("/webhooks")
async def receive(request):
body = await request.body()
sig = request.headers["X-AgentContextOS-Signature"]
if not verify(body, MY_SECRET, sig, tolerance_s=300):
return Response(status_code=400)
event = json.loads(body)
# dedupe on event["id"] — delivery is at-least-once
...
return Response(status_code=200) # 2xx ACKs the deliveryThe HMAC is HMAC-SHA256(secret, f"{timestamp}.{body}"); the header is
t=<timestamp>,v1=<hex>. Reject deliveries whose timestamp is outside your
tolerance (replay protection), and dedupe on event.id — a retry after a
slow ACK can deliver the same id twice.
webhooks:
max_attempts: 5
timeout_s: 8.0
subscriptions:
- tenant_id: acme
url: https://your-app.example/webhooks
event_types: [ingest.completed, audit.policy_violation]
secret: ${ACME_WEBHOOK_SECRET}- reference/webhooks.md — event catalogue, full API, headers.
- architecture/webhooks.md — delivery + signing design.
- ADR-0018 — semantics + package boundary.