Skip to content

Webhooks

Overview

How OnCore delivers webhooks, and how to verify each kind.

OnCore pushes events to your server as HTTP POST requests with JSON bodies. Call and conversation events are sent to the assistant's serverUrl if set (per-assistant override), otherwise to your tenant-level server URL — both configured in your OnCore dashboard.

There are two delivery models, and they are verified differently:

ModelEventsVerificationRetries
Shared-secret headerVoice, messaging, and tool eventsPlaintext x-sadie-core-secret headerNone
HMAC-signedWhatsApp onboarding eventsX-OnCore-Signature: sha256=<hex> over the raw body5 attempts, exponential backoff

Both use the same secret value: your client server secret, shown on the API Keys page in your OnCore dashboard.

Model A — shared-secret header#

Voice, messaging, and tool deliveries carry your client server secret verbatim in a header:

Request headers
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
x-sadie-core-secret: YOUR_CLIENT_SERVER_SECRET

Compare it against the value you have on file and reject mismatches:

Express example
app.post("/sadie/webhooks", (req, res) => {
  if (req.header("x-sadie-core-secret") !== process.env.SADIE_CLIENT_SERVER_SECRET) {
    return res.status(401).end();
  }
  // handle req.body by its `type` field
  res.status(200).end();
});

No retries — respond fast

These deliveries are currently not retried: if your endpoint is down or returns a non-2xx status, the event is not redelivered. Respond quickly with a 2xx and do your processing asynchronously.

Model B — HMAC signature (WhatsApp events)#

WhatsApp onboarding events are signed instead. Each delivery carries:

  • X-OnCore-Signature: sha256=<hex> — HMAC-SHA256 of the raw request body bytes, keyed by the same client server secret
  • X-OnCore-Delivery: <uuid> — the delivery id

Verify against the raw body before JSON-parsing it, using a constant-time comparison:

Express example (raw-body HMAC verification)
import { createHmac, timingSafeEqual } from "crypto";
import express from "express";

const app = express();

app.post(
  "/sadie/whatsapp-webhooks",
  express.raw({ type: "application/json" }), // keep the raw bytes
  (req, res) => {
    const secret = process.env.SADIE_CLIENT_SERVER_SECRET!;
    const expected = `sha256=${createHmac("sha256", secret).update(req.body).digest("hex")}`;
    const received = req.header("X-OnCore-Signature") ?? "";

    const a = Buffer.from(expected);
    const b = Buffer.from(received);
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
      return res.status(401).end();
    }

    const event = JSON.parse(req.body.toString("utf8"));
    // dedupe on req.header("X-OnCore-Delivery") — retries reuse the same id
    res.status(200).end();
  },
);

Delivery is at-least-once: a non-2xx response or timeout is retried up to 5 times with exponential backoff. All retries of one event reuse the same X-OnCore-Delivery id — dedupe on it so a retried delivery isn't processed twice. Respond 2xx promptly to stop retries.

Event catalog#

EventFires whenModelPage
assistant-requestA voice call startsAAssistant request
end-of-call-reportA voice call endsAEnd-of-call report
assistant-request (messaging)A messaging conversation startsAEnd-of-conversation report
end-of-conversation-reportA messaging conversation endsAEnd-of-conversation report
Tool invocationThe assistant calls one of your toolsATool proxy
whatsapp.onboarding.tier_pending / completed / failed, whatsapp.calling.liveWhatsApp onboarding lifecycleBWhatsApp events

Telling calls and conversations apart

Voice events carry a call_id; messaging events carry a conversation_id plus channel and customer_identifier. Branch on the presence of these fields (or on type + channel) in a shared endpoint.