Skip to main content
Inbound webhooks let external systems (Five9, Stripe, anything that can POST) trigger one of your Definitions. You create a webhook config bound to an active Definition; the sender POSTs signed deliveries to a per-customer receive URL; each verified delivery starts a normal job (source: "webhook").
This surface is inbound-only — deliveries coming into GetDialed. Outbound webhooks (a flow calling an external URL) are a separate catalog action, not receiver machinery.

How inbound webhooks work

  1. An admin creates a webhook config (POST /webhooks) bound to an active Definition. GetDialed mints a signing secret and returns it once.
  2. GetDialed returns a receive_url — a relative path /webhooks/{slug}/{webhook_id}.
  3. Your external sender computes an HMAC-SHA256 signature over each delivery body and POSTs to the public receive URL.
  4. The receiver verifies the signature on the raw bytes, then starts a job with the normalized payload as input_data.

Step 1 — Create the webhook

The X-API-Key header is admin-equivalent; a Clerk JWT needs the admin role. Only provider: "standard" (Standard Webhooks, HMAC-SHA256) and mode: "trigger" are implemented today — other values are rejected with 422.
curl -X POST https://api.getdialed.ai/flows/webhooks \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "definition_id": "def_a1b2c3d4",
    "description": "Five9 call-completed events"
  }'
const res = await fetch("https://api.getdialed.ai/flows/webhooks", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    definition_id: "def_a1b2c3d4",
    description: "Five9 call-completed events",
  }),
});
const webhook = await res.json();
// Store webhook.signing_secret NOW — it is never returned again.
import os
import httpx

res = httpx.post(
    "https://api.getdialed.ai/flows/webhooks",
    headers={"X-API-Key": os.environ["API_KEY"]},
    json={
        "definition_id": "def_a1b2c3d4",
        "description": "Five9 call-completed events",
    },
)
webhook = res.json()
# Store webhook["signing_secret"] NOW — it is never returned again.
The 201 response carries two show-once fields, signing_secret and receive_url:
{
  "id": "8a9c86a8b78c48d3a3e5e00ad0ed5b96",
  "definition_id": "def_a1b2c3d4",
  "org_id": "org_12345678",
  "description": "Five9 call-completed events",
  "mode": "trigger",
  "provider": "standard",
  "enabled": true,
  "created_at": "2026-07-07T20:00:00Z",
  "updated_at": "2026-07-07T20:00:00Z",
  "signing_secret": "$WEBHOOK_SIGNING_SECRET",
  "receive_url": "/webhooks/acme/8a9c86a8b78c48d3a3e5e00ad0ed5b96"
}
The signing_secret is returned exactly once — here, and again only if you explicitly rotate it. It is never retrievable from any GET. Copy it into the sending system immediately. If you lose it, use POST /webhooks/{webhook_id}/rotate-secret (24h old-secret overlap).

Step 2 — Build the receive URL

The receive_url is a relative path with no host and no /flows prefix. Prepend the production base URL to get the address your sender calls:
https://api.getdialed.ai/flows/webhooks/{slug}/{webhook_id}
For the example above that is https://api.getdialed.ai/flows/webhooks/acme/8a9c86a8b78c48d3a3e5e00ad0ed5b96. The webhook_id is a 128-bit opaque hex value, so the URL is unguessable by construction. The tenant is identified by the slug; the webhook by the webhook_id.

Step 3 — Sign and send a delivery

