LeadlyIVR API
Build integrations on top of your LeadlyIVR workspace: read the call log (including AI summaries and transcripts), watch agent presence, place click-to-call calls, transfer live calls, and receive real-time webhooks for every call event — from your CRM, helpdesk, or any external application.
Import the OpenAPI spec into Postman / Insomnia, or generate a typed client with your favourite generator.
Quickstart
- Log in to your dashboard → API Keys → create a key. The plaintext (
lk_live_…) is shown once — store it securely. - Choose scopes for the key (e.g.
calls:readonly, for a reporting integration). - Call the API:
curl https://api.leadlyivr.com/v1/calls \ -H "Authorization: Bearer lk_live_XXXXXXXXXXXX"
Authentication
Every request needs your API key, in either header:
Authorization: Bearer lk_live_XXXXXXXXXXXX # or X-API-Key: lk_live_XXXXXXXXXXXX
Keys are scoped to your workspace — every response is automatically limited to your own data. Scopes are granular (calls:read, calls:write, agents:read, numbers:read, flows:read, analytics:read) with coarse read / write aliases. Keys can be revoked any time from the dashboard; revocation is immediate.
Endpoints
Base URL: https://api.leadlyivr.com/v1
| Endpoint | Scope | Description |
|---|---|---|
| GET /calls | calls:read | Call log, newest first. ?limit (≤200), ?status filter. Includes AI summary/transcript on AI-handled calls. |
| GET /calls/:callUuid | calls:read | One call by its call UUID. |
| GET /analytics/summary | analytics:read | Totals (answered, missed, voicemail, monthly minutes…) + 7-day daily series. |
| GET /agents | agents:read | Agents with live softphone presence (online: true/false). |
| GET /numbers | numbers:read | Your virtual numbers and the flow answering each. |
| GET /flows | flows:read | Your IVR flows. |
| POST /calls | calls:write | Click-to-call: rings the agent, then bridges to the destination. Body: { to, agentId } (agentId required). Returns 202 { requestUuid } — track via call.started / call.completed webhooks. |
| POST /calls/:callUuid/transfer | calls:write | Transfer a live call to another number. Body: { to }. |
| GET /ai-assistants | flows:read | AI assistants available for AI calls: departments with instructions + flow AI nodes. Returns [{ id, name, kind: 'department'|'node', flow }]. |
| POST /dialer-token | calls:write | Mint an embed token for the Dialer widget. Body: { agentId, ttlMinutes? (default 720, max 1440) }. Returns { token, embedUrl }. |
Examples
curl -X POST https://api.leadlyivr.com/v1/calls \
-H "Authorization: Bearer lk_live_XXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{ "to": "919812345678", "agentId": "<agent-uuid from GET /agents>" }'
# → 202 Accepted
# { "requestUuid": "f3a1c9e2-…", "message": "call fired" }
# The agent's phone rings first, then the customer is bridged in.
# Subscribe to call.started / call.completed webhooks to track progress;
# requestUuid matches call_uuid in those events and in GET /calls.# 1. Once: list the assistants and remember the one you want.
curl https://api.leadlyivr.com/v1/ai-assistants -H "Authorization: Bearer lk_live_XXXXXXXXXXXX"
# → [{ "id": "…", "name": "Sales", "kind": "department", "flow": null }]
# 2. On every new lead, fire one call. Nobody has to click anything.
curl -X POST https://api.leadlyivr.com/v1/calls \
-H "Authorization: Bearer lk_live_XXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"to": "919812345678",
"mode": "ai",
"departmentId": "<id from step 1>",
"greeting": "Hi Ravi! Thank you for filling the form on our website — I am calling from Acme about your enquiry.",
"context": "Lead source: website form. Interested in: balcony pigeon net."
}'
# → 202 { "requestUuid": "…", "mode": "ai", "assistant": "Sales" }
# We ring the customer directly — no agent is involved. When they answer, the
# assistant opens with your greeting (never "thank you for calling"), and
# `context` tells it why it called. The transcript, recording and a written
# summary arrive on the call.completed webhook (is_ai: true).
#
# Use "aiNodeId" instead of "departmentId" for a flow AI node.
# Both greeting and context are optional; without a greeting the assistant
# introduces itself with your business name.
# 402 = AI minutes exhausted (top up); 409 = that department has no AI
# instructions yet.{
"call_uuid": "f3a1c9e2-…",
"direction": "inbound",
"from_number": "919008413315",
"to_number": "918065951644",
"status": "completed",
"digits_path": "1",
"duration_seconds": 73,
"recording_url": "https://…/recording.mp3",
"started_at": "2026-07-05T08:45:09Z",
"ended_at": "2026-07-05T08:46:22Z",
"agent_name": "Priya",
"department_name": "Sales",
"is_ai": false,
"ai_summary": null,
"ai_transcript": null
}const res = await fetch('https://api.leadlyivr.com/v1/calls?limit=50', {
headers: { Authorization: `Bearer ${process.env.LEADLY_API_KEY}` },
});
const calls = await res.json();
for (const c of calls.filter((c) => c.is_ai && c.ai_summary)) {
await crm.createNote({ phone: c.from_number, note: c.ai_summary });
}Embed the Dialer in your app
Put the full LeadlyIVR softphone inside your CRM as an iframe widget — your agents answer and place calls without ever opening LeadlyIVR. Your backend mints a short-lived token for the agent, and the widget handles registration, availability, incoming calls, dialing and transfers. The token only unlocks the phone — it cannot read the rest of the account.
curl -X POST https://api.leadlyivr.com/v1/dialer-token \
-H "Authorization: Bearer lk_live_XXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{ "agentId": "<agent-uuid from GET /agents>" }'
# → { "token": "eyJ…", "agent": "Priya",
# "embedUrl": "https://leadlyivr.com/softphone/embed?token=eyJ…",
# "expiresInMinutes": 720 }<iframe
src="{embedUrl}&origin=https://your-crm.example.com"
allow="microphone"
style="width: 380px; height: 420px; border: 0; border-radius: 12px;"
></iframe>// Click a phone number in your CRM → dial it in the widget:
iframe.contentWindow.postMessage(
{ type: 'leadly:dial', to: '919812345678' }, 'https://leadlyivr.com');
// Also accepted: leadly:answer, leadly:reject, leadly:hangup
// Pop your contact card when a call comes in:
window.addEventListener('message', (ev) => {
if (ev.origin !== 'https://leadlyivr.com') return;
if (ev.data.type === 'leadly:incoming') showContact(ev.data.from);
// Other events: leadly:ready, leadly:registered, leadly:answered,
// leadly:ended, leadly:error
});
// The bridge only activates when the &origin= param matches your page's origin.Webhooks (real-time events)
Instead of polling, subscribe to events: dashboard → Webhooks → add your HTTPS endpoint. The signing secret is shown once. LeadlyIVR POSTs signed JSON on:
call.started— an inbound call hit your numbercall.answered— an agent picked upcall.completed— call ended (final duration, recording, AI summary when present)call.no_answer— nobody picked up (data.voicemail: truewhen a voicemail was left)recording.ready— the recording URL is availablecall.info_requested— the caller asked the AI to be sent the brochure / location (data.requested=["brochure","location"], plusfrom_numberandcontact_name)whatsapp.reply— a customer replied to an auto-sent WhatsApp message (data.from_number,data.text) — call them backwebhook.test— sent by the dashboard's "Send test" button
POST https://your-app.example.com/leadly-webhook
X-LeadlyIVR-Event: call.completed
X-LeadlyIVR-Signature: t=1751712000,v1=6f3a…
{
"id": "evt_9f2c…",
"event": "call.completed",
"created_at": "2026-07-05T08:46:23Z",
"tenant_id": "…",
"data": {
"call_uuid": "…",
"from_number": "919008413315", // also sent as "from" (legacy alias)
"to_number": "918065951644", // also sent as "to" (legacy alias)
"direction": "inbound",
"status": "completed",
"agent_name": "Priya", // null when no agent handled it
"department_name": "Sales", // null when the call never hit a department
"digits_path": "1",
"duration_seconds": 73,
"recording_url": null,
"is_ai": false,
"ai_summary": null, // set on AI-handled calls (call.completed)
"started_at": "2026-07-23T08:45:09.000Z",
"ended_at": "2026-07-23T08:46:22.000Z"
}
}Verify the signature on every delivery — HMAC-SHA256 of `${t}.${rawBody}` with your endpoint's secret. Reject if t is older than ~5 minutes:
const crypto = require('crypto');
function verify(rawBody, sigHeader, secret) {
const { t, v1 } = Object.fromEntries(sigHeader.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // stale
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
// IMPORTANT: verify against the RAW request body, before any JSON parsing.Respond with any 2xx to acknowledge. Delivery is at-least-once: failures retry with backoff (1m / 5m / 30m / 2h / 6h, 5 attempts), and every attempt is visible in the dashboard's Deliveries view — so make your handler idempotent (dedupe on id).
Errors & rate limits
| Status | Meaning |
|---|---|
400 | Invalid request (e.g. missing to / agentId) |
401 | Missing, invalid, or revoked API key |
402 | Plan limit reached or insufficient credits. AI calls return { code: "ai_credit" } — the workspace is out of AI minutes. Mark the lead for follow-up and stop retrying; we also alert the workspace owner on Telegram so they can top up. |
409 | Not usable yet — e.g. the chosen department has no AI instructions, or the agent has no phone/Dialer. The message says what to fix. |
403 | Key lacks the required scope |
404 | Resource not found (or belongs to another workspace) |
429 | Rate limit exceeded — default 120 requests / 60 s per key. Back off and retry. |
Errors are JSON: { "error": "message" }.
We're happy to help with sandbox access, extra scopes, or webhook events you're missing.
Talk to us