CitationBenchTalk to Sales

Webhooks — signed events for approvals, rank drops, and citations

Receive outbound events from CitationBench — approval requests, rank drops, AI citation drops, link placements, indexing outcomes. HMAC-SHA256 signed, retried, dead-lettered.

Receive outbound events from CitationBench in your service: approval requests, rank drops, citation drops, link placements, indexing outcomes. Signed with HMAC-SHA256. Retried on failure. Dead-lettered if all retries fail.

Endpoint shape

POST https://your-server.com/your-handler HTTP/1.1
Content-Type: application/json
CitationBench-Signature: t=1716537272,v1=8d2a3b1e...
CitationBench-Event-Id: evt_***
CitationBench-Event-Type: agent.invocation.awaiting_approval
CitationBench-Delivery-Attempt: 1

{
  "type":        "agent.invocation.awaiting_approval",
  "id":          "evt_***",
  "createdAt":   "2026-05-24T08:14:32Z",
  "workspaceId": "ws_***",
  "data": {
    "approvalId":   "appr_***",
    "invocationId": "inv_***",
    "skill":        "produce.publish",
    "preview":      { "...": "..." }
  }
}

Endpoints

MethodPathPurpose
GET/v1/webhooksList registered webhooks
POST/v1/webhooksRegister
GET/v1/webhooks/{id}Get one
PATCH/v1/webhooks/{id}Update (URL, events, secret rotation)
DELETE/v1/webhooks/{id}Delete
POST/v1/webhooks/{id}/testFire a test event
GET/v1/webhooks/dead-letterList events that failed all retries
POST/v1/webhooks/dead-letter/{id}/replayReplay a dead-lettered event

Register

curl -X POST https://api.citationbench.com/v1/webhooks \
  -H "Authorization: Bearer sk_live_***" \
  -d '{
    "url":    "https://your-server.com/webhooks/citationbench",
    "events": [
      "agent.invocation.awaiting_approval",
      "agent.invocation.completed",
      "rank.dropped",
      "ai_citation.share_of_voice.dropped",
      "link_building.link.placed"
    ],
    "description": "Production webhook for client portal"
  }'

Response

{
  "id": "whk_***",
  "url": "https://your-server.com/webhooks/citationbench",
  "events": ["agent.invocation.awaiting_approval", "..."],
  "secret": "whsec_***",
  "createdAt": "2026-05-24T08:00:00Z",
  "active": true
}

Save the secret immediately — you can't see it again. Use it to verify signatures.


Event catalog

Agent invocations

EventWhen
agent.invocation.startedInvocation moves from PENDING to RUNNING
agent.invocation.awaiting_approvalInvocation pauses for human review
agent.invocation.completedTerminal SUCCEEDED
agent.invocation.failedTerminal FAILED (includes error details)
agent.invocation.cancelledTerminal CANCELLED

Rank tracking

EventWhen
rank.checkedA rank check completed (fires per workspace per run)
rank.droppedA drop exceeded the configured threshold
rank.improvedA keyword gained 5+ positions
rank.lost_top_10A keyword fell out of top 10

AI citations (GEO)

EventWhen
ai_citation.sample.completedA daily sample run finished for a query
ai_citation.share_of_voice.droppedShare-of-voice dropped beyond threshold
ai_citation.share_of_voice.improvedShare-of-voice rose

Production

EventWhen
produce.blog_post.createdDraft ready
produce.blog_post.publishedPushed to a CMS
produce.landing_page.createdDraft ready
produce.landing_page.publishedPushed to a CMS
produce.publish.completedGeneric publish event
produce.evaluate.completedAn evaluation finished (with score)

Indexing

EventWhen
indexing.url.submittedSent to GSC / IndexNow
indexing.url.indexedVerified as indexed
indexing.url.not_indexedVerified as not indexed after a window
indexing.url.failedSubmission failure
EventWhen
link_building.draft.awaiting_approvalA campaign draft is ready for review
link_building.email.sentAn outreach email actually sent
link_building.email.openedRecipient opened
link_building.email.repliedRecipient replied
link_building.link.placedA link went live
link_building.inbound.receivedAn inbound message was ingested
link_building.inbound.requires_reviewAn inbound was escalated by an eval gate

Research

EventWhen
research.discuss.pain_point.surfacedA new high-signal pain point detected
research.content_gap.report.createdA gap report is ready

Signature verification

Every webhook is signed. Verify with your secret.

TypeScript

import crypto from "node:crypto";

function verify(
  body: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const [tPart, v1Part] = signatureHeader.split(",");
  const timestamp = tPart.split("=")[1];
  const v1Sig = v1Part.split("=")[1];

  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return false; // 5-minute replay protection
  }

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${body}`)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(v1Sig), Buffer.from(expected));
}

Python

import hmac, hashlib, time

def verify(body: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(p.split("=") for p in signature_header.split(","))
    timestamp = int(parts["t"])
    v1        = parts["v1"]

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

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.{body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(v1, expected)

Retry policy

AttemptBackoff
1immediate
25 s
330 s
45 min
5dead-letter

Failures (anything other than 2xx) trigger a retry. After the 4th retry, the event goes to the dead-letter queue.

Dead-letter queue

# List
curl .../v1/webhooks/dead-letter \
  -G --data-urlencode "since=2026-05-01T00:00:00Z"

# Replay
curl -X POST .../v1/webhooks/dead-letter/dlq_***/replay

Dead-lettered events are retained for 30 days.

Test a webhook

curl -X POST .../v1/webhooks/whk_***/test \
  -d '{ "event": "agent.invocation.awaiting_approval" }'

Sends a fake event of the chosen type. Useful for end-to-end testing your handler.

Best practices

  • Respond fast. Return 2xx within 5 seconds. Process asynchronously after returning.
  • Idempotency. Use CitationBench-Event-Id to dedupe — the same event can re-deliver after a retry.
  • Verify signatures always. Including in dev (use a separate dev webhook with a different secret).
  • Subscribe narrowly. Don't subscribe to * and filter server-side; subscribe to the specific events you handle.
  • Rotate secrets quarterly. PATCH /v1/webhooks/{id} with a new secret rotates without losing in-flight deliveries.

Errors during delivery

The delivery log on a webhook surfaces per-attempt errors:

curl .../v1/webhooks/whk_***/deliveries \
  -G --data-urlencode "since=2026-05-20T00:00:00Z"

Each delivery shows the response code, body excerpt, latency, and attempt number.

On this page