Talkybara

Developer Reference

Webhook Documentation

Talkybara can send real-time HTTP POST requests to a URL you configure whenever certain events happen — a new contact is captured, a call ends, an appointment is booked. This page covers the exact request shape, how to verify it genuinely came from Talkybara, and how delivery retries work. Configure your endpoint and choose which events to send from your dashboard's Settings → Automations tab (Scale plan).

Quickstart

  1. 1Add your HTTPS endpoint URL and choose which events to send from Settings → Automations (Scale plan).
  2. 2Verify the X-Talkybara-Signature header on every request using your signing secret — see Verifying Signatures.
  3. 3Respond with a 2xx status within 8 seconds. Anything else is retried automatically — see Retries & Backoff.

Overview

Every webhook request is:

  • An HTTP POST to the URL you configured, with header Content-Type: application/json.
  • Signed with a header, X-Talkybara-Signature — an HMAC-SHA256 hex digest of the raw request body, keyed with your webhook signing secret. See Verifying Signatures below.
  • A JSON body shaped { event, timestamp, data }event is one of the seven event type strings below, timestamp is an ISO 8601 UTC timestamp of when the event fired, and data is an event-specific object (see next section).

Your endpoint should respond with a 2xx status within 8 seconds. Anything else — a non-2xx status, a timeout, a connection error — is treated as a failed delivery and queued for retry (see Retries & Backoff).

Event Types & Payloads

Seven event types, toggled individually from the Automations tab:

EventFires when
contact.createdThe AI captures a genuinely new contact's information (a phone number this tenant has never seen before).
call.completedAny phone call with the AI ends — fires for every call, regardless of outcome.
callback.requestedA caller explicitly asks for a callback. Fires in addition to call.completed for that same call, not instead of it.
appointment.bookedA new appointment is successfully booked — either by the AI during a call, or manually from the dashboard.
appointment.rescheduledAn existing appointment is moved to a new time (dashboard only).
appointment.canceledAn appointment is canceled (dashboard only).
appointment.no_showAn appointment is marked as a no-show (dashboard only).

contact.created

name/email/address are only populated if the caller actually stated them on this specific call — any of them can be null.

{
  "event": "contact.created",
  "timestamp": "2026-08-07T21:30:37.192Z",
  "data": {
    "phone": "+15551234567",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "address": null
  }
}

call.completed / callback.requested

Both use the identical data shape (the call itself) — outcome is one of booked, transferred, callback_requested, abandoned, or completed; direction is inbound or outbound.

{
  "event": "call.completed",
  "timestamp": "2026-08-07T21:30:37.192Z",
  "data": {
    "id": "8ea7c53e-4d05-43ac-934a-8d39eb3e0f52",
    "outcome": "booked",
    "direction": "inbound",
    "from_number": "+15551234567",
    "to_number": "+15559876543",
    "started_at": "2026-08-07T21:29:10.954925+00:00",
    "ended_at": "2026-08-07T21:30:24.508160+00:00",
    "duration_secs": 74
  }
}

appointment.booked / .rescheduled / .canceled / .no_show

status is one of confirmed, canceled, or no_show. google_event_id/notes can be null.

{
  "event": "appointment.booked",
  "timestamp": "2026-08-07T21:30:37.192Z",
  "data": {
    "id": "42983446-4de3-456e-b42a-bc30cfb2df3d",
    "tenant_id": "3107bd43-01e6-42f8-8b8f-375dd27091bd",
    "start_time": "2026-08-07T22:00:00+00:00",
    "end_time": "2026-08-07T22:30:00+00:00",
    "google_event_id": "hshvpd6fcqsmd9s8aqqhrjq52k",
    "contact_phone": "+15551234567",
    "twilio_call_sid": "CA214de4b932f7183cb0801f960e6467c5",
    "service": "Guitar restring",
    "notes": null,
    "status": "confirmed",
    "created_at": "2026-08-07T21:29:52.465656+00:00"
  }
}

One real difference worth knowing: only appointment.booked can originate from the AI mid-call (as opposed to a manual dashboard booking) — when it does, the payload is a smaller subset, missing tenant_id, google_event_id, notes, and created_at. rescheduled/canceled/no_show are dashboard-only actions, so they always send the full shape above.

{
  "event": "appointment.booked",
  "timestamp": "2026-08-07T21:30:37.192Z",
  "data": {
    "id": "42983446-4de3-456e-b42a-bc30cfb2df3d",
    "start_time": "2026-08-07T22:00:00+00:00",
    "end_time": "2026-08-07T22:30:00+00:00",
    "contact_phone": "+15551234567",
    "twilio_call_sid": "CA214de4b932f7183cb0801f960e6467c5",
    "service": "Guitar restring",
    "status": "confirmed"
  }
}

Verifying Signatures

Every request carries an X-Talkybara-Signature header: a lowercase hex HMAC-SHA256 digest of the raw request body, computed with your webhook signing secret (Settings → Automations → Webhook Secret Key) as the HMAC key.

Hash the raw bytes, not a re-serialized copy. If your framework parses the JSON body before your handler sees it, re-stringifying that parsed object is not guaranteed to produce byte-identical output (key order, whitespace, number formatting can all differ) — verification will fail even for a genuine request. Capture the raw body before any JSON parsing happens.

const crypto = require("crypto");

function isValidTalkybaraSignature(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody) // the exact raw request body -- see note below
    .digest("hex");

  // Constant-time comparison -- a plain expected === signatureHeader
  // check leaks timing information an attacker could use to guess the
  // signature one byte at a time.
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signatureHeader, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Example: Express route handler
app.post(
  "/webhooks/talkybara",
  express.raw({ type: "application/json" }), // keep the body raw, don't let Express parse it first
  (req, res) => {
    const signature = req.headers["x-talkybara-signature"];
    const rawBody = req.body; // a Buffer, thanks to express.raw()

    if (!isValidTalkybaraSignature(rawBody, signature, process.env.TALKYBARA_WEBHOOK_SECRET)) {
      return res.status(401).send("Invalid signature");
    }

    const payload = JSON.parse(rawBody);
    // ... handle payload.event / payload.data
    res.status(200).send("ok");
  },
);

Regenerating your secret (Settings → Automations) immediately invalidates the old one for every future delivery — update your endpoint's stored secret at the same time, or verification will start failing.

Retries & Backoff

A delivery is retried automatically if your endpoint doesn't return a 2xx status within 8 seconds. Up to 6 attempts total (the initial attempt plus 5 retries), spaced out with increasing backoff:

AttemptDelay since previous failure
1 (initial)immediate
25 minutes
330 minutes
42 hours
512 hours
6 (final)24 hours

If attempt 6 still fails, the delivery is marked permanently failed — it stops retrying, but the attempt (with its response code and error) stays visible in your dashboard's delivery log indefinitely. A delivery is never silently dropped.

Each retry re-signs the payload with your current secret at the time of that attempt, not whatever secret was active when the event originally fired — if you regenerated your secret between a failure and its retry, the retry uses the new one.

Make your handler idempotent. A delivery is only retried when your endpoint doesn't return a 2xx — but if your endpoint finished processing the event and the success response itself didn't make it back in time, that still looks like a failure from here, and the same event gets resent. The payload has no delivery ID to dedupe against, so use a natural key instead — e.g. the call, contact, or appointment id from data, plus event — and make handling that pair a second time a no-op.

Troubleshooting

  • Signature never matches. Almost always caused by verifying against a re-serialized/parsed body instead of the raw bytes (see Verifying Signatures), or a stale secret after a regenerate.
  • Not receiving any deliveries. Confirm the relevant event type is toggled on and your endpoint URL is saved in Settings → Automations — an unconfigured or unsaved URL sends nothing, silently (not an error state, just nothing to send yet).
  • Endpoint must be HTTPS. Plain http:// URLs are rejected when saving — a signed payload over an unencrypted connection can be observed or altered in transit.
  • Seeing repeated retries for the same event. Your endpoint is either erroring (check response codes in the delivery log), or not responding within 8 seconds — respond quickly with a 2xx and do slow processing asynchronously afterward, rather than making Talkybara wait on it.
  • Check the delivery log. Settings → Automations shows the last 20 delivery attempts per tenant, including HTTP response codes and attempt counts, for exactly this kind of debugging.
  • Handling the same event twice. Expected behavior, not a bug — see Retries & Backoff for why, and how to make your handler idempotent against it.