Developer documentation
Build on LeadInbox
One REST API for every conversation your business has — WhatsApp, LinkedIn, Instagram, Telegram, Messenger, X and email — plus the contacts, pipeline and templates around them. Read a thread, send a reply, move a deal, or drop the whole inbox into your own product with a single iframe.
Start here
Getting started
LeadInbox is a unified multi-channel inbox with a lead CRM behind it. Every message that reaches your business — WhatsApp, LinkedIn, Instagram, Telegram, Messenger, X, Gmail, Outlook or any IMAP mailbox — lands in one threaded inbox, attached to a single contact record, with a visual pipeline, reply templates and optional AI assistance on top.
Everything the web app does, it does through the same public REST API documented here. There is no separate private surface: the inbox you use, the inbox you embed, and the inbox you script all speak to https://www.leadinbox.ai/api/v1.
1. Create a workspace
Sign up at leadinbox.ai/signup. Signup runs email → plan → profile, and the workspace you land in is your tenant: every contact, conversation, lead, template and API credential belongs to it, and nothing is shared between workspaces. Your pipeline is seeded with seven default stages — New Lead, Contacted, Qualified, Proposal Sent, Negotiation, Won and Lost — which you can rename, recolour, reorder or replace.
2. Connect your first channel
Go to Accounts and pick a provider. WhatsApp and Telegram connect by scanning a QR code (WhatsApp also supports a phone pairing code); Instagram, LinkedIn, Messenger and X connect with your credentials, walking you through a 2FA checkpoint if the provider asks for one; IMAP mailboxes connect with host, port and an app password. Gmail and Outlook use a hosted OAuth flow in a popup window. See Channels for the full breakdown.
Once a channel is connected, LeadInbox backfills recent chats and messages, then keeps the inbox live from provider webhooks. Each connected channel counts as one account against your plan.
3. Get your API key
Your workspace API key lives in the app under Settings → Developer, in the API Credentials panel. The same screen generates your embed snippet and a ready-made prompt you can paste into an AI coding assistant. Copy the key, store it in your server environment, and send it as the X-API-KEY header on every request.
curl -H "X-API-KEY: $LEADINBOX_API_KEY" \
"https://www.leadinbox.ai/api/v1/conversations?limit=5"A successful call returns JSON with a conversations array and a next_cursor. If you get a 401, the key is wrong or missing; if you get a 403 on an AI or account endpoint, your plan does not include that capability yet.
Security
Authentication
Every request to /api/v1 is authenticated — there are no public endpoints. External callers use one of two credentials, and they are checked in this order: the X-API-KEY header first, then an Authorization: Bearer token. (The web app itself uses a third path, the Supabase session cookie, which is not something you can use from outside the browser.)
API key — X-API-KEY
This is the credential for server-to-server work: scripts, back-office jobs, automations, data syncs. The key is per-workspace, not per-user. Calls made with it act as the workspace administrator and can read and write every record in that workspace, so treat it like a database password: keep it on your server, never in browser JavaScript, a mobile binary or a public repository, and rotate it if it leaks by contacting support.
GET /api/v1/contacts?limit=25 HTTP/1.1
Host: www.leadinbox.ai
X-API-KEY: your-workspace-api-key
Accept: application/jsonKeys shorter than 20 characters are ignored outright, so a truncated copy-paste presents as an unauthenticated request rather than an invalid key.
Embed token — Authorization: Bearer
The Bearer token is a signed JWT carrying a workspace id and a user id. It is what the embedded inbox uses, and the whole /api/v1 surface accepts it, so it also works as a credential for a front end you control. Generate one from Settings → Developer (it is baked into the embed snippet) or by calling GET /settings/embed-token. Tokens issued this way are long lived — a year — which is convenient for a pinned embed and another reason to keep them off public pages.
curl -H "Authorization: Bearer $LEADINBOX_EMBED_TOKEN" \
"https://www.leadinbox.ai/api/v1/conversations?limit=20"Which one should I use?
Use the API key when your own server is the caller and no end user is involved. Use the embed token when a browser is the caller — the iframe embed, or a custom UI you have built — because it is scoped to a specific workspace and user and can be reissued without disturbing your server integrations. Never ship the API key to a browser.
Failure responses
Status codes
| 401 | Unauthorized | No credential was supplied, the API key does not match any workspace, or the Bearer token failed signature verification or is missing tenant_id / user_id. Fix the credential — retrying will not help. |
| 403 | Forbidden | The credential is valid but the action is not allowed: the workspace has no active members, the plan has no free account slots, or the endpoint is an AI feature that requires the Growth plan. AI responses include "upgrade": true. |
| 404 | Not found | The record does not exist, or belongs to a different workspace. Every query is scoped by workspace, so a valid id from another tenant reads as missing. |
How the API behaves
Conventions
Requests and responses
Everything is JSON, in and out. Send Content-Type: application/json on requests with a body — the one exception is sending a message with attachments, which uses multipart/form-data. Responses are always a JSON object, never a bare array: a list of conversations comes back as { "conversations": [...] }, a single lead as { "lead": {...} }. That leaves room for pagination cursors and related records alongside the payload without a breaking change.
Cursor pagination
List endpoints use keyset pagination rather than offsets, so a page never shifts under you while new messages arrive. Every paginated response carries a next_cursor. Pass it back as the cursor query parameter to fetch the next page, and stop when next_cursor is null, which means the page was not full and you have reached the end.
The cursor is an ISO 8601 timestamp taken from the last item on the page, using whichever column that list is sorted by — last_message_at for conversations, last_contacted_at for contacts, created_at for leads, sent_at for messages. It is opaque in practice: echo it back rather than constructing one yourself.
cursor=""
while : ; do
resp=$(curl -s -H "X-API-KEY: $LEADINBOX_API_KEY" \
"https://www.leadinbox.ai/api/v1/conversations?limit=100${cursor:+&cursor=$cursor}")
echo "$resp" | jq '.conversations[].id'
cursor=$(echo "$resp" | jq -r '.next_cursor // empty')
[ -z "$cursor" ] && break
doneLimits
Each list endpoint takes a limit and silently clamps it to its maximum rather than erroring, so asking for 500 conversations returns 100.
Page sizes
| GET /conversations | limit | Default 30, maximum 100. |
| GET /conversations/{id} | message_limit | Default 50, maximum 100 messages per page. |
| GET /contacts | limit | Default 50, maximum 200. |
| GET /leads | limit | Default 50, maximum 200. |
| GET /templates, GET /pipeline/stages, GET /accounts | — | Not paginated. These lists are small and return in full. |
Errors
Errors use standard HTTP status codes with a JSON body whose error field is a human-readable sentence. When a request body fails schema validation the response also carries a details array naming the offending fields.
{
"error": "Invalid request",
"details": [
{
"code": "invalid_type",
"expected": "string",
"path": ["display_name"],
"message": "Required"
}
]
}Common statuses
| 200 / 201 | Success | 201 is returned when a record was created. |
| 400 | Bad request | Malformed JSON, a failed field validation, or an unknown field on a strict update endpoint. |
| 401 | Unauthorized | Missing or invalid credential. |
| 403 | Forbidden | Plan limit or feature gate. |
| 404 | Not found | Unknown id, or an id outside your workspace. |
| 409 | Conflict | Returned when creating a contact whose email already exists; the body includes existing_id. |
| 413 | Payload too large | Message attachments exceeded the 20 MB per-message budget. |
| 500 | Server error | Something failed on our side. Safe to retry idempotent reads. |
| 502 | Upstream error | A channel provider rejected or timed out on the operation — for example a send that the messaging provider refused. |
Timestamps, ids and casing
All timestamps are ISO 8601 strings in UTC, for example 2026-03-14T09:41:07.812Z. All record ids are UUIDs generated by LeadInbox; where a provider has its own identifier it is exposed separately as unipile_chat_id, unipile_message_id or unipile_account_id. Field names are snake_case. Channel names in provider fields are upper case — WHATSAPP, LINKEDIN, INSTAGRAM, TELEGRAM, MESSENGER, TWITTER, GOOGLE (Gmail), OUTLOOK, MAIL (IMAP). Filters accept either case and are upper-cased for you.
Partial updates
Update endpoints are PATCH and take only the fields you want to change; omitted fields are left alone. Most of them validate strictly, meaning a typo in a field name is a 400 rather than a silently ignored write — a deliberate choice, because a silently dropped update is much harder to debug than a rejected one.
Integration
Embedding the inbox
The fastest way to put LeadInbox inside another product is not to rebuild the UI at all. The full inbox — conversation list, thread view, composer, templates, attachments — runs inside an iframe and can be dropped into any page you control.
Getting your snippet
Open Settings → Developer in the app. The Embed panel generates a snippet with your token already in the URL, and offers a preview link so you can confirm the frame renders before you paste it anywhere. The same payload is available from the API:
curl -H "X-API-KEY: $LEADINBOX_API_KEY" \
"https://www.leadinbox.ai/api/v1/settings/embed-token"
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"embed_url": "https://www.leadinbox.ai/embed/inbox?token=...",
"snippet": "<iframe src=... ></iframe>"
}The snippet
<iframe
src="https://www.leadinbox.ai/embed/inbox?token=YOUR_EMBED_TOKEN"
style="width: 100%; height: 700px; border: 0; border-radius: 12px;"
allow="clipboard-write"
title="LeadInbox"
></iframe>How the token is handled
The token in the URL is what authenticates the frame. On load, the embed page moves it into sessionStorage, strips it from the visible URL with a history replace, and attaches it as an Authorization: Bearer header to every API request the inbox makes. It is not persisted anywhere else and does not survive the tab being closed. If the frame renders “Missing embed token”, the URL parameter was dropped somewhere between your page and the iframe — usually by a link shortener or a proxy that rewrites query strings.
Sizing and layout
The embedded inbox fills the iframe and manages its own internal scrolling, so the frame does not grow with the content — you set the box, the inbox lays itself out inside it. Give it a real height: a percentage height only works if every ancestor also has one, so a fixed height in pixels, a viewport unit, or a flex or grid cell with a resolved height are all safer choices. Below roughly 600 px of height the two-pane layout becomes cramped; around 700 px is a comfortable default. Width is fluid, and the layout collapses to a single pane on narrow screens, so width: 100% inside your own container is usually right.
<div style="height: calc(100vh - 64px);">
<iframe
src="https://www.leadinbox.ai/embed/inbox?token=YOUR_EMBED_TOKEN"
style="width: 100%; height: 100%; border: 0;"
allow="clipboard-write"
title="LeadInbox"
></iframe>
</div>Keep allow="clipboard-write" — copying a phone number or a message body from inside the thread depends on it.
Building your own UI instead
If the embed is not the right fit, the same embed token authenticates the whole REST API, so you can build a bespoke interface against the endpoints below and keep LeadInbox as the messaging backend.
API reference
Conversations
A conversation is one thread on one channel: a WhatsApp chat, a LinkedIn conversation, an email thread. It links a contact to the connected account that received it, and carries the counters the inbox list renders — unread count, last message preview, direction and timestamp.
/conversationsList conversations, newest activity first. Threads that have never received a message are omitted, and closed threads are excluded unless you ask for them by status.
Parameters
| status | string | active, archived, snoozed or closed. Omit to get everything except closed. |
| provider | string | Filter by channel, e.g. WHATSAPP or LINKEDIN. Case-insensitive. |
| account_id | uuid | Narrower than provider — one specific connected account, useful when two WhatsApp numbers are linked. |
| assigned_to | uuid | "me" | Only threads assigned to that user. "me" resolves to the authenticated user. |
| is_read | boolean | true or false. Pair with limit to build an unread badge. |
| is_starred | true | Only starred threads. |
| search | string | Case-insensitive match against the last message preview and the contact name. |
| cursor | timestamp | The next_cursor from the previous page. |
| limit | integer | Default 30, maximum 100. |
Each item embeds its contact (name, avatar, phone, email, company, job title), its connected_account, and a lead object with the current pipeline stage and AI score when the contact has an open lead — enough to render a rich inbox row without a second round trip.
curl -H "X-API-KEY: $LEADINBOX_API_KEY" \
"https://www.leadinbox.ai/api/v1/conversations?provider=WHATSAPP&is_read=false&limit=20"
{
"conversations": [
{
"id": "6f1c...",
"provider": "WHATSAPP",
"status": "active",
"is_read": false,
"unread_count": 3,
"message_count": 14,
"last_message_at": "2026-03-14T09:41:07.812Z",
"last_message_preview": "Can you send the proposal today?",
"last_message_direction": "inbound",
"contact": { "id": "b2e0...", "display_name": "Maya Osei", "company": "Northwind" },
"connected_account": { "id": "0a9d...", "provider": "WHATSAPP" },
"lead": { "stage": { "name": "Qualified" }, "ai_score": 78 }
}
],
"next_cursor": "2026-03-14T09:41:07.812Z"
}/conversations/{id}One conversation with a page of its messages, in chronological order, plus the full contact profile and the connected account. Fetching a thread marks it as read.
Parameters
| message_limit | integer | Messages per page. Default 50, maximum 100. |
| message_cursor | timestamp | The next_message_cursor from the previous page. Pages backwards through history. |
Messages include direction, body_text, body_html, email headers where relevant (email_subject, email_from, email_to, email_cc), delivery status, and an attachments array of ids, filenames, MIME types and sizes.
curl -H "X-API-KEY: $LEADINBOX_API_KEY" \
"https://www.leadinbox.ai/api/v1/conversations/CONVERSATION_ID?message_limit=50"/conversations/{id}Update thread state: star, pin, mark read or unread, archive, close, snooze, or assign to a teammate. Only the fields you send change; unknown fields are rejected with a 400.
Body fields
| is_starred | boolean | Star or unstar the thread. |
| is_read | boolean | Marking read also zeroes unread_count. |
| is_pinned | boolean | Pin to the top of the inbox list. |
| status | string | active, archived, snoozed or closed. |
| assigned_to | uuid | null | Assign to a workspace user, or null to unassign. |
| snoozed_until | ISO datetime | null | When a snoozed thread should resurface. |
curl -X PATCH -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "archived", "is_read": true}' \
"https://www.leadinbox.ai/api/v1/conversations/CONVERSATION_ID"API reference
Messages
Messages are read as part of a conversation and written with a single send endpoint. Sending is real: the message goes out on the underlying channel to the actual recipient. There is no test mode, so guard automated sending carefully.
/conversations/{id}/messagesSend a message in an existing conversation. LeadInbox picks the right transport from the thread's channel — a chat message for WhatsApp, LinkedIn, Instagram, Telegram, Messenger and X; a threaded reply to the most recent inbound email for Gmail, Outlook and IMAP.
JSON body
| body | string | Required. The message text. Must not be empty. |
curl -X POST -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body": "Thanks — the proposal is on its way this afternoon."}' \
"https://www.leadinbox.ai/api/v1/conversations/CONVERSATION_ID/messages"
HTTP/1.1 201 Created
{
"message": {
"id": "9c33...",
"direction": "outbound",
"body_text": "Thanks — the proposal is on its way this afternoon.",
"has_attachments": false,
"attachment_count": 0,
"status": "sent",
"sent_at": "2026-03-14T10:02:44.301Z"
}
}Sending attachments
To attach files, post the same endpoint as multipart/form-data with a body field and one or more files parts. Repeat files for multiple attachments. With attachments present the text may be empty, but a request with neither text nor files is a 400.
curl -X POST -H "X-API-KEY: $LEADINBOX_API_KEY" \
-F "body=Here is the proposal and the case study." \
-F "files=@proposal.pdf" \
-F "files=@case-study.pdf" \
"https://www.leadinbox.ai/api/v1/conversations/CONVERSATION_ID/messages"Attachment rules
| Total size | 20 MB | Combined across all files in one message. Over that the request fails with 413 before anything is sent. |
| Per channel | varies | Channels impose their own caps — WhatsApp media is around 16 MB, and email providers commonly stop around 25 MB. Staying well under 20 MB avoids provider-side rejection. |
| Storage | private bucket | Sent files are stored in a private bucket so your own thread renders them immediately; they are fetched back through the attachments endpoint with a short-lived signed URL. |
Failure modes
Errors
| 400 | No content | Neither body nor files were supplied, or the conversation has no chat id on a messaging channel. |
| 404 | Unknown thread | The conversation id does not exist in this workspace. |
| 413 | Too large | Attachments exceeded 20 MB in total. |
| 502 | Provider refused | The channel provider rejected or timed out on the send. Nothing was stored — the message did not go out. |
| 500 | Stored badly | Rare: the message was sent on the channel but could not be recorded locally. Do not blindly retry — check the thread first. |
On success the thread's preview, message count and the contact's last_contacted_at are updated in the same call, so the inbox reflects the send immediately. If the provider echoes the message back through its own webhook, LeadInbox merges the echo onto the row it already wrote rather than duplicating it.
/attachments/{id}Fetch an attachment by the id returned in a message's attachments array. Stored files redirect to a signed URL valid for one hour; inbound media that was never stored is streamed straight from the provider. Returns 404 when no copy can be produced.
curl -L -H "X-API-KEY: $LEADINBOX_API_KEY" \
-o invoice.pdf \
"https://www.leadinbox.ai/api/v1/attachments/ATTACHMENT_ID"API reference
Contacts
A contact is one person across every channel. When someone who emails you also messages on WhatsApp, both threads hang off the same contact record, and provider_identities records which handle belongs to which channel. Contacts also carry free-form tags and a custom_fields object you can use for your own attributes.
/contactsList contacts, most recently contacted first.
Parameters
| search | string | Case-insensitive match across name, email, company and phone. |
| tag | string | Only contacts carrying this tag. |
| source_provider | string | The channel the contact first arrived on. |
| cursor | timestamp | The next_cursor from the previous page (last_contacted_at). |
| limit | integer | Default 50, maximum 200. |
curl -H "X-API-KEY: $LEADINBOX_API_KEY" \
"https://www.leadinbox.ai/api/v1/contacts?search=acme&limit=50"/contactsCreate a contact by hand — useful when importing a list or capturing a web form. Creating a contact whose email already exists returns 409 with the existing id so you can update instead.
Body fields
| display_name | string | Required. 1–200 characters. |
| string | Must be a valid address. | |
| phone | string | Up to 30 characters. |
| company | string | Up to 200 characters. |
| job_title | string | Up to 200 characters. |
| location | string | Up to 200 characters. |
| website | string | Up to 500 characters. |
| tags | string[] | Free-form labels. |
curl -X POST -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Maya Osei",
"email": "maya@northwind.example",
"company": "Northwind",
"job_title": "Head of Ops",
"tags": ["inbound", "webinar"]
}' \
"https://www.leadinbox.ai/api/v1/contacts"/contacts/{id}The full profile: the contact record, its five most recent conversations, up to ten linked leads with their pipeline stage, the last twenty messages across all channels, and a computed stats block (messages sent and received, response rate, channel count, first and last contact).
/contacts/{id}Update contact fields. Nullable fields accept null to clear them. custom_fields is merged into whatever is already stored rather than replacing it; tags replace wholesale. Unknown fields are rejected.
Body fields
| display_name | string | 1–200 characters. |
| email, phone, company, job_title, location, website, avatar_url | string | null | Set or clear individually. |
| bio | string | null | Up to 2000 characters. |
| tags | string[] | Replaces the existing tag list. |
| custom_fields | object | Shallow-merged into the stored object. |
curl -X PATCH -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tags": ["inbound", "priority"], "custom_fields": {"crm_id": "AC-4471"}}' \
"https://www.leadinbox.ai/api/v1/contacts/CONTACT_ID"/contacts/mergeMerge duplicates. Conversations, messages and leads move from the secondary contact to the primary; provider identities and tags are combined; custom fields and enrichment data merge with the primary winning; message counters are summed and the first and last contact dates widened. The secondary record is then deleted. This cannot be undone.
Body fields
| primary_id | uuid | Required. The record that survives. |
| secondary_id | uuid | Required. Merged in, then deleted. Must differ from primary_id. |
curl -X POST -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"primary_id": "KEEP_ID", "secondary_id": "DUPLICATE_ID"}' \
"https://www.leadinbox.ai/api/v1/contacts/merge"API reference
Pipeline & leads
The pipeline is a set of ordered stages; a lead is a contact moving through them. Stages carry a name, colour, position and three behavioural flags — is_default (where new leads land), is_won and is_lost — which drive the kanban board and the won/lost reporting.
Stages
/pipeline/stagesList every stage in the workspace, ordered by position. Not paginated.
curl -H "X-API-KEY: $LEADINBOX_API_KEY" "https://www.leadinbox.ai/api/v1/pipeline/stages"
{
"stages": [
{ "id": "1f2a...", "name": "New lead", "slug": "new-lead", "color": "#6B7280",
"position": 0, "is_default": true, "is_won": false, "is_lost": false }
]
}/pipeline/stagesCreate a stage. The slug is derived from the name automatically.
Body fields
| name | string | Required. 1–50 characters. |
| position | integer | Required. Order on the board, 0 or greater. The seeded default stages run from 1 upwards. |
| color | string | Hex colour. Defaults to #6B7280. |
| icon | string | Optional icon key. |
| is_default | boolean | Where newly created leads land. |
| is_won | boolean | Marks the stage as a win. |
| is_lost | boolean | Marks the stage as a loss. |
curl -X POST -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Demo booked", "color": "#2563EB", "position": 3}' \
"https://www.leadinbox.ai/api/v1/pipeline/stages"/pipeline/stages/{id}Rename, recolour, reorder or attach automation settings to a stage. Renaming regenerates the slug.
Body fields
| name | string | 1–50 characters. |
| color | string | Hex colour. |
| icon | string | Icon key. |
| position | integer | New board position. |
| auto_actions | object | Stage automation settings. |
/pipeline/stages/{id}Delete a stage. The default, won and lost stages are protected and return 400 — reassign those flags to another stage first.
Leads
/leadsList leads, newest first, with the contact, the stage and the assignee embedded. Defaults to open leads only.
Parameters
| status | string | open (default), won, lost or archived. |
| stage_id | uuid | Only leads in this stage. |
| assigned_to | uuid | "me" | Only leads owned by that user. |
| ai_classification | string | hot, warm, cold or unqualified. |
| min_score / max_score | integer | Bound the AI score, 0–100. |
| search | string | Matches the lead title and the contact name. |
| cursor | timestamp | The next_cursor from the previous page (created_at). |
| limit | integer | Default 50, maximum 200. |
curl -H "X-API-KEY: $LEADINBOX_API_KEY" \
"https://www.leadinbox.ai/api/v1/leads?ai_classification=hot&min_score=70&limit=50"/leadsCreate a lead against an existing contact and stage. Creation is recorded on the lead's activity timeline.
Body fields
| contact_id | uuid | Required. |
| stage_id | uuid | Required. Usually your default stage. |
| title | string | Deal name. |
| value | number | Deal value. |
| currency | string | Defaults to USD. |
| source | string | Where the lead came from. |
| source_provider | string | The channel it arrived on. |
| assigned_to | uuid | Owning user. |
curl -X POST -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "CONTACT_ID",
"stage_id": "STAGE_ID",
"title": "Northwind — 40 seats",
"value": 12000,
"currency": "GBP",
"source": "webinar"
}' \
"https://www.leadinbox.ai/api/v1/leads"/leads/{id}One lead with its contact, stage and assignee, the last thirty activity entries, and up to ten conversations belonging to the same contact.
/leads/{id}Move a lead, reassign it, set its value, or close it as won or lost. Stage changes, assignment changes and status changes are each written to the activity timeline, and a stage change also queues a fresh AI score where the plan allows it.
Body fields
| stage_id | uuid | Move to another stage; stage_entered_at is reset. |
| assigned_to | uuid | null | Reassign or unassign. |
| status | string | open, won, lost or archived. won and lost stamp won_at / lost_at. |
| lost_reason | string | Recorded when status is lost. |
| title | string | Deal name. |
| value | number | null | Deal value. |
| currency | string | Currency code. |
| expected_close_date | string | null | Forecast close date. |
| manual_score | integer | null | Your own 0–100 score. |
| manual_classification | string | null | hot, warm, cold or unqualified. |
| score_override | boolean | Make the manual score win over the AI score. |
curl -X PATCH -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "won", "value": 12000}' \
"https://www.leadinbox.ai/api/v1/leads/LEAD_ID"/leads/{id}Archive a lead. This is a soft delete — the status becomes archived and an activity entry is written; the record and its history are retained.
/leads/{id}/scoreRun AI scoring on demand. Requires the Growth plan. To control cost, a lead scored within the last hour is returned unchanged with skipped: true rather than being re-scored.
curl -X POST -H "X-API-KEY: $LEADINBOX_API_KEY" \
"https://www.leadinbox.ai/api/v1/leads/LEAD_ID/score"
{
"lead": { "ai_score": 78, "ai_classification": "hot", "ai_scored_at": "..." },
"scoring": {
"score": 78,
"classification": "hot",
"reasons": ["Asked for pricing", "Decision-maker job title"],
"nextAction": "Send the proposal and offer two call slots this week"
}
}API reference
Accounts
A connected account is one channel login: a WhatsApp number, a LinkedIn profile, a mailbox. Accounts are the unit your plan counts, and their status tells you whether messages are still flowing — ok, connecting, credentials_expired, checkpoint_required, disconnected or error.
/accountsList connected accounts with their provider, display name or email, avatar, status, status message and last sync time. Not paginated.
curl -H "X-API-KEY: $LEADINBOX_API_KEY" "https://www.leadinbox.ai/api/v1/accounts"
{
"accounts": [
{
"id": "0a9d...",
"provider": "WHATSAPP",
"provider_user_name": "+44 7700 900123",
"status": "ok",
"last_sync_at": "2026-03-14T09:12:00.000Z"
}
]
}/accounts/{id}One account, with its live status refreshed from the provider where possible. If the upstream check fails the cached record is returned rather than an error.
/accountsStart a connection and get back a hosted authentication link to open in a browser. This is how Gmail and Outlook connect, and it works as a generic fallback for the other providers too. Returns 403 when every account slot on your plan is already in use.
Body fields
| providers | "*" | string[] | Which channels to offer. "*" (the default) offers all of them. GMAIL and IMAP are translated to the provider names the hosted flow expects. |
| callback_url | string | Where to send the user after connecting. Defaults to your LeadInbox accounts page. |
curl -X POST -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"providers": ["GMAIL"], "callback_url": "https://app.example.com/settings"}' \
"https://www.leadinbox.ai/api/v1/accounts"
{ "auth_link": { "url": "https://..." } }/accounts/{id}Disconnect an account. By default the login is revoked upstream and the account is marked disconnected while its conversation history is kept. Pass ?mode=remove to delete the record entirely, which cascades to its conversations, messages, attachments and AI records — contacts and their leads are workspace CRM data and are preserved.
# Disconnect, keep history
curl -X DELETE -H "X-API-KEY: $LEADINBOX_API_KEY" \
"https://www.leadinbox.ai/api/v1/accounts/ACCOUNT_ID"
# Remove entirely
curl -X DELETE -H "X-API-KEY: $LEADINBOX_API_KEY" \
"https://www.leadinbox.ai/api/v1/accounts/ACCOUNT_ID?mode=remove"API reference
Templates
Reply templates are saved snippets with a shortcode, a category and optional channel restrictions. Shared templates are visible to the whole workspace; private ones only to their author. Lists are returned most-used first and are not paginated.
/templatesList templates available to the caller — every shared template plus the caller's own private ones.
Parameters
| category | string | Filter by category, e.g. follow_up. |
| provider | string | Only templates for this channel. Templates with an empty providers array count as all channels and are always included. |
| search | string | Matches name, shortcode and body text. |
/templatesCreate a template. If you omit the shortcode, one is generated from the name — “Follow up” becomes /follow-up. Templates are shared by default.
Body fields
| name | string | Required. 1–100 characters. |
| body_text | string | Required. Up to 5000 characters. |
| body_html | string | Rich version for email. Up to 10000 characters. |
| shortcode | string | Typed in the composer to insert the template. |
| category | string | Defaults to general. |
| providers | string[] | Restrict to specific channels. Empty means all. |
| is_shared | boolean | Defaults to true. |
curl -X POST -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Follow up",
"category": "follow_up",
"body_text": "Hi {{contact.first_name}}, just following up on our conversation — would a call this week work?",
"providers": ["LINKEDIN", "GOOGLE"],
"is_shared": true
}' \
"https://www.leadinbox.ai/api/v1/templates"/templates/{id}Update any of the fields above. Unknown fields are rejected.
/templates/{id}Delete a template permanently.
Variables
Template bodies may contain {{variable}} placeholders, which the composer fills in from the open conversation at the moment you insert the template. A placeholder with no value resolves to an empty string rather than leaving the raw token in the message. The API stores and returns the template text verbatim — substitution happens where the template is used, so if you send a template body through the messages endpoint yourself, substitute the values first.
{{contact.name}}{{contact.first_name}}{{contact.company}}{{contact.job_title}}{{contact.email}}{{user.name}}{{lead.stage}}{{lead.score}}API reference
Sync & workspace
Day to day, LeadInbox stays current from provider webhooks — you should not need to poll. These endpoints cover the exceptions: a manual backfill, and the workspace-level settings behind the developer tools.
/syncTrigger a sync across every connected account in the workspace: recent chats and messages are fetched and upserted, creating contacts and conversations as needed. Useful after connecting an account, or to repair a gap. This is a heavy call — run it deliberately, not on a tight schedule.
curl -X POST -H "X-API-KEY: $LEADINBOX_API_KEY" "https://www.leadinbox.ai/api/v1/sync"
{
"success": true,
"synced": { "contacts": 12, "conversations": 31, "messages": 428 },
"accounts_found": 3,
"chats_fetched": 31
}If individual accounts fail, the response still succeeds and lists what went wrong in an errors array.
/settingsWorkspace settings: name, slug, API key, allowed origins, outbound webhook URL, AI provider and key health, plan limits, the ten most recent webhook events received, and your AI scoring rules. Secrets are masked — the webhook secret shows only its last four characters and the AI key is never returned, only a flag and a hint.
/settingsUpdate workspace settings. The settings object is merged rather than replaced. The AI key cannot be set here — it has its own endpoint that live-tests the key before storing it.
Body fields
| name | string | Workspace name, 1–100 characters. |
| webhook_url | string | null | Destination URL for outbound notifications. |
| webhook_secret | string | null | Shared secret stored alongside it. |
| allowed_origins | string[] | Origins permitted to embed or call from the browser. |
| settings | object | Free-form workspace preferences, shallow-merged. |
/settings/embed-tokenIssue an embed token valid for one year, together with the embed URL and a ready-to-paste iframe snippet. See Embedding the inbox.
/settings/ai-keyStore your own AI provider key. The key is live-tested against the provider before it is saved, so a stored key is always a working key. Requires the Growth plan.
Body fields
| provider | string | anthropic (default), openai or google. |
| api_key | string | The provider key, 20–300 characters. |
/settings/ai-keyRe-test the stored key and record the result — the “test connection” action for a key that is already saved.
/settings/ai-keyRemove the stored AI key and its health record. AI features stop working until a new key is saved.
/user/preferencesRead the authenticated user's preferences object.
/user/preferencesMerge-update preferences. Currently supports theme, which accepts light or dark.
/A tiny unauthenticated-looking identity endpoint returning the API name and version — handy as a connectivity check.
curl "https://www.leadinbox.ai/api/v1"
{ "name": "LeadInbox API", "version": "1.0.0" }Platform
Channels
LeadInbox connects nine channel types across messaging and email. Each connection is one account against your plan, and you can connect several of the same type — two WhatsApp numbers and three mailboxes is four accounts, all landing in the same inbox.
In-app connection flows
Most channels connect natively inside the app, with no third-party wizard and no redirect away from LeadInbox.
Native providers
| QR or pairing code | Scan the QR from WhatsApp on your phone, or enter your number with country code to receive a pairing code to type into WhatsApp instead. The modal polls until the link completes. | |
| Telegram | QR | Scan the QR from the Telegram app. |
| Credentials | Username and password, with a checkpoint step for 2FA codes or in-app approval. | |
| Credentials | Username and password, plus a checkpoint where required. | |
| Messenger | Credentials | Facebook credentials, plus a checkpoint where required. |
| X (Twitter) | Credentials | Username, password and the email on the account, plus a checkpoint where required. |
| IMAP / other email | Server credentials | Address, password or app password, IMAP host and port (default 993) and SMTP host and port (default 587). The login is validated before the account is created. |
Hosted connection flow
Gmail and Outlook connect through a hosted OAuth flow, which opens in a popup because their OAuth consent must be granted to the messaging platform's own application. Click Connect on those cards, complete Google's or Microsoft's consent screen, and you are returned to your accounts page. The same flow is available over the API via POST /accounts, which returns a link to open.
After connecting
Recent chats and messages are backfilled so the inbox is not empty, contacts are created or matched, and from then on inbound activity arrives by webhook. Sending is symmetrical: a reply on a messaging channel goes out as a chat message, and a reply on an email channel goes out as a threaded reply to the most recent inbound message in that thread. If a channel's credentials expire, its account status changes and it is flagged in the app so you can reconnect.
Platform
AI features
AI in LeadInbox runs on your own provider key. You supply a key from Anthropic, OpenAI or Google under Settings → AI; it is live-tested before it is stored, and every AI call is billed by that provider to you directly. Nothing is resold, and there are no opaque credit packs. AI features are part of the Growth plan — on other plans the AI endpoints return 403 with upgrade: true.
What each feature does
| Lead scoring | POST /leads/{id}/score | Reads the lead, the contact profile and the last ten messages, and returns a 0–100 score, a hot / warm / cold / unqualified classification, the reasons behind it, and a suggested next action. Re-scoring within an hour is skipped to control cost, and won/lost outcomes are fed back as calibration data. |
| Reply suggestions | GET /conversations/{id}/suggestions | Drafts replies from the recent thread, the contact, the channel and the lead score, so a LinkedIn suggestion reads differently from an email one. POST the same path to record whether a suggestion was used, edited or dismissed. |
| Conversation insights | GET /conversations/{id}/insights | Summarises up to fifty messages in a thread into the state of play — what the contact wants, where the deal stands, what is blocking it. |
| Stage suggestions | GET /conversations/{id}/stage-suggestion | Watches the conversation against your pipeline and proposes moving the lead when the thread justifies it, with a confidence value. POST with accepted or dismissed to act on it — accepting moves the lead and writes the activity entry; both outcomes recalibrate future confidence. |
Each feature can also be switched off for the whole workspace, in which case its endpoint returns a 403 or an empty suggestion rather than calling your provider. AI usage is logged per workspace so you can reconcile it against your provider bill.
Platform
Webhooks
It is worth separating two things that both get called “webhooks”.
Inbound: how LeadInbox stays live
LeadInbox receives webhooks from the messaging platform that fronts your connected channels. Those deliveries are what make the inbox real time: a new WhatsApp message, an email arriving or being sent, an account status change, a new LinkedIn relation, and email tracking events (opens and link clicks) are all pushed to LeadInbox, verified by signature, and turned into conversation and message records. Every delivery is recorded in a webhook event log, and the ten most recent are visible in the app under Settings and returned by GET /settings. You do not configure this — connecting an account sets it up — but it is why you rarely need to poll or call POST /sync.
Outbound: notifying your systems
Your workspace has a webhook_url and a webhook_secret, set under Settings → Developer or with PATCH /settings. These fields configure the destination for outbound notifications and the shared secret that will be used to sign them.
GET /conversations?is_read=false on a sensible interval, or to page with a cursor from your last-seen timestamp. If outbound webhooks matter to your project, email contact@desklink.ai and we will tell you where it stands.curl -X PATCH -H "X-API-KEY: $LEADINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://api.example.com/hooks/leadinbox",
"webhook_secret": "a-long-random-string"
}' \
"https://www.leadinbox.ai/api/v1/settings"Reading settings back never echoes the secret in full — it is masked to its last four characters, so store your own copy when you set it.
Help
Support
There is no official SDK package for LeadInbox yet — no npm or PyPI client, and no published OpenAPI document. The REST API described on this page is the supported integration route, and it is deliberately plain: JSON over HTTPS, one header for authentication, cursor pagination. Any HTTP client in any language works, and Settings → Developer includes a copy-paste prompt that gives an AI coding assistant your embed snippet, your key and the endpoint reference in one go.
For anything else — an endpoint behaving unexpectedly, a channel that will not connect, a key you need rotated, or a capability you need that is not here — email contact@desklink.ai. Include your workspace name, the endpoint and method, the approximate time of the request and the status code you saw; never include your API key or an embed token in an email.