The receiver carries no X-API-Key/Authorization requirement — the trust boundary is the HMAC-SHA256 signature over the raw request body, verified per the Standard Webhooks spec before the body is parsed. Every delivery must carry three headers:
HeaderDescription
webhook-idSender-chosen unique delivery ID. This is the idempotency key — resending the same webhook-id is safe (dedups to one job).
webhook-timestampUnix seconds. Deliveries outside a 5-minute replay window are rejected with 401.
webhook-signaturev1,<base64 HMAC-SHA256> computed over {webhook-id}.{webhook-timestamp}.{raw body} with the signing secret.
The standardwebhooks library computes the signature for you — never hand-roll the HMAC.
BODY='{"call_id": "C123", "disposition": "completed"}'
MSG_ID="msg_$(uuidgen)"
TS=$(date +%s)
SIG=$(python3 -c "
from standardwebhooks import Webhook
import datetime
print(Webhook('$WEBHOOK_SIGNING_SECRET').sign('$MSG_ID', datetime.datetime.fromtimestamp($TS), '$BODY'))
")

curl -i -X POST "https://api.getdialed.ai/flows/webhooks/acme/8a9c86a8b78c48d3a3e5e00ad0ed5b96" \
  -H "Content-Type: application/json" \
  -H "webhook-id: $MSG_ID" \
  -H "webhook-timestamp: $TS" \
  -H "webhook-signature: $SIG" \
  -d "$BODY"
import { Webhook } from "standardwebhooks";

const url =
  "https://api.getdialed.ai/flows/webhooks/acme/8a9c86a8b78c48d3a3e5e00ad0ed5b96";
const body = JSON.stringify({ call_id: "C123", disposition: "completed" });
const msgId = `msg_${crypto.randomUUID()}`;
const timestamp = new Date();

const wh = new Webhook(process.env.WEBHOOK_SIGNING_SECRET);
const signature = wh.sign(msgId, timestamp, body);

await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "webhook-id": msgId,
    "webhook-timestamp": Math.floor(timestamp.getTime() / 1000).toString(),
    "webhook-signature": signature,
  },
  body,
});
import os
import uuid
import datetime
import httpx
from standardwebhooks import Webhook

url = "https://api.getdialed.ai/flows/webhooks/acme/8a9c86a8b78c48d3a3e5e00ad0ed5b96"
body = '{"call_id": "C123", "disposition": "completed"}'
msg_id = f"msg_{uuid.uuid4()}"
timestamp = datetime.datetime.now()

wh = Webhook(os.environ["WEBHOOK_SIGNING_SECRET"])
signature = wh.sign(msg_id, timestamp, body)

httpx.post(
    url,
    headers={
        "Content-Type": "application/json",
        "webhook-id": msg_id,
        "webhook-timestamp": str(int(timestamp.timestamp())),
        "webhook-signature": signature,
    },
    content=body,
)
The receiver acks within ~50ms with 202 — job creation happens asynchronously after the ack.
StatusMeaning
202Accepted. A replayed webhook-id also returns 202 but does not dispatch again.
401Signature verification failed (missing header, expired timestamp, tampered body, or wrong secret — all return the same generic message).
404Unknown slug, unknown webhook_id, or a disabled webhook — never 401 for existence failures.
429Per-webhook rate limit exceeded (default 1000 deliveries/minute).

Step 4 — Confirm the delivery dispatched

The receiver’s 202 only means “accepted.” Check the delivery log to see whether it started a job:
curl "https://api.getdialed.ai/flows/webhooks/8a9c86a8b78c48d3a3e5e00ad0ed5b96/deliveries" \
  -H "X-API-Key: $API_KEY"
const res = await fetch(
  "https://api.getdialed.ai/flows/webhooks/8a9c86a8b78c48d3a3e5e00ad0ed5b96/deliveries",
  { headers: { "X-API-Key": process.env.API_KEY } },
);
const { items } = await res.json();
import os
import httpx

res = httpx.get(
    "https://api.getdialed.ai/flows/webhooks/8a9c86a8b78c48d3a3e5e00ad0ed5b96/deliveries",
    headers={"X-API-Key": os.environ["API_KEY"]},
)
deliveries = res.json()["items"]
Each delivery record moves receiveddispatched (with the created job_id) or failed (with an error).

Next steps

Webhooks

The full object reference, signing spec, and receiver semantics.

Jobs and executions

Every verified delivery starts a job — track it end to end.