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/v1Every 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.
| Situation | Answer |
|---|---|
| Several workspaces and no header | 400 workspace_required |
| Not your workspace | 404 workspace_not_found |
| The workspace is suspended | 403 workspace_suspended |
| API access is turned off for the workspace | 403 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.
code | HTTP | What happened |
|---|---|---|
invalid_json | 400 | The body is not valid JSON. |
workspace_required | 400 | Several workspaces and no X-Workspace-Id. |
invalid_api_key | 401 | The token is missing, unknown or not a personal token. |
forbidden | 403 | Your role does not allow this. |
workspace_suspended | 403 | The workspace is suspended. |
api_disabled | 403 | API access is turned off for the workspace. |
not_found | 404 | No such object, or one you may not see (the answer is the same); also any unknown path. |
workspace_not_found | 404 | Not a workspace of yours. |
contact_exists | 409 | A contact with this phone, e-mail or identifier exists — its id is in details.id. |
idempotency_in_progress | 409 | A request with this Idempotency-Key is still running. |
duplicate | 409 | The same dialog start (channel, recipient, text) less than a minute ago. |
message_limit_reached | 409 | The workspace's message limit is used up — details.limit, details.used. |
channel_disconnected | 409 | The channel is not connected right now. |
validation_failed | 422 | The request is invalid; details names the fields. |
invalid_cursor | 422 | The cursor is damaged. |
idempotency_key_reused | 422 | This Idempotency-Key was used with a different request. |
identifier_read_only | 422 | A messenger contact's identifier cannot be changed. |
invalid_assignee | 422 | That person does not work in the conversation's channel. |
invalid_team | 422 | No such team in the workspace. |
attachment_invalid | 422 | A file is too large or cannot be stored. |
channel_reply_only | 422 | The channel can only answer people who wrote first. |
channel_cannot_start | 422 | The channel cannot start dialogs through the API. |
probe_not_supported | 422 | Number checks work on messenger channels only. |
template_not_found | 422 | Not an approved WhatsApp Business template of this channel. |
invalid_recipient | 422 | Not a valid recipient for the channel. |
recipient_rejected | 422 | The messenger refused this recipient. |
send_rejected | 422 | The messenger refused the message. |
rate_limited | 429 | Over a rate limit — wait Retry-After seconds. |
internal_error | 500 | Something went wrong on our side. |
upstream_unavailable | 503 | The 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 counted | Limit |
|---|---|
| Requests with one token | 300 a minute |
POST /dialogs with one token | 20 a minute |
GET /dialogs/probe with one token | 30 a minute |
POST /webhooks/{id}/test with one token | 5 a minute |
POST /channels and POST /channels/{id}/reconnect with one token, together | 10 a minute |
Queued messenger work with one token, together: POST …/history-import, PUT …/chat-filter, POST …/chat-filter/allow, POST /broadcasts, POST /broadcasts/{id}/rephrase | 10 a minute |
| Requests from one IP address | 600 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; useIdempotency-Keyfor safe retries. 202with"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 sameIdempotency-Keyreturns the sameunknownand does not send again.503 upstream_unavailable— nothing was sent, retrying is safe.GET /dialogs/probechecks 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
statusbecomesreconnect_requiredordisconnected,POST /channels/{id}/reconnectgives the same kind of page for the same account. The channel, its id and its conversations stay; once done, the status isconnectedandchannel.status_changedarrives. - Disconnecting.
POST /channels/{id}/disconnectunlinks the account. The channel and its conversations stay, withstatus: disconnected. A disconnected number can be hidden together with its conversations —PATCH /channels/{id}with{"hidden": true}; it is listed withinclude_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_reachedwithdetails.limitanddetails.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.yamlIt 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-clientNext
Webhooks
Events, the delivery format, checking the signature, retries.
Swagger UI
Every address on one page: schemas, examples and a form to send a request.
Reference
Every API address: parameters, request and response bodies, errors — and a form to send a request.
Bitrix24
If your CRM is Bitrix24, the conversation can come to it through a ready-made integration instead of being built on the API.