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.

https://www.leadinbox.ai/api/v1Plans & limits

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.

Your first 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.

Base URL: https://www.leadinbox.ai/api/v1 — every path in this reference is relative to it. There is no versioned subdomain and no separate sandbox host; use a second workspace if you want an isolated place to experiment.

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.

Header form
GET /api/v1/contacts?limit=25 HTTP/1.1
Host: www.leadinbox.ai
X-API-KEY: your-workspace-api-key
Accept: application/json

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

Bearer form
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

401UnauthorizedNo 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.
403ForbiddenThe 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.
404Not foundThe 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.
Both credentials grant workspace-wide access. LeadInbox does not currently offer read-only, scoped or per-user API keys — if you need to expose a narrow slice of data to a third party, proxy it through your own service rather than handing over the key.

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.

Paging through every conversation
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
done

Limits

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 /conversationslimitDefault 30, maximum 100.
GET /conversations/{id}message_limitDefault 50, maximum 100 messages per page.
GET /contactslimitDefault 50, maximum 200.
GET /leadslimitDefault 50, maximum 200.
GET /templates, GET /pipeline/stages, GET /accountsNot 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.

400 Bad Request
{
  "error": "Invalid request",
  "details": [
    {
      "code": "invalid_type",
      "expected": "string",
      "path": ["display_name"],
      "message": "Required"
    }
  ]
}

Common statuses

200 / 201Success201 is returned when a record was created.
400Bad requestMalformed JSON, a failed field validation, or an unknown field on a strict update endpoint.
401UnauthorizedMissing or invalid credential.
403ForbiddenPlan limit or feature gate.
404Not foundUnknown id, or an id outside your workspace.
409ConflictReturned when creating a contact whose email already exists; the body includes existing_id.
413Payload too largeMessage attachments exceeded the 20 MB per-message budget.
500Server errorSomething failed on our side. Safe to retry idempotent reads.
502Upstream errorA 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:

GET /settings/embed-token
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

Paste into your page
<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.

Only embed on pages you control. The token grants access to that workspace's inbox — reading every conversation and sending on every connected channel. Put the iframe behind your own authentication, serve it from your own domain, and never publish it on a page that anonymous visitors can reach or view-source.

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.

Full-height embed
<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.

GET/conversations

List 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

statusstringactive, archived, snoozed or closed. Omit to get everything except closed.
providerstringFilter by channel, e.g. WHATSAPP or LINKEDIN. Case-insensitive.
account_iduuidNarrower than provider — one specific connected account, useful when two WhatsApp numbers are linked.
assigned_touuid | "me"Only threads assigned to that user. "me" resolves to the authenticated user.
is_readbooleantrue or false. Pair with limit to build an unread badge.
is_starredtrueOnly starred threads.
searchstringCase-insensitive match against the last message preview and the contact name.
cursortimestampThe next_cursor from the previous page.
limitintegerDefault 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.

Unread WhatsApp threads
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"
}
GET/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_limitintegerMessages per page. Default 50, maximum 100.
message_cursortimestampThe 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"
PATCH/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_starredbooleanStar or unstar the thread.
is_readbooleanMarking read also zeroes unread_count.
is_pinnedbooleanPin to the top of the inbox list.
statusstringactive, archived, snoozed or closed.
assigned_touuid | nullAssign to a workspace user, or null to unassign.
snoozed_untilISO datetime | nullWhen a snoozed thread should resurface.
Archive a thread
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.

POST/conversations/{id}/messages

Send 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

bodystringRequired. The message text. Must not be empty.
Send text
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.

Send text with attachments
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 size20 MBCombined across all files in one message. Over that the request fails with 413 before anything is sent.
Per channelvariesChannels 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.
Storageprivate bucketSent 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

400No contentNeither body nor files were supplied, or the conversation has no chat id on a messaging channel.
404Unknown threadThe conversation id does not exist in this workspace.
413Too largeAttachments exceeded 20 MB in total.
502Provider refusedThe channel provider rejected or timed out on the send. Nothing was stored — the message did not go out.
500Stored badlyRare: 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.

GET/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.

GET/contacts

List contacts, most recently contacted first.

Parameters

searchstringCase-insensitive match across name, email, company and phone.
tagstringOnly contacts carrying this tag.
source_providerstringThe channel the contact first arrived on.
cursortimestampThe next_cursor from the previous page (last_contacted_at).
limitintegerDefault 50, maximum 200.
curl -H "X-API-KEY: $LEADINBOX_API_KEY" \
  "https://www.leadinbox.ai/api/v1/contacts?search=acme&limit=50"
POST/contacts

Create 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_namestringRequired. 1–200 characters.
emailstringMust be a valid address.
phonestringUp to 30 characters.
companystringUp to 200 characters.
job_titlestringUp to 200 characters.
locationstringUp to 200 characters.
websitestringUp to 500 characters.
tagsstring[]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"
GET/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).

PATCH/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_namestring1–200 characters.
email, phone, company, job_title, location, website, avatar_urlstring | nullSet or clear individually.
biostring | nullUp to 2000 characters.
tagsstring[]Replaces the existing tag list.
custom_fieldsobjectShallow-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"
POST/contacts/merge

Merge 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_iduuidRequired. The record that survives.
secondary_iduuidRequired. 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

GET/pipeline/stages

List 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 }
  ]
}
POST/pipeline/stages

Create a stage. The slug is derived from the name automatically.

Body fields

namestringRequired. 1–50 characters.
positionintegerRequired. Order on the board, 0 or greater. The seeded default stages run from 1 upwards.
colorstringHex colour. Defaults to #6B7280.
iconstringOptional icon key.
is_defaultbooleanWhere newly created leads land.
is_wonbooleanMarks the stage as a win.
is_lostbooleanMarks 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"
PATCH/pipeline/stages/{id}

Rename, recolour, reorder or attach automation settings to a stage. Renaming regenerates the slug.

Body fields

namestring1–50 characters.
colorstringHex colour.
iconstringIcon key.
positionintegerNew board position.
auto_actionsobjectStage automation settings.
DELETE/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

GET/leads

List leads, newest first, with the contact, the stage and the assignee embedded. Defaults to open leads only.

Parameters

statusstringopen (default), won, lost or archived.
stage_iduuidOnly leads in this stage.
assigned_touuid | "me"Only leads owned by that user.
ai_classificationstringhot, warm, cold or unqualified.
min_score / max_scoreintegerBound the AI score, 0–100.
searchstringMatches the lead title and the contact name.
cursortimestampThe next_cursor from the previous page (created_at).
limitintegerDefault 50, maximum 200.
Hot open leads
curl -H "X-API-KEY: $LEADINBOX_API_KEY" \
  "https://www.leadinbox.ai/api/v1/leads?ai_classification=hot&min_score=70&limit=50"
POST/leads

Create a lead against an existing contact and stage. Creation is recorded on the lead's activity timeline.

Body fields

contact_iduuidRequired.
stage_iduuidRequired. Usually your default stage.
titlestringDeal name.
valuenumberDeal value.
currencystringDefaults to USD.
sourcestringWhere the lead came from.
source_providerstringThe channel it arrived on.
assigned_touuidOwning 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"
GET/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.

PATCH/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_iduuidMove to another stage; stage_entered_at is reset.
assigned_touuid | nullReassign or unassign.
statusstringopen, won, lost or archived. won and lost stamp won_at / lost_at.
lost_reasonstringRecorded when status is lost.
titlestringDeal name.
valuenumber | nullDeal value.
currencystringCurrency code.
expected_close_datestring | nullForecast close date.
manual_scoreinteger | nullYour own 0–100 score.
manual_classificationstring | nullhot, warm, cold or unqualified.
score_overridebooleanMake the manual score win over the AI score.
Close a deal
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"
DELETE/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.

POST/leads/{id}/score

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

GET/accounts

List 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"
    }
  ]
}
GET/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.

POST/accounts

Start 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_urlstringWhere 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://..." } }
DELETE/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"
The QR, pairing-code and credential login flows for WhatsApp, Telegram, Instagram, LinkedIn, Messenger, X and IMAP are driven by the in-app connect modal, which handles QR refresh and 2FA checkpoints interactively. Connect those channels through the app rather than scripting them.

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.

GET/templates

List templates available to the caller — every shared template plus the caller's own private ones.

Parameters

categorystringFilter by category, e.g. follow_up.
providerstringOnly templates for this channel. Templates with an empty providers array count as all channels and are always included.
searchstringMatches name, shortcode and body text.
POST/templates

Create 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

namestringRequired. 1–100 characters.
body_textstringRequired. Up to 5000 characters.
body_htmlstringRich version for email. Up to 10000 characters.
shortcodestringTyped in the composer to insert the template.
categorystringDefaults to general.
providersstring[]Restrict to specific channels. Empty means all.
is_sharedbooleanDefaults 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"
PATCH/templates/{id}

Update any of the fields above. Unknown fields are rejected.

DELETE/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.

POST/sync

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

GET/settings

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

PATCH/settings

Update 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

namestringWorkspace name, 1–100 characters.
webhook_urlstring | nullDestination URL for outbound notifications.
webhook_secretstring | nullShared secret stored alongside it.
allowed_originsstring[]Origins permitted to embed or call from the browser.
settingsobjectFree-form workspace preferences, shallow-merged.
GET/settings/embed-token

Issue an embed token valid for one year, together with the embed URL and a ready-to-paste iframe snippet. See Embedding the inbox.

POST/settings/ai-key

Store 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

providerstringanthropic (default), openai or google.
api_keystringThe provider key, 20–300 characters.
PUT/settings/ai-key

Re-test the stored key and record the result — the “test connection” action for a key that is already saved.

DELETE/settings/ai-key

Remove the stored AI key and its health record. AI features stop working until a new key is saved.

GET/user/preferences

Read the authenticated user's preferences object.

PATCH/user/preferences

Merge-update preferences. Currently supports theme, which accepts light or dark.

GET/

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

WhatsAppQR or pairing codeScan 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.
TelegramQRScan the QR from the Telegram app.
LinkedInCredentialsUsername and password, with a checkpoint step for 2FA codes or in-app approval.
InstagramCredentialsUsername and password, plus a checkpoint where required.
MessengerCredentialsFacebook credentials, plus a checkpoint where required.
X (Twitter)CredentialsUsername, password and the email on the account, plus a checkpoint where required.
IMAP / other emailServer credentialsAddress, 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 scoringPOST /leads/{id}/scoreReads 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 suggestionsGET /conversations/{id}/suggestionsDrafts 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 insightsGET /conversations/{id}/insightsSummarises 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 suggestionsGET /conversations/{id}/stage-suggestionWatches 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.

Be aware: outbound delivery to that URL is not live yet. Setting the field today stores your destination and secret; it does not currently push events to you, and we are not going to publish a payload schema we have not shipped. Until it lands, the reliable pattern is to poll 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.
Configure the destination
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.