Webhooks
Get notified when work finishes, without holding a connection open.
Register an endpoint
curl -sX POST $RTFC/webhooks \
-H "X-Auth-Token: $TOKEN" -H 'Content-Type: application/json' \
-d '{"url": "https://yourapp.example/hooks/rtfc",
"events": ["claim.completed"],
"description": "production"}'{"id": "…", "url": "…", "events": ["claim.completed"], "active": true,
"secret": "whsec_…", "secret_prefix": "whsec_Sr3ozl-Y"}The secret is returned exactly once - store it now. Later reads show only the prefix, so a leaked listing cannot be used to forge signatures.
Subscribing to no events means all of them.
Events
| Event | When |
|---|---|
claim.completed | A claim finished research and has a best answer |
debate.ingest_completed | Ingest finished and every claim is settled |
debate.paused | Research was paused (credit or quota) |
quota.threshold_reached | The account crossed 80% or 100% of its allowance |
brand.mention_alert | A brand monitor found new mentions of a brand in an account's latest videos |
New events may be added. Ignore an event you do not recognise rather than erroring - that is the contract.
Headers: X-RTFC-Event, X-RTFC-Delivery, X-RTFC-Signature.
Verify the signature - do not skip this
Anyone can POST to your URL. The signature is what proves a delivery came from us.
X-RTFC-Signature: t=1735689600,v1=5257a869e7ecebeda32affa62cdca3fa…v1 is HMAC-SHA256 over "{t}.{raw body}" - the timestamp is inside the signed payload on purpose. Signing the body alone would let anyone who once captured a valid delivery replay it forever, since the signature would stay valid.
import hashlib, hmac, time
def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
try:
parts = dict(p.split("=", 1) for p in header.split(","))
ts, received = int(parts["t"]), parts["v1"]
except Exception:
return False
if abs(time.time() - ts) > tolerance: # reject replays
return False
expected = hmac.new(secret.encode(),
f"{ts}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)Three things people get wrong:
- Compute over the RAW body. - Not a parsed-and-re-serialised object - key order and whitespace differ and the HMAC will not match.
- Check the timestamp. - Without it the replay protection does nothing.
- Compare in constant time. -
==leaks timing.
Delivery, retries, duplicates
Respond 2xx promptly. Anything else is retried with backoff - 1m, 5m, 30m, 2h, 6h - then given up. A 4xx other than 408/429 is not retried: you rejected the request itself and an identical resend cannot help.
Do the real work asynchronously and acknowledge fast. A slow receiver looks like a failing one.
Delivery is at-least-once. A receiver that succeeds after the connection drops will be retried, so deduplicate on the payload's id - it is stable across retries of the same delivery.
Debugging
curl -s $RTFC/webhooks/$ID/deliveries -H "X-Auth-Token: $TOKEN"The delivery log shows status, attempt count, next retry and error for each attempt - enough to tell "we never sent it" from "your endpoint 500'd".
POST /v1/webhooks/{id}/rotate-secret issues a new secret. The old one stops working immediately, so deploy the new one to your receiver first.