lydoh
Sign inStart free trial

Developer docs

Webhooks

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.started

Delivery

How events arrive

  • POST to your HTTPS endpoint with a JSON body; respond with any 2xx within 5 seconds.
  • Headers: x-lydoh-event, x-lydoh-timestamp (unix seconds), x-lydoh-signature (sha256=<hex>).
  • Delivery is at-least-once: a non-2xx or timeout is retried with exponential backoff for ~24 hours. The id in the body is stable across every retry: dedupe on it. See recent deliveries and redeliver manually from Settings → Webhooks.
  • Payloads can contain patient contact details and call transcripts. Only point webhooks at systems covered by your compliance posture.

Verification

Verify every delivery

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

Event reference

call.started

A 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.ended

The 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.created

A 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.started

An 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.

Every call, pushed to your CRM.

Signed webhooks deliver call starts, full transcripts and summaries, appointment and refill requests, and urgent escalations to any endpoint.