Lalexi
API

API

Lalexi conversations, messages, contacts, channels, connecting numbers and broadcasts from your CRM, website or back office: the address, the personal token, workspaces, errors, pages, limits and safe retries.

The Lalexi API gives your own systems — a CRM, a website, a back office — the same conversations, messages, contacts and channels your team works with in Lalexi. Through the API you can connect and disconnect messenger numbers and import their history, read conversations and write into them, write first to a new number, run broadcasts, keep contacts, change a conversation's status, assignee and labels, see message usage and receive events through webhooks.

Every request runs as one person — the owner of the token — with exactly the access that person has in the workspace. Whatever is sent or changed through the API is recorded as done by them, as if they had clicked in Lalexi themselves.

Address

https://app.lalexi.com/v1

Every path below is relative to it: GET /me is GET https://app.lalexi.com/v1/me. Request and response bodies are JSON (Content-Type: application/json); files go as multipart/form-data.

Token

The API takes a personal access token. Every member of a workspace has one: Profile settings → Access Token. Send it in the Authorization header:

curl https://app.lalexi.com/v1/me \
  -H "Authorization: Bearer <your token>"
  • The token's access is your access. An administrator's token sees the whole workspace, an agent's token only the agent's channels and conversations. There are no separate "integration keys" with other rights: if an integration should not act in your name, give it a member of its own with the role it needs.
  • Resetting the token in the profile settings turns the old one off at once.
  • No token, an unknown token or one that is not a personal token answers 401 invalid_api_key.
  • Keep the token like a password: on your server only, never in a web page or a mobile app. The API is meant for server-to-server calls and does not answer browser requests from other sites (CORS). The one exception is this documentation site itself, so that the Send button of the Reference works.

Try it in the browser

Every page of the Reference is also a working form. Put your personal token in the Authorization field, fill in the parameters and press Send: the request goes straight from your browser and the answer appears on the same page. That is quicker than assembling a curl by hand, and it shows the real fields of a real answer rather than only the schema.

Two warnings, both of them real.

The requests are real. The API has no test mode: POST /dialogs does write to a person in a messenger, POST /channels/{id}/disconnect does disconnect a number, DELETE /webhooks/{id} does remove a subscription. Anything GET is safe to poke at.

The token stays in your browser. The page keeps it in browser storage so you need not paste it every time, and sends it to app.lalexi.com only — not to the docs server, not anywhere else. On somebody else's computer, or a shared one, better not to paste it there at all; if you did, clear the site's data in the browser afterwards.

Workspace

If you belong to several workspaces, name one in the X-Workspace-Id header of every request. Your workspaces' ids and your role in each are in the answer of GET /me (which needs no header):

curl https://app.lalexi.com/v1/channels \
  -H "Authorization: Bearer <your token>" \
  -H "X-Workspace-Id: 7"

With a single workspace the header can be left out.

SituationAnswer
Several workspaces and no header400 workspace_required
Not your workspace404 workspace_not_found
The workspace is suspended403 workspace_suspended
API access is turned off for the workspace403 api_disabled

Ids and times

A conversation's id is the number in its Lalexi URL, so you can link straight to it. Every other object's id is its own number. Times are ISO 8601 in UTC: 2026-10-27T09:30:00Z.

Errors

Every error comes in one shape:

{
  "error": {
    "code": "validation_failed",
    "message": "The request is invalid.",
    "details": { "text": ["is required"] }
  }
}

Branch on code — the list is closed. message is for people and its wording may change. details is there only when there is more to say: which fields failed, the id of an existing contact, the numbers of a reached limit.

