Developer docs
Lydoh POSTs signed JSON events to your endpoint as calls happen, so your CRM, ticketing system, or automation tool stays in sync without polling. Configure the endpoint in Dashboard → Settings → Webhooks.
call.startedcall.endedrequest.createdescalation.startedDelivery
x-lydoh-event, x-lydoh-timestamp (unix seconds), x-lydoh-signature (sha256=<hex>).id in the body is stable across every retry: dedupe on it. See recent deliveries and redeliver manually from Settings → Webhooks.Verification
The signature is an HMAC-SHA256 of `${timestamp}.${rawBody}` with your signing secret (shown once when you save the webhook). Node example:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyLydohWebhook(req: { headers: Record<string, string>; rawBody: string }, secret: string): boolean {
const ts = req.headers["x-lydoh-timestamp"];
const sig = req.headers["x-lydoh-signature"]; // "sha256=<hex>"
if (!ts || !sig) return false;
// Reject anything older than 5 minutes (replay protection).
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = "sha256=" + createHmac("sha256", secret).update(ts + "." + req.rawBody).digest("hex");
return expected.length === sig.length && timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}Events
call.startedA call to your Lydoh number is beginning.
{
"id": "evt_9c2f…",
"event": "call.started",
"createdAt": "2026-07-12T02:14:07Z",
"tenantId": "561c…",
"data": {
"callId": "c1a2…",
"callerNumber": "+12015550142",
"toNumber": "+16575550110",
"startedAt": "2026-07-12T02:14:07Z"
}
}call.endedThe call finished. Includes the flat transcript, structured timestamped turns, recording URL, AI summary, and a disposition (resolved | needs_followup | unresolved), so you can file the whole record, and decide which calls need a human, without polling.
{
"event": "call.ended",
"data": {
"callId": "c1a2…",
"callerNumber": "+12015550142",
"startedAt": "…", "endedAt": "…",
"durationSeconds": 184,
"endedReason": "customer-ended-call",
"afterHours": true,
"disposition": "needs_followup",
"summary": "Caller requested an appointment for…",
"transcript": "AI: Thank you for calling…\nCaller: Hi, I'd like to…",
"recordingUrl": "https://…/recording.mp3",
"turns": [
{ "role": "assistant", "text": "Thank you for calling…", "at": 0.4 },
{ "role": "user", "text": "Hi, I'd like to…", "at": 5.2 }
]
}
}request.createdA structured request was captured. kind is "appointment", "refill" (medical), or "message" (non-practice tenants).
{
"event": "request.created",
"data": {
"requestId": "7f3b…",
"kind": "refill",
"urgency": "routine",
"callerName": "Pat Doe",
"callerPhone": "+12015550142",
"medication": "lisinopril 10mg",
"pharmacy": "CVS Cedar Grove",
"notes": "Medication: lisinopril 10mg · Pharmacy: CVS…",
"afterHours": false,
"callId": "c1a2…"
}
}escalation.startedAn urgent after-hours call entered the on-call cascade (medical practices).
{
"event": "escalation.started",
"data": {
"escalationId": "e88a…",
"callerName": "Pat Doe",
"callerNumber": "+12015550142",
"reason": "post-op bleeding",
"callId": "c1a2…"
}
}A pingevent with the same envelope is sent by the "Send test event" button in Settings.
Signed webhooks deliver call starts, full transcripts and summaries, appointment and refill requests, and urgent escalations to any endpoint.