Skip to content
Browse documentation
On this page

Webhooks

Get a signed HTTPS POST when calls start and end, texts arrive, contacts change or AI recaps are ready. Event types, payloads, signature checks and retries.

Webhooks send an HTTPS POST to your endpoint when something happens in Dialstitch, so your systems don't have to poll the API.

Add an endpoint

  1. Open webhooks

    Go to Admin, then API & Webhooks, and open the Webhooks tab.

  2. Enter the URL

    Under Add endpoint, enter your URL. It must be https on the standard port, on a public host, with no username or password in it.

  3. Pick events

    Tick the events you want. Add a Description if it helps you remember what the endpoint is for.

  4. Save the secret

    Select Add endpoint and copy the signing secret that starts with whsec_. It's shown once.

  5. Send a test

    Select Send test event. Dialstitch posts a signed test.ping event right away and logs the result.

You can also manage endpoints with the API, using the webhooks:manage scope. See Manage webhooks with the API.

Events

  • call.started: a call starts. An inbound call reaches a number, or an outbound call connects to the carrier.
  • call.ended: any call ends, answered or not.
  • call.missed: an inbound call ends without being answered.
  • call.recapped: a call's AI transcript and summary are ready.
  • voicemail.received: a voicemail is saved.
  • sms.received: an inbound text arrives.
  • sms.sent: an outbound text is handed to the carrier.
  • contact.created: a contact is created.
  • contact.updated: a contact is edited.
  • disposition.logged: a disposition is logged on a call.
  • moment.detected: an AI Moment is found in a call.

Emergency (911) calls never send webhooks.

The delivery

Each delivery is a POST with a JSON envelope:

{
  "id": "5b0c9a2e-6f1d-4b7a-9c3e-2d8f1a0b4c6e",
  "type": "call.ended",
  "created_at": "2026-11-02T15:09:41.000Z",
  "data": {
    "event": "call.ended",
    "occurred_at": "2026-11-02T15:09:41.000Z",
    "call": {
      "id": "3f1c2a9e-8d4b-4c1e-9a7f-2b6d5e8c1a40",
      "direction": "inbound",
      "from": "+14155550123",
      "to": "+16045550188",
      "remote_number": "+14155550123",
      "phone_number_id": "b8e2f7c4-1d3a-4e5b-8c9d-0a1b2c3d4e5f",
      "started_at": "2026-11-02T15:04:05.000Z",
      "duration_seconds": 328
    },
    "contact": { "id": "7a9b0c1d-...", "name": "Dana Ruiz", "company": "Ruiz Plumbing" },
    "user": { "id": 42, "name": "Sam Lee", "email": "sam@example.com" }
  }
}

id is the delivery id. It stays the same on every retry, so use it to drop duplicates. What's in data depends on the event:

  • Call events and voicemail.received: call, plus the matching contact and the user who handled it (either can be null). call.recapped adds summary.
  • sms.received and sms.sent: message with id, from, to, body, phone_number_id and conversation_id, plus contact and user.
  • contact.created and contact.updated: contact with id, name, company, notes and phones, plus user.
  • disposition.logged: call, disposition (id, name), sub_disposition, source, contact and user.
  • moment.detected: call, moment (id, name, snippet), contact and user.

Headers

  • X-Dialstitch-Event: the event type, such as call.ended.
  • X-Dialstitch-Delivery: the delivery id, the same as id in the body.
  • X-Dialstitch-Timestamp: when this attempt was signed, in Unix seconds.
  • X-Dialstitch-Signature: t=<unix seconds>,v1=<signature>.
  • User-Agent: Dialstitch-Webhooks/1.0.

Verify the signature

The signature is a hex HMAC-SHA256 of <t>.<raw body>, keyed with your endpoint's secret. Check it against the raw request body before you parse the JSON, and reject timestamps more than 5 minutes off.

import crypto from "node:crypto";

export function verifyDialstitch(rawBody, header, secret) {
  const parts = Object.fromEntries(
    (header ?? "").split(",").map((p) => p.trim().split("=", 2)),
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: keep the raw body for the check
app.post("/dialstitch", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyDialstitch(raw, req.get("X-Dialstitch-Signature"), process.env.DIALSTITCH_WEBHOOK_SECRET)) {
    return res.status(400).send("bad signature");
  }
  const event = JSON.parse(raw);
  // handle event.type ...
  res.sendStatus(200);
});

The same check in Python:

import hashlib, hmac, time

def verify_dialstitch(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > 300 or "v1" not in parts:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Retries

  • Answer with any 2xx within 10 seconds to acknowledge a delivery. Nothing in your response body is read.
  • Any other status, a timeout or a redirect counts as a failure. Redirects aren't followed, so use the final URL.
  • A failed delivery is retried with backoff of 2, 4, 8, 16 and 32 minutes: 6 attempts in all.
  • After 5 deliveries in a row use up every retry, the endpoint is turned off. Turn it back on from the Webhooks tab.
  • Deliveries can arrive more than once and out of order. Use id to drop duplicates and occurred_at to order them.

Each endpoint on the Webhooks tab shows its latest deliveries, with the status, attempts and response code. Turn off pauses deliveries. Remove deletes the endpoint.

Manage webhooks with the API

These endpoints need the webhooks:manage scope.

  • GET /webhooks: list your webhooks.
  • POST /webhooks: add one. Send url, events (a list of event names) and an optional description. The response includes the secret, once.
  • GET /webhooks/{id}: one webhook and its 25 latest deliveries.
  • DELETE /webhooks/{id}: remove it. Returns 204.
curl -X POST https://dialstitch.com/api/v1/webhooks \
  -H "Authorization: Bearer dpk_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/dialstitch", "events": ["call.ended", "sms.received"]}'
{
  "data": {
    "id": "0d1e2f3a-4b5c-4d6e-8f7a-9b0c1d2e3f4a",
    "url": "https://example.com/dialstitch",
    "description": null,
    "events": ["call.ended", "sms.received"],
    "enabled": true,
    "disabled_reason": null,
    "created_at": "2026-11-02T17:00:00.000Z",
    "secret": "whsec_..."
  }
}