View Categories

Webhooks

Webhooks let your server receive a real-time HTTP callback whenever a configured Agent event occurs (a prompt is submitted, a CTA is clicked, a guardrail trips, and more). Each webhook notification sends an HTTP POST with a JSON body to the URL you provide in Apologist Ignite.

This page is for developers receiving these webhooks: it describes delivery, the payload shape, security modes, and how to verify signatures. Agents can also send email and Slack notifications for the same events; those use the same payload data but different transports and are not covered in detail here.

Configuration #

Webhook notifications are configured per Agent in Apologist Ignite (Notifications). You choose:

  • Which events to subscribe to
  • The destination URL
  • An optional secret, and whether to use signature mode (sign_request) or shared-secret mode

Webhook delivery does not use your Fusion API key. Securing the endpoint is your responsibility via the modes below. For inbound Fusion API calls, see Authentication on the Overview page.

Delivery #

  • Method: POST
  • Content-Type: application/json
  • Body: the canonical JSON payload described below (sent verbatim)
  • A 2xx response is treated as success. Non-2xx responses or connection errors are logged on our side; there is currently no automatic retry, so acknowledge quickly and do heavy work asynchronously.

Headers #

HeaderWhen sentDescription
Content-Typealwaysapplication/json
X-Webhook-Secretshared-secret mode onlyThe plaintext shared secret you configured
X-Webhook-Timestampsignature mode onlyUnix time in seconds at which the request was signed
X-Webhook-Signaturesignature mode onlysha256=<hex> — the HMAC-SHA256 signature

Payload Structure #

The body is a JSON object. notification, event, and agent are always present; the remaining sections appear only when relevant to the event.

{
  "notification": { "id": 17, "name": "Prod Hook" },
  "event": {
    "key": "cta_click",
    "label": "Call to Action Click",
    "occurred_at": "2026-06-14T20:54:37.123Z"
  },
  "agent": { "id": 7, "name": "Example Agent" },
  "completion": {
    "id": "11111111-1111-1111-1111-111111111111",
    "prompt": "…",
    "response": "…",
    "language": "en",
    "liked": null,
    "flagged": false,
    "cached": false,
    "client": "api",
    "user_id": "crm-123",
    "device_id": null,
    "session_id": null,
    "conversation_id": "conv-1",
    "prompted_at": "…",
    "response_started_at": "…",
    "response_completed_at": "…",
    "response_duration": 1.234,
    "automations": [
      {
        "id": "22222222-2222-2222-2222-222222222222",
        "evaluator_id": 2,
        "evaluator_name": "Relevance",
        "agent_id": 7,
        "benchmark_run_id": null,
        "benchmark_question_id": null,
        "content": "…",
        "started_at": "2026-06-14T20:54:38.000Z",
        "completed_at": "2026-06-14T20:54:39.100Z",
        "score": 0.92,
        "passed": true,
        "reasoning": "…",
        "input_tokens": 120,
        "output_tokens": 40,
        "reasoning_tokens": null,
        "evaluator_option_id": 55,
        "responder_id": null,
        "tag_id": 3
      }
    ],
    "tags": [
      { "id": 3, "name": "Praise" }
    ]
  },
  "channel": { "id": 3, "name": "SMS" },
  "platform": { "id": 1, "name": "Twilio" },
  "cta": { "id": 9, "name": "Buy", "content": "…" },
  "guardrail": { "id": 4, "name": "Off-topic" },
  "evaluator": { "id": 2, "name": "Relevance" },
  "evaluation": { "score": 1, "passed": true, "content": "…" }
}
SectionIncluded when
notificationAlways — the notification’s id and name
eventAlways — key, human-readable label, ISO-8601 occurred_at
agentAlways
completionThe event is tied to a prompt/completion (includes automations and tags when available, plus timing and identity fields)
channel / platformThe completion arrived via a channel (e.g. SMS, Discord)
ctacta_trigger, cta_click
guardrailguardrail_trigger
evaluator / evaluationCTA or guardrail events that ran an evaluation

Treat the payload as additive: new keys may be introduced over time. Ignore fields you do not recognize rather than failing the request.

Event keys #

KeyLabel
prompt_submitPrompt Submit
response_startResponse Start
response_endResponse Complete
automations_endAutomations Complete
response_likeResponse Like
response_flagResponse Flag
response_feedbackResponse Feedback
referral_clickSource Referral Click
cta_triggerCall to Action Trigger
cta_clickCall to Action Click
guardrail_triggerGuardrail Trigger
attribution_clickAttribution Click
footer_clickFooter Click
new_userNew User
new_deviceNew Device
new_sessionNew Session
new_conversationNew Conversation
errorError

Security Modes #

Each webhook notification is configured with an optional secret and a sign_request flag that selects how that secret is used.

1. Shared-secret mode (default) #

When sign_request is false (the default) and a secret is set, the plaintext secret is sent in the X-Webhook-Secret header. Verify by comparing it to your stored secret using a constant-time comparison.

This mode is simple but places the secret on the wire with every request. Prefer signature mode for new integrations.

2. Signature mode (HMAC-SHA256) — recommended #

When sign_request is true and a secret is set, the secret never leaves our servers. Instead we send:

  • X-Webhook-Timestamp: <unix-seconds>
  • X-Webhook-Signature: sha256=<hex>

The signature is the lowercase-hex HMAC-SHA256 of the string ${timestamp}.${rawBody} (the timestamp, a literal ., then the exact raw request body), keyed by your secret:

signature = HEX( HMAC_SHA256( secret, timestamp + "." + rawBody ) )

Binding the timestamp into the signed string lets you reject replayed requests.

Verifying the Signature #

  1. Read the raw request body as bytes/text before JSON parsing — you must sign the exact bytes you received, not a re-serialized object.
  2. Read X-Webhook-Timestamp and reject the request if it is too old (e.g. more than 5 minutes from now) to mitigate replay attacks.
  3. Compute HMAC_SHA256(secret, timestamp + "." + rawBody) and hex-encode it.
  4. Compare sha256=<computed> to the X-Webhook-Signature header using a constant-time comparison. Reject on mismatch.
  5. Only after the signature is valid should you parse and act on the JSON.

Best Practices #

  • Always verify the signature (or shared secret) before trusting a payload.
  • Sign over the raw request bytes — never a re-serialized object.
  • Use a constant-time comparison (crypto.timingSafeEqual, hmac.compare_digest) to avoid timing attacks.
  • Enforce a timestamp tolerance and, ideally, de-duplicate on a value you persist to defend against replays.
  • Respond quickly with a 2xx; offload slow processing to a queue.
  • Keep your secret confidential and rotate it if you suspect exposure.