Webhook Connectors
Receive real-time events via HTTP POST to your endpoint. The simplest way to get adaptlive data into external systems—no polling, no batch delays.
How It Works
- Create a webhook subscription via API or in the developer portal at /portal/webhooks
- Choose which event types to receive (or all events)
- Provide your HTTPS endpoint URL
- Store the signing secret securely
- Events are POSTed to your URL as they occur
Quick Setup
Via API
POST /v1/webhooks
Authorization: Bearer ak_live_...
Content-Type: application/json
{
"name": "My Integration",
"url": "https://your-app.com/webhooks/adaptlive",
"eventTypes": ["call.ended", "sms.received"]
}
// Response includes your signing secret (shown once!)
{
"data": {
"id": "wh_sub_xyz",
"secret": "whsec_XXXXXX..."
}
}Via the developer portal
- Go to /portal/webhooks
- Click "Add Webhook"
- Enter a name and your HTTPS endpoint URL
- Select event types (or leave empty for all)
- Copy and securely store the signing secret
Endpoint Requirements
- HTTPS only: We do not deliver to HTTP endpoints.
- Respond with 2xx: Any 2xx status within 10 seconds counts as success.
- Accept POST: All deliveries are HTTP POST with JSON body.
- Idempotent handling: You may receive the same event more than once.
Building Your Handler
Node.js / Express
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
// IMPORTANT: Use raw body for signature verification
app.post(
"/webhooks/adaptlive",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.headers["x-adaptlive-signature"];
const rawBody = req.body.toString();
if (!verifySignature(signature, rawBody, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(rawBody);
const eventType = req.headers["x-adaptlive-event"];
const eventId = req.headers["x-adaptlive-event-id"];
// Dedupe by eventId
if (await alreadyProcessed(eventId)) {
return res.status(200).send("Already processed");
}
// Handle the event asynchronously
processEvent(eventType, event).catch(console.error);
// Respond quickly
res.status(200).send("OK");
}
);
function verifySignature(header, body, secret) {
if (!header || !secret) return false;
const parts = String(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);
if (!Number.isFinite(t) || !/^[0-9a-f]{64}$/i.test(v1)) {
return false;
}
// Check timestamp freshness
const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - t);
if (ageSeconds > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${body}`)
.digest("hex");
const expectedBytes = Buffer.from(expected, "hex");
const actualBytes = Buffer.from(v1, "hex");
return (
expectedBytes.length === actualBytes.length &&
timingSafeEqual(expectedBytes, actualBytes)
);
}Next.js API Route
// app/api/webhooks/adaptlive/route.ts
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(req: Request) {
const signature = req.headers.get("x-adaptlive-signature");
const rawBody = await req.text();
if (!signature || !verifySignature(signature, rawBody)) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(rawBody);
const eventType = req.headers.get("x-adaptlive-event");
// Handle based on event type
switch (eventType) {
case "call.ended":
await handleCallEnded(event.data);
break;
case "sms.received":
await handleSmsReceived(event.data);
break;
// ... other event types
}
return new Response("OK", { status: 200 });
}
function verifySignature(header: string, body: string): boolean {
const secret = process.env.ADAPTLIVE_WEBHOOK_SECRET!;
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 (!secret || !tEntry || !v1Entry) return false;
const t = Number(tEntry.slice(2));
const v1 = v1Entry.slice(3);
if (!Number.isFinite(t) || !/^[0-9a-f]{64}$/i.test(v1)) {
return false;
}
const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - t);
if (ageSeconds > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${body}`)
.digest("hex");
const expectedBytes = Buffer.from(expected, "hex");
const actualBytes = Buffer.from(v1, "hex");
return (
expectedBytes.length === actualBytes.length &&
timingSafeEqual(expectedBytes, actualBytes)
);
}Common Patterns
CRM Sync
On call.ended, look up the contact in your CRM by phone number, create if missing, then log the call as an activity with the summary and recording link.
Slack Notifications
Forward events to Slack's Incoming Webhooks. Format messages with caller info, summary, and deep links back to adaptlive.
Data Lake Ingestion
Write events to S3/GCS as JSON files, partitioned by date. Load into your data warehouse on a schedule for analytics.
Custom Workflows
Trigger internal workflows based on events. Missed calls → create callback tasks, voicemails → queue for transcription review.
Testing Webhooks
Use tools like webhook.site or ngrok to test during development:
- Create a temporary public URL with webhook.site or ngrok
- Register that URL as a webhook subscription
- Trigger events in adaptlive (make test calls, send messages)
- Inspect the payloads in your testing tool
- Update to your production URL when ready
