RTFC API

Webhooks

Get notified when work finishes, without holding a connection open.

Register an endpoint

bash
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"}'
json
{"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

EventWhen
claim.completedA claim finished research and has a best answer
debate.ingest_completedIngest finished and every claim is settled
debate.pausedResearch was paused (credit or quota)
quota.threshold_reachedThe account crossed 80% or 100% of its allowance
brand.mention_alertA 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.

python
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:

  1. Compute over the RAW body. - Not a parsed-and-re-serialised object - key order and whitespace differ and the HMAC will not match.
  2. Check the timestamp. - Without it the replay protection does nothing.
  3. 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

bash
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.