adaptlive
← API Reference

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

  1. Extract t (timestamp) and v1 (signature) from the header.
  2. Construct the signed payload: ${t}.${rawBody}
  3. Compute HMAC-SHA256 with your signing secret.
  4. Compare using constant-time comparison.
  5. Reject if t is 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.

AttemptDelay
1 (initial)Immediate
21 minute
35 minutes
415 minutes
51 hour
66 hours
712 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.ended

A live call ended. Payload includes structured summary, transcript availability, draft fields, and captured facts. Most popular trigger.

sms.received

An inbound SMS landed in a thread. Fires once per message.

sms.sent

An outbound SMS was sent (manually or via automation).

voicemail.received

A caller left a voicemail. Includes transcript and recording URL.

call.missed

A call rang and went unanswered (no voicemail). Useful for callback workflows.

person.created

A new Person was created (via call, manual entry, or API). Fires once per person.

work_record.created

A new WorkRecord (job/appointment/case/ticket) was created.

work_record.updated

An existing WorkRecord changed status, assignment, or tracked fields.

fact.confirmed

A PROPOSED fact was confirmed by AI threshold or human action.

fact.promoted

A WorkRecord-scope fact was promoted to a Person/Company as a persistent attribute.

draft.approved

Post-call draft approved by human, ready to sync to external systems.

signal.fired

A signal (complaint, upsell_opportunity, etc.) fired with high confidence.

workflow.finished

A workflow run completed. Includes captured fields and workflow definition key.

appointment.scheduled

A new appointment was scheduled during a call or via the API.

task.created

A new task/follow-up was captured.

note.added

A 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.
Error Handling →Zapier Integration →

We use essential cookies to keep the app secure. Optional cookies help us improve reliability and measure campaigns. Cookie policy