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

New events may be added. Ignore an event you do not recognise rather than erroring - that is the contract.

json
{
  "id": "evt_0a86dae901e64fae99aebaa64b376cab",
  "event": "claim.completed",
  "created_at": "2026-08-24T12:31:08.512Z",
  "data": {"debate_id": "…", "claim_id": "…", "verdict": "false",
           "confidence": 0.97, "explanation": "…"}
}

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)   # constant time
js
const crypto = require("crypto");

function verify(secret, rawBody, header, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const ts = parseInt(parts.t, 10);
  if (Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
  const expected = crypto.createHmac("sha256", secret)
                         .update(`${ts}.`).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

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.

  1. Check the timestamp. Without it the replay protection does nothing.
  2. Compare in constant time. == leaks timing.

Delivery, retries, duplicates

Respond 2xx promptly. Anything else is retried with backoff - 1m, 5m, 30m, 2h, 6h

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 -sX POST $RTFC/webhooks/$ID/test    -H "X-Auth-Token: $TOKEN"   # send one now
curl -s     $RTFC/webhooks/$ID/deliveries -H "X-Auth-Token: $TOKEN"  # what happened

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.