codeHTTPWhat happened
invalid_json400The body is not valid JSON.
workspace_required400Several workspaces and no X-Workspace-Id.
invalid_api_key401The token is missing, unknown or not a personal token.
forbidden403Your role does not allow this.
workspace_suspended403The workspace is suspended.
api_disabled403API access is turned off for the workspace.
not_found404No such object, or one you may not see (the answer is the same); also any unknown path.
workspace_not_found404Not a workspace of yours.
contact_exists409A contact with this phone, e-mail or identifier exists — its id is in details.id.
idempotency_in_progress409A request with this Idempotency-Key is still running.
duplicate409The same dialog start (channel, recipient, text) less than a minute ago.
message_limit_reached409The workspace's message limit is used up — details.limit, details.used.
channel_disconnected409The channel is not connected right now.
validation_failed422The request is invalid; details names the fields.
invalid_cursor422The cursor is damaged.
idempotency_key_reused422This Idempotency-Key was used with a different request.
identifier_read_only422A messenger contact's identifier cannot be changed.
invalid_assignee422That person does not work in the conversation's channel.
invalid_team422No such team in the workspace.
attachment_invalid422A file is too large or cannot be stored.
channel_reply_only422The channel can only answer people who wrote first.
channel_cannot_start422The channel cannot start dialogs through the API.
probe_not_supported422Number checks work on messenger channels only.
template_not_found422Not an approved WhatsApp Business template of this channel.
invalid_recipient422Not a valid recipient for the channel.
recipient_rejected422The messenger refused this recipient.
send_rejected422The messenger refused the message.
rate_limited429Over a rate limit — wait Retry-After seconds.
internal_error500Something went wrong on our side.
upstream_unavailable503The messaging service did not answer; nothing was sent, retrying is safe.

Pages

Lists answer {"data": [...], "next_cursor": "..."}. limit is how many items to return (1 to 100, 25 by default). For the next page pass next_cursor from the answer as cursor; on the last page next_cursor is null. A cursor is an opaque string: never build or edit one.

Short lists — channels, agents, teams, labels, custom fields, webhooks — come whole, with next_cursor: null.

To sync conversations use GET /conversations?updated_since=…: with it, conversations come from the oldest change to the newest, and one that changes while you walk the pages is not lost — it moves to the end. Keep the updated_at of the last conversation you received and pass it as updated_since next time.

Safe retries

POST /conversations/{conversation_id}/messages and POST /dialogs accept an Idempotency-Key header — any string up to 255 characters, such as a UUID or the id of the record in your system:

curl https://app.lalexi.com/v1/conversations/1042/messages \
  -H "Authorization: Bearer <your token>" \
  -H "Idempotency-Key: crm-order-1042-confirmation" \
  -H "Content-Type: application/json" \
  -d '{"text": "Your order has shipped."}'
  • A repeat with the same key within 24 hours sends nothing new and returns the first answer.
  • While the first request is running, a repeat gets 409 idempotency_in_progress.
  • The same key with a different body gets 422 idempotency_key_reused.
  • Keys are separate per person, per workspace and per address.
  • If the first attempt failed on our side (a 5xx), the key is released and the next attempt runs for real.

So a request is safe to repeat after a dropped connection or a timeout: the message does not go out twice.

Rate limits

What is countedLimit
Requests with one token300 a minute
POST /dialogs with one token20 a minute
GET /dialogs/probe with one token30 a minute
POST /webhooks/{id}/test with one token5 a minute
POST /channels and POST /channels/{id}/reconnect with one token, together10 a minute
Queued messenger work with one token, together: POST …/history-import, PUT …/chat-filter, POST …/chat-filter/allow, POST /broadcasts, POST /broadcasts/{id}/rephrase10 a minute
Requests from one IP address600 a minute

Over a limit the answer is 429 rate_limited with a Retry-After header: the seconds until the current minute ends. The workspace's message limits are a different thing: they are the same as in Lalexi and shown by GET /usage (administrators only, like the Usage and Limits pages).

Starting a dialog

POST /dialogs writes first — what the New message button does. It works on channels whose capabilities.can_start_dialog is true: messengers (by phone number), e-mail (by address, with a subject) and WhatsApp Business (by number, with an approved template). Bots and marketplace accounts can only answer — 422 channel_reply_only.

  • The 60-second rule. On messenger channels the same text to the same number from the same channel does not go out twice within a minute — the answer is 409 duplicate. It guards against double submits; use Idempotency-Key for safe retries.
  • 202 with "status": "unknown" means the outcome did not come back in time: the message may have gone out. Do not resend it blindly — look for the conversation first (GET /conversations?phone=…). A repeat with the same Idempotency-Key returns the same unknown and does not send again.
  • 503 upstream_unavailable — nothing was sent, retrying is safe.
  • GET /dialogs/probe checks beforehand whether a number has an account on the channel's messenger, without sending anything.

Connecting a number

POST /channels with {"type": "whatsapp"} starts connecting a number — what the channel tile under Settings → Channels → Add does. The answer carries connect_url: a Lalexi page where the owner of the number scans the QR code, enters the code from the messenger or pastes the bot token. Open it in a browser, in a new tab or a pop-up. The link is short-lived: take a new one for every attempt.

When connecting is done, the channel appears in GET /channels with connected_by — you — and the channel.created webhook arrives. Any member of the workspace may connect numbers, as in Lalexi. Which types can be connected now, and what each can do: GET /channel-types.

  • Reconnecting. When a number's status becomes reconnect_required or disconnected, POST /channels/{id}/reconnect gives the same kind of page for the same account. The channel, its id and its conversations stay; once done, the status is connected and channel.status_changed arrives.
  • Disconnecting. POST /channels/{id}/disconnect unlinks the account. The channel and its conversations stay, with status: disconnected. A disconnected number can be hidden together with its conversations — PATCH /channels/{id} with {"hidden": true}; it is listed with include_hidden=true.
  • A label. PATCH /channels/{id} with {"note": "Sales desk"} — the team's label on the number.
  • The connection limit. When every connection of the plan is in use, the answer is 409 connection_limit_reached with details.limit and details.used.

An administrator manages any number, an agent their own (those they work in).

History and chat filter

A new number takes in all its chats from the moment it is connected. For the earlier correspondence, start POST /channels/{channel_id}/history-import; a GET on the same address shows how much is in. Running it again adds only what is missing.

Which of the number's chats come into the workspace is the filter's choice: PUT /channels/{channel_id}/chat-filter with mode — all, include (only chat_ids) or exclude (all but chat_ids). Take the chat ids from GET /channels/{channel_id}/chats: the list loads in the background; while status is loading, ask again in a second or two. Once a filter is saved, the history of the chats it lets through is imported on its own. POST /channels/{channel_id}/chat-filter/allow lets one more chat in — by number or id — without touching the rest.

Bots have no chat list and no history: these addresses answer 422 not_supported there (see the channel's capabilities.chat_list).

Broadcasts

POST /broadcasts creates a broadcast as a draft: nothing goes out until you call POST /broadcasts/{id}/schedule. Each row of items goes out from its own number (channel_id), one after another, with a delay_seconds pause (30–300 seconds) and not before scheduled_at. The number must be one you work in that can write first (capabilities.broadcasts).

Rows that cannot be sent — someone else's number, a bot, a bad phone, an empty text — stay out of the broadcast and are listed in errors by their position (index). When none is left, no broadcast is created — 422 invalid_items.

Then: GET /broadcasts/{broadcast_id}/items — the recipients and their statuses; PATCH /broadcasts/{broadcast_id}/items/{id} — another text or time for one recipient; POST /broadcasts/{id}/rephrase — AI variations of the text following the workspace's prompt (/broadcasts/rephrase-prompt); POST /broadcasts/{id}/cancel and DELETE /broadcasts/{id}. A workspace's broadcasts are visible to all its members, as in Lalexi. Changing one — starting, cancelling, deleting, editing recipients — is for an administrator, or for anyone whose own numbers the touched rows go out from (403 forbidden otherwise).

Your own messages

Everything written through the API — POST /conversations/{conversation_id}/messages and POST /dialogs — carries "via": "api" in answers and in webhooks. Pass your own external_id (up to 255 characters) and it comes back on the same message. That is how an integration tells its own messages from those written in Lalexi, on the phone or by the contact, and does not take them in twice.

Compatibility

Within v1 changes are additive only: new addresses, new optional request fields, new response fields, new webhook events, new error codes, new values in open lists (channel types, attachment types). Removing or renaming a field or changing its type happens only in a new version, /v2.

So your integration must skip fields and values it does not know, and handle an unknown error code by its HTTP status.

Specification

The whole API is one OpenAPI 3.1 document, and you can download it:

https://docs.lalexi.com/openapi/public-api.yaml

It is the same document the Reference is built from, so the two cannot drift apart. Use it to generate a client in your language, to load the schema into Postman or Insomnia, or to hand to a coding agent — then it builds requests from the description rather than from guesswork.

The same file is open in Swagger UI — every address on one page, the screen anyone who has worked with another API already knows. You can send a request from there too: press "Authorize", paste your token, open an address and "Try it out".

Generating a client, for example:

npx @openapitools/openapi-generator-cli generate \
  -i https://docs.lalexi.com/openapi/public-api.yaml \
  -g typescript-fetch -o ./lalexi-client

Next

On this page