CiteFlow API

Webhooks

Stop polling. Receive signed events when state changes.

CiteFlow can push events to an HTTPS endpoint of your choice. Configure one or more endpoints at /dashboard/api/webhooks.

Up to 5 endpoints per workspace. Each endpoint subscribes to one or more event types and gets its own signing secret.

Event catalog

TypeFires when
audit.completedAudit reaches terminal complete state.
audit.failedAudit reaches terminal failed state.
audit.cancelledAudit reaches terminal cancelled state (via POST /audit/{id}:cancel).
balance.lowBalance drops below low_balance_threshold. Debounced 24h per threshold crossing.

Envelope

Every event posts JSON with a stable envelope:

{
  "id": "evt_01jbq6t8tjk0vfgz5gd9p4d4qa",
  "type": "audit.completed",
  "created_at": "2026-05-27T14:32:54.622Z",
  "data": { "...": "event-specific payload" }
}

audit.* data

{
  "audit_id": "vH4kZcLm9YxQrTpA2gWb",
  "status": "complete",
  "type": "seo",
  "url": "https://example.com",
  "scores": {
    "overall": 76, "seo": 81, "aeo": null, "geo": null
  },
  "failure_reason": null,
  "credits": { "charged": 80, "refunded": 0 },
  "created_at": "...",
  "completed_at": "..."
}

scores is null on audit.failed and audit.cancelled; failure_reason is null on audit.completed.

balance.low data

{
  "balance": 320,
  "balance_usd": "$3.20",
  "threshold": 500,
  "recommended_topup_tier": "starter"
}

Headers CiteFlow sends

POST /your/webhook HTTP/1.1
Content-Type: application/json
X-CiteFlow-Webhook-Id: evt_01jbq6t8tjk0vfgz5gd9p4d4qa
X-CiteFlow-Webhook-Timestamp: 1748400000
X-CiteFlow-Webhook-Signature: v1=base64(hmac-sha256(secret, "evt_…1748400000.<body>"))

Verifying the signature

The signed string is ${id}.${timestamp}.${rawBody}. The signature header may carry multiple comma-separated v1= values during the 7-day grace period after a secret rotation.

Node.js

import crypto from 'node:crypto';

function verify(rawBody: string, headers: Record<string, string>, secret: string) {
  const id = headers['x-citeflow-webhook-id'];
  const ts = headers['x-citeflow-webhook-timestamp'];
  const sig = headers['x-citeflow-webhook-signature'];
  if (!id || !ts || !sig) return false;

  // Reject anything older than 5 minutes to defeat replay attacks.
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${id}.${ts}.${rawBody}`)
    .digest('base64');

  return sig
    .split(',')
    .some((part) =>
      crypto.timingSafeEqual(
        Buffer.from(part.replace('v1=', '')),
        Buffer.from(expected),
      ),
    );
}

Python

import hmac, hashlib, base64, time

def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
    id_ = headers['x-citeflow-webhook-id']
    ts = headers['x-citeflow-webhook-timestamp']
    sig = headers['x-citeflow-webhook-signature']

    if abs(int(time.time()) - int(ts)) > 300:
        return False

    signed = f"{id_}.{ts}.".encode() + raw_body
    expected = base64.b64encode(
        hmac.new(secret.encode(), signed, hashlib.sha256).digest()
    ).decode()

    for part in sig.split(','):
        if hmac.compare_digest(part.replace('v1=', ''), expected):
            return True
    return False

Both functions must read the raw body bytes before any JSON parsing — signature is computed over the literal bytes CiteFlow sent.

Delivery & retries

  • 5-second HTTP timeout per attempt.
  • Retries: 5 attempts over ~24 hours with exponential back-off.
  • After 10 consecutive failures, the endpoint is auto-disabled and an email goes to the workspace contact.
  • Concurrency cap: 3 simultaneous deliveries per endpoint.

Replay

curl -X POST \
  "https://www.citeflow.io/api/v1/webhooks/deliveries/wd_…/replay" \
  -H "Authorization: Bearer ckf_…"

Re-fires the original payload with a fresh timestamp + signature. Use when your endpoint was down during the retry window or you want to reprocess events without touching CiteFlow.

409 WEBHOOK_ENDPOINT_DISABLED if the endpoint is currently disabled — re-enable from the dashboard first.

Secret rotation

Click Rotate secret in the dashboard. The old secret remains valid for 7 days; signatures during the grace window carry both:

X-CiteFlow-Webhook-Signature: v1=<old>,v1=<new>

Your verifier should accept either.

On this page