Developers

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.

Get an account →OpenAPI 3.1 spec (JSON)

Import the OpenAPI spec into Postman / Insomnia, or generate a typed client with your favourite generator.

Quickstart

  1. Log in to your dashboard → API Keys → create a key. The plaintext (lk_live_…) is shown once — store it securely.
  2. Choose scopes for the key (e.g. calls:read only, for a reporting integration).
  3. Call the API:
curl
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

EndpointScopeDescription
GET /callscalls:readCall log, newest first. ?limit (≤200), ?status filter. Includes AI summary/transcript on AI-handled calls.
GET /calls/:callUuidcalls:readOne call by its call UUID.
GET /analytics/summaryanalytics:readTotals (answered, missed, voicemail, monthly minutes…) + 7-day daily series.
GET /agentsagents:readAgents with live softphone presence (online: true/false).
GET /numbersnumbers:readYour virtual numbers and the flow answering each.
GET /flowsflows:readYour IVR flows.
POST /callscalls:writeClick-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/transfercalls:writeTransfer a live call to another number. Body: { to }.
GET /ai-assistantsflows:readAI assistants available for AI calls: departments with instructions + flow AI nodes. Returns [{ id, name, kind: 'department'|'node', flow }].
POST /dialer-tokencalls:writeMint an embed token for the Dialer widget. Body: { agentId, ttlMinutes? (default 720, max 1440) }. Returns { token, embedUrl }.

Examples

Place a call from your CRM (click-to-call)
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.
Let the AI make the call (speed-to-lead: call a new lead automatically)
# 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.
A call object (GET /calls response item)
{
  "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
}
Node.js — poll for AI-handled calls and push to your CRM
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.

1. Your backend mints a token (keep your API key server-side)
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 }
2. Your frontend embeds it (allow="microphone" is required)
<iframe
  src="{embedUrl}&origin=https://your-crm.example.com"
  allow="microphone"
  style="width: 380px; height: 420px; border: 0; border-radius: 12px;"
></iframe>
3. Optional: talk to the widget (postMessage bridge)
// 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 number
  • call.answered — an agent picked up
  • call.completed — call ended (final duration, recording, AI summary when present)
  • call.no_answer — nobody picked up (data.voicemail: true when a voicemail was left)
  • recording.ready — the recording URL is available
  • call.info_requested — the caller asked the AI to be sent the brochure / location (data.requested = ["brochure","location"], plus from_number and contact_name)
  • whatsapp.reply — a customer replied to an auto-sent WhatsApp message (data.from_number, data.text) — call them back
  • webhook.test — sent by the dashboard's "Send test" button
Event envelope
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:

Node.js verification
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

StatusMeaning
400Invalid request (e.g. missing to / agentId)
401Missing, invalid, or revoked API key
402Plan 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.
409Not usable yet — e.g. the chosen department has no AI instructions, or the agent has no phone/Dialer. The message says what to fix.
403Key lacks the required scope
404Resource not found (or belongs to another workspace)
429Rate limit exceeded — default 120 requests / 60 s per key. Back off and retry.

Errors are JSON: { "error": "message" }.

Building an integration?

We're happy to help with sandbox access, extra scopes, or webhook events you're missing.

Talk to us