Webhooks
Webhooks are available from the Pro plan upward. Programmatic recovery and canonical re-fetch require Business.
Create endpoints in Agente → Integrazioni → Webhooks in the Talki dashboard. Each endpoint must be a public HTTPS URL. The signing secret is shown once; keep it in the same server-side secret store as an API key.
Envelope
Section titled “Envelope”{ "id": "evt_04d8e87897db4ecab459a456ea55e754", "type": "call.ended", "created_at": "2026-09-15T09:18:03+00:00", "api_version": "2026-06-01", "account_id": "f7e39b9a-1ac8-45e4-93da-2600f17cb286", "origin": { "source": "voice", "credential_id": null, "idempotency_key": null }, "data": { "object": { "id": "38d23d84-02d2-4418-b169-3608aa04a8f8", "caller": { "contact_id": "8fbb25f2-4f3b-4496-8aea-f0fc66de512d", "phone_number": "+393331234567", "display_name": "Giulia Bianchi" }, "started_at": "2026-09-15T09:14:31Z", "answered_at": "2026-09-15T09:14:34Z", "ended_at": "2026-09-15T09:18:02Z", "duration_seconds": 208, "summary": "Giulia asked to move her haircut to Friday morning.", "appointment_ids": [], "created_at": "2026-09-15T09:18:03Z" } }}Use id as the delivery deduplication key and account_id as the tenant key.
origin reports whether the change came from the voice assistant, the dashboard,
or—in a future write API—a credential. It can be null on older redeliveries.
For resource events, data.object is the canonical public object. Event-only
notifications such as ping and usage.threshold_reached do not have an
object.
Event catalogue
Section titled “Event catalogue”| Event | When it is emitted | data.object |
|---|---|---|
call.ended |
A call ends and its summary is ready | Call |
call.transferred |
The assistant transfers a call to a person | Call, plus target_label and transferred_at |
appointment.created |
A new Talki appointment is committed | Appointment |
appointment.updated |
An appointment is edited, moved, or restored | Appointment |
appointment.cancelled |
An appointment is cancelled | Appointment with status: "cancelled" |
contact.created |
A curated contact is added | Contact |
contact.updated |
A curated contact changes | Contact |
usage.threshold_reached |
The account reaches 75% of included minutes | None; usage counters are in data |
ping |
The account owner chooses Invia test | None; data.message confirms the test |
An AI-inferred contact name does not emit a contact event until a person approves
it. A voice reschedule cancels the old appointment and creates a new ID, so it
produces appointment.cancelled followed by appointment.created.
Delivery headers
Section titled “Delivery headers”Content-Type: application/jsonUser-Agent: Hubtec-Webhooks/1.0X-Hubtec-Signature: t=1789463883,v1=4a3d…X-Hubtec-Event: call.endedX-Hubtec-Delivery: evt_04d8e87897db4ecab459a456ea55e754Header names are case-insensitive. X-Hubtec-Delivery equals the envelope id.
Delivery semantics
Section titled “Delivery semantics”- Delivery is at least once. Persist the event ID before applying side effects.
- Any
2xxacknowledges the event. Redirects and every non-2xx response fail. - Network failures and non-2xx responses are retried after 10 seconds, 1 minute, 10 minutes, 1 hour, and 6 hours—six attempts including the first.
- Events can arrive out of order. Use object timestamps and make updates idempotent; never infer sequence from arrival order.
- A
410 Goneresponse disables the endpoint immediately. - Twenty consecutive terminally failed deliveries disable the endpoint. One success resets the failure counter.
- Delivery attempts time out after 10 seconds. Acknowledge quickly and move expensive work to your own queue.
- Delivery logs and stored payloads are retained for 30 days.
Signature verification
Section titled “Signature verification”Talki signs the exact bytes sent in the request body. Parse
X-Hubtec-Signature, then compute HMAC-SHA256 over:
<timestamp>.<raw request body>Use the complete whsec_… secret, compare digests in constant time, and reject
timestamps older than five minutes. Verify before parsing JSON or queuing the
event.
Node.js
Section titled “Node.js”import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyTalkiWebhook(rawBody, signatureHeader, secret) { const parts = Object.fromEntries( signatureHeader.split(',').map((part) => part.split('=', 2)), ) const timestamp = Number(parts.t) if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) { throw new Error('stale webhook') }
const expected = createHmac('sha256', secret) .update(`${parts.t}.`) .update(rawBody) .digest() const received = Buffer.from(parts.v1 ?? '', 'hex') if (received.length !== expected.length || !timingSafeEqual(received, expected)) { throw new Error('invalid webhook signature') }
return JSON.parse(rawBody.toString('utf8'))}Pass a Buffer captured before your framework’s JSON middleware modifies the
body.
Python
Section titled “Python”import hashlibimport hmacimport jsonimport time
def verify_talki_webhook(raw_body: bytes, signature_header: str, secret: str): parts = dict(part.split("=", 1) for part in signature_header.split(",")) timestamp = int(parts["t"]) if abs(time.time() - timestamp) > 300: raise ValueError("stale webhook")
signed = parts["t"].encode() + b"." + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() if not hmac.compare_digest(parts.get("v1", ""), expected): raise ValueError("invalid webhook signature")
return json.loads(raw_body)Recovery
Section titled “Recovery”For missed call events, re-query GET /v1/calls with created_after, a one-minute
safety overlap, and cursor pagination. Deduplicate on call ID. See Core
concepts.
Appointment and contact REST recovery endpoints are not released yet. Inspect the 30-day delivery log and redeliver failures from the dashboard. If the gap is older than that, contact Talki support before attempting a manual reconciliation.
