Webhooks
Subscribe to real-time events. When something happens in adaptlive, we POST a signed JSON payload to your endpoint. At-least-once delivery with automatic retries.
Creating a Subscription
Create webhooks via the API or in the developer portal at /portal/webhooks. Each subscription gets a unique signing secret (shown once at creation).
POST /v1/webhooks
Authorization: Bearer ak_live_...
Content-Type: application/json
{
"name": "CRM Sync",
"url": "https://your-app.com/webhooks/adaptlive",
"eventTypes": ["call.ended", "work_record.created"]
}
// Response (201 Created)
{
"requestId": "req_abc123",
"data": {
"id": "wh_sub_xyz789",
"name": "CRM Sync",
"url": "https://your-app.com/webhooks/adaptlive",
"eventTypes": ["call.ended", "work_record.created"],
"status": "ACTIVE",
"secret": "whsec_XXXXXXXXXXXXXXXX", // Shown once!
"createdAt": "2024-03-25T10:00:00Z"
}
}Pass an empty eventTypes array to subscribe to all events.
Payload Format
Every webhook delivery includes these headers and envelope:
// Headers
Content-Type: application/json
X-AdaptLive-Signature: t=1717009200,v1=<hmac-sha256-hex>
X-AdaptLive-Event: call.ended
X-AdaptLive-Event-Id: 01HYZXXXXXXXXXXXXXXX
// Body
{
"eventId": "01HYZXXXXXXXXXXXXXXX",
"occurredAt": "2024-03-25T14:30:00.000Z",
"organizationId": "org_abc123",
"data": {
"callId": "call_xyz789",
"direction": "inbound",
"duration": 342,
"from": "+15125551234",
"to": "+15125559876",
"summary": "Customer called about AC repair appointment...",
"transcript": "...",
"recordingUrl": "https://...",
"facts": [...],
"workRecordId": "wr_job456"
}
}Signature Verification
Verify every webhook to ensure it came from adaptlive. The signature header format is t=timestamp,v1=signature.
Verification Steps
- Extract
t(timestamp) andv1(signature) from the header. - Construct the signed payload:
${t}.${rawBody} - Compute HMAC-SHA256 with your signing secret.
- Compare using constant-time comparison.
- Reject if
tis older than 5 minutes.
// Node.js verification example — defensive against malformed input
import { createHmac, timingSafeEqual } from "node:crypto";
function verifySignature(header, body, secret) {
// Parse 't=<unix>,v1=<hex>' without trusting the shape — an
// attacker can send anything, and a TypeError here would crash
// your webhook handler.
const parts = (header ?? "").split(",").map((s) => s.trim());
const tEntry = parts.find((p) => p.startsWith("t="));
const v1Entry = parts.find((p) => p.startsWith("v1="));
if (!tEntry || !v1Entry) return false;
const t = Number(tEntry.slice(2));
const v1 = v1Entry.slice(3);
// Number.isFinite catches NaN — without it, 'NaN > 300 === false'
// silently bypasses the replay-protection window.
if (!Number.isFinite(t)) return false;
// Check timestamp freshness (5 min tolerance)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - t) > 300) return false;
// Compute expected signature against the EXACT raw body bytes.
// Don't re-stringify JSON before hashing — key reordering or
// whitespace differences will produce a mismatch.
const expected = createHmac("sha256", secret)
.update(`${t}.${body}`)
.digest("hex");
// Constant-time comparison. Length-check first because
// timingSafeEqual throws on length mismatch.
const a = Buffer.from(expected, "hex");
const b = Buffer.from(v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}Retry Policy
We retry failed deliveries with exponential backoff. A delivery is considered successful if your endpoint returns 2xx within 10 seconds.
| Attempt | Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 6 hours |
| 7 | 12 hours |
| 8 (final) | 24 hours |
After 8 failed attempts, the delivery is marked as DEAD_LETTERED. You can manually replay dead-lettered deliveries from the webhook settings.
Event Types
call.endedA live call ended. Payload includes structured summary, transcript availability, draft fields, and captured facts. Most popular trigger.
sms.receivedAn inbound SMS landed in a thread. Fires once per message.
sms.sentAn outbound SMS was sent (manually or via automation).
voicemail.receivedA caller left a voicemail. Includes transcript and recording URL.
call.missedA call rang and went unanswered (no voicemail). Useful for callback workflows.
person.createdA new Person was created (via call, manual entry, or API). Fires once per person.
work_record.createdA new WorkRecord (job/appointment/case/ticket) was created.
work_record.updatedAn existing WorkRecord changed status, assignment, or tracked fields.
fact.confirmedA PROPOSED fact was confirmed by AI threshold or human action.
fact.promotedA WorkRecord-scope fact was promoted to a Person/Company as a persistent attribute.
draft.approvedPost-call draft approved by human, ready to sync to external systems.
signal.firedA signal (complaint, upsell_opportunity, etc.) fired with high confidence.
workflow.finishedA workflow run completed. Includes captured fields and workflow definition key.
appointment.scheduledA new appointment was scheduled during a call or via the API.
task.createdA new task/follow-up was captured.
note.addedA free-form note was added to a person, company, customer, or work record.
Best Practices
- Return 2xx quickly. Do heavy processing asynchronously after acknowledging receipt.
- Dedupe on eventId. At-least-once delivery means you may receive the same event more than once.
- Validate signatures. Always verify the HMAC before trusting the payload.
- Handle missing fields gracefully. We may add new fields to payloads over time.
- Use HTTPS endpoints. We only deliver to
https://URLs.
