API Reference
API Reference
This page lists every endpoint of the RabtCRM API, the scopes each one needs, and the errors it returns. The base URL is https://rabtcrm.com/api/v1. Requests and responses are JSON.
The same contract is published as an OpenAPI 3.1 document at GET /api/v1/openapi.json.
On this page: Authentication · Scopes · Errors · Pagination · Rate limits · Idempotency · Suspended teams and plan features · Compatibility · AI agents (MCP) · Endpoints
Authentication
Send your API key in the Authorization header of every request:
Authorization: Bearer YOUR_API_KEY_HERE
Create keys in Settings → Developers (see Authentication). The examples below read the key from an environment variable:
export RABT_API_KEY="YOUR_API_KEY_HERE"
- All endpoints except
POST /api/v1/sendaccept only an API key. A logged-in dashboard session is not accepted, and a request without the header gets401 invalid_api_key. POST /api/v1/sendaccepts an API key with themessages:sendscope, or the dashboard session when noAuthorizationheader is sent.- An API key acts for its whole team. It sees what the team owner sees.
Scopes
Each key has a list of scopes. An endpoint runs only if the key has the scope it needs; otherwise it returns 403 insufficient_scope with the missing scope in details.requiredScope.
| Scope | Endpoints it allows |
|---|---|
instances:read | instances.list, instances.get |
contacts:read | tags.list, customFields.list, contacts.list, contacts.get |
contacts:write | contacts.update, contacts.import |
chats:read | chats.list |
messages:read | messages.list |
messages:send | messages.send, templates.list |
automations:read | automations.list, automations.get |
automations:write | automations.enable, automations.disable |
ai_agents:read | aiAgents.list, aiAgents.get |
ai_agents:write | aiAgents.update |
me.get needs no scope: any valid key can call it.
- Scopes do not imply each other. A
:writescope does not include the matching:readscope, andchats:readdoes not includemessages:read. Give the key every scope it needs. - Keys created before scopes existed have exactly
messages:send. In the key list they show as Send only (legacy). - Presets: Read-only selects the six
:readscopes. Full access selects all ten.
Setting scopes
- Go to Settings → Developers.
- When you create a key (Create New Key), pick its scopes under Access: check them one by one, or use Read-only or Full access. A key needs at least one scope.
- To change an existing key, click Edit access next to it, choose the scopes, and click Save. The change applies from the key's next request. This also works for legacy keys.
Errors
Every endpoint except POST /api/v1/send and the public openapi.json returns errors in this shape:
{
"error": {
"code": "insufficient_scope",
"message": "This API key needs the contacts:read scope.",
"details": { "requiredScope": "contacts:read" }
},
"requestId": "6f1c2a9e-3b7d-4c55-9a0e-2f8b1d4e7c10"
}
codeis stable; use it in your code.messageis for people and may change.detailsappears only for some codes.- Every response from these endpoints, success or error, carries an
X-Request-Idheader. On an error it has the same value asrequestId. Include it when you contact support.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_failed | A path parameter, query parameter or body field is invalid. details.issues, when present, lists each failing path and code (never the value you sent). |
| 400 | invalid_json | The body is not valid JSON. |
| 400 | invalid_reference | An id in the request belongs to something your team does not have. details.field, when present, names the field. |
| 400 | invalid_idempotency_key | The Idempotency-Key header is not 1 to 255 printable ASCII characters. |
| 401 | invalid_api_key | The key is missing, malformed or unknown. |
| 402 | team_suspended | The team is suspended. Writes are refused; reads still work. |
| 403 | insufficient_scope | The key lacks the scope in details.requiredScope. |
| 403 | feature_not_enabled | Your plan does not include the feature in details.feature. |
| 403 | contact_limit_reached | contacts.import would create more contacts than your plan allows. |
| 404 | not_found | The id does not exist in your team. Another team's ids also return 404. |
| 409 | idempotency_conflict | A request with this Idempotency-Key is still running. |
| 409 | import_in_progress | Another API import for your team is still running. |
| 409 | custom_fields_changed | A custom field was deleted or changed while the request ran. Nothing was written. |
| 409 | agent_uses_prompt_blocks | The AI agent's prompt is built by the setup wizard, so systemPrompt cannot be set through the API. |
| 415 | unsupported_media_type | The endpoint takes a JSON body and Content-Type is not application/json. |
| 422 | idempotency_key_reused | This Idempotency-Key was already used for a different request. |
| 422 | invalid_custom_value | A custom field value does not fit the field's type. |
| 429 | rate_limited | Too many requests. Wait the number of seconds in Retry-After. |
| 500 | internal_error | Something failed on our side. |
Every endpoint can return 401 invalid_api_key, 429 rate_limited and 500 internal_error. Every endpoint that needs a scope can also return 403 insufficient_scope. The endpoint sections below list only their other codes.
The checks run in this order, so you get the first one that fails: API key → scope → rate limit → suspended team (writes) → plan feature → path and query parameters → Content-Type → JSON → body fields.
POST /api/v1/send uses a different error shape: { "error": "<message>" }, or { "error": "<message>", "code": "<code>" } when there is a stable code. See messages.send.
Pagination
Lists return { "data": [...], "nextCursor": ... }.
contacts.list, chats.list and messages.list are paged:
| Query parameter | Rule |
|---|---|
limit | Items per page, 1 to 100. Default 50. |
cursor | The nextCursor value from the previous page. |
nextCursorisnullon the last page.- The cursor is opaque. Pass it back unchanged, with the same filters. A cursor the server cannot read returns
400 validation_failed.
The small lists (instances.list, tags.list, customFields.list, automations.list, aiAgents.list, templates.list) take no paging parameters. They return up to 500 items, and nextCursor is always null.
First page, then the next page with nextCursor from the previous response:
curl "https://rabtcrm.com/api/v1/contacts?limit=100" \
-H "Authorization: Bearer $RABT_API_KEY"
curl "https://rabtcrm.com/api/v1/contacts?limit=100&cursor=NEXT_CURSOR_VALUE" \
-H "Authorization: Bearer $RABT_API_KEY"
Rate limits
Limits are counted per API key, in 60-second windows:
| Requests | Limit |
|---|---|
Reads (GET) | 120 per minute |
Writes (POST, PATCH) | 60 per minute |
| contacts.import | 6 per minute, in addition to the write limit |
Every response from every endpoint except /send and /openapi.json carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | When the window resets, in Unix seconds |
Over the limit you get 429 rate_limited with a Retry-After header (seconds to wait). On contacts.import the headers describe the import limit, unless the write limit is the one you exceeded. A 403 insufficient_scope response does not count against the limit.
Failed authentication is limited to 20 per minute per IP address. Only requests with an Authorization: Bearer key that does not exist count. A valid key always authenticates, even from an IP address that is over this limit. Over the limit, a wrong key gets 429 rate_limited with Retry-After. On 401 and on this 429, the X-RateLimit-* headers describe the failed-authentication limit.
POST /api/v1/send does not use the per-key limits above:
- The team media limit is 5 per minute. It counts every
multipart/form-datarequest (checked before the body is read), and every JSON media send whosemediaUrlthis server downloads: on WhatsAppWHATSAPP-BAILEYSandWHATSAPP-BUSINESSinstances, Messenger and Instagram. AmediaUrlon a WhatsApp Cloud API (META-CLOUD) instance is passed to Meta and does not count, nor does a template header link. Over the limit you get429withRetry-AfterandX-RateLimit-*. This is the only/sendresponse that carriesX-RateLimit-*. - The provider's own
429(it rejected the send as too many) is passed through with noRetry-After. Back off before retrying. - The failed-authentication limit applies here too. Its
429carriesRetry-Afteronly. X-Request-Idis set only when you call with an API key and send anIdempotency-Key.
Idempotency
POST /api/v1/send and POST /api/v1/contacts/import accept an Idempotency-Key header. With it, retrying a request after a network error cannot send the message or run the import twice.
Idempotency-Key: order-1042-confirmation
- The key is 1 to 255 printable ASCII characters. Anything else returns
400 invalid_idempotency_key. - Keys are unique per team, across all of the team's API keys and both endpoints.
- On
/send, the header is used only when you call with an API key. A dashboard session call ignores it. - The key is used only after authentication, scope, rate limit and request validation pass. A request that fails those checks is not stored, and you can send it again with the same key. On contacts.import this also covers an unknown
instanceIdand409 import_in_progress. On/send, request validation means the body fields and the field-combination rules only: a later refusal (for example an unknown instance, or atextsend withoutmessage) is an outcome and is stored, except422 outside_service_windowand422 recipient_ambiguous(below).
What happens when you send the same key again:
| Situation | Response |
|---|---|
Same request (same endpoint and same body; on a multipart /send, also the same file) | The stored status and body, with Idempotent-Replayed: true and the original X-Request-Id. Nothing runs again. |
| Different request | 422 idempotency_key_reused |
| The first request is still running | 409 idempotency_conflict |
Every outcome is final for 24 hours, errors included, with five exceptions. The first response for a key (a success, a 4xx or a 5xx) is stored and replayed for 24 hours. A 504 from /send means the provider timed out, and the message may still have been delivered; replaying the key returns the same 504 and never sends again.
These five refusals happen before anything is sent or written, so they are not stored. Retry them with the same key after the condition clears:
429from the team media limit on a JSON/sendwhosemediaUrlthis server downloads. WaitRetry-Afterseconds.402for a team that is already suspended when the request arrives, on any/sendmessage: every instance type, templates, and Messenger or Instagram recipients. If the suspension takes effect while the send is running,META-CLOUDmessages, templates (onMETA-CLOUDandWHATSAPP-BUSINESS) and Messenger or Instagram messages return a402that is stored like any other outcome; text and media through aWHATSAPP-BAILEYSorWHATSAPP-BUSINESSinstance are not checked again and are sent.422 outside_service_windowon a free-form WhatsApp/send: retry with the same key once the customer has written, or send a template.422 recipient_ambiguouson a free-form WhatsApp/send: send to the customer's current phone number, or send a template.403 contact_limit_reachedon an import.
The media limit for a multipart upload is checked before the key is used, so it is not stored either.
To retry on purpose, use a new key. If the server could not store an outcome, the key keeps returning 409 until it expires. After 24 hours the key can be used again.
Suspended teams and plan features
- Suspended team: every write (
POSTandPATCH) returns402 team_suspended. Reads still work.POST /api/v1/sendreturns402in its own shape when the team is suspended or the storage limit is reached. On/sendthe suspension is checked after the request body is validated and before the instance or channel is looked up. - Plan features: some writes need a feature in your plan. Without it they return
403 feature_not_enabledwith the feature indetails.feature:
| Endpoint | Needs |
|---|---|
| automations.enable, automations.disable | The flow builder feature (isFlowBuilderEnabled) |
| aiAgents.update | The AI feature (isAiEnabled) |
Reading automations and AI agents does not need these features.
Compatibility
- Additive changes ship within v1 without notice: new endpoints, new optional request fields, new response fields, new error codes and new enum values. Your client must ignore response fields, error codes and enum values it does not know.
/sendand the WhatsApp 24-hour window:/sendused to answer200for a free-form message to aMETA-CLOUDorWHATSAPP-BUSINESScustomer outside the 24-hour window, and the message then failed. It now answers422 outside_service_windowand sends nothing. Branch oncode, not only on the status:422is alsoidempotency_key_reusedorrecipient_ambiguous.- A breaking change gets a new path version (
/api/v2/...), announced in advance. The v1 endpoint it replaces then sendsDeprecationandSunsetheaders until it is removed.
Use the API from AI agents (MCP)
RabtCRM runs a remote MCP (Model Context Protocol) server, so an AI agent such as Claude Code can call this API as tools.
- Endpoint:
https://rabtcrm.com/api/mcp, Streamable HTTP transport (JSON responses, no sessions). Protocol versions2025-11-25and2025-06-18. Tools only. - Authentication: the same team API keys, created in Settings → Developers, sent as
Authorization: Bearer YOUR_API_KEY_HEREon every request. A request without the header, or with an invalid key, gets401; after 20 failed attempts in a minute from one IP,429withRetry-After(see Rate limits).
Claude Code:
claude mcp add --transport http rabtcrm https://rabtcrm.com/api/mcp \
--header "Authorization: Bearer $RABT_API_KEY"
Then run /mcp in Claude Code to see the RabtCRM tools.
Other MCP clients work if they can send a custom Authorization header and send no Origin header. A request with an Origin header from another site gets 403, so MCP clients that run inside a web page cannot connect. These configurations are untested:
Cursor (.cursor/mcp.json), untested:
{
"mcpServers": {
"rabtcrm": {
"url": "https://rabtcrm.com/api/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY_HERE" }
}
}
}
VS Code (.vscode/mcp.json), untested:
{
"servers": {
"rabtcrm": {
"type": "http",
"url": "https://rabtcrm.com/api/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY_HERE" }
}
}
}
Both files hold your API key: keep them out of version control (add them to .gitignore, or put the server in your client's user-level configuration instead of the project folder).
Not supported yet: custom connectors in claude.ai and Claude Desktop, and ChatGPT connectors. They cannot send an API key; they need OAuth, which is coming in a later version.
Tools
- Each operation in Endpoints, except
openapi.json, is one tool. The tool name is the operation name with.replaced by_:me_get,contacts_list,contacts_update,aiAgents_update, and so on. Its arguments are the endpoint's path parameters, query parameters and body fields, in one object. messages_sendsends a message through messages.send. It needs themessages:sendscope. Media works only as a link (mediaUrl); file upload is not available through MCP.templates_listlists the templatesmessages_sendcan send (same scope,messages:send). Outside the WhatsApp 24-hour windowmessages_sendreturnsoutside_service_window; the agent then sends one of these templates.- The key's scopes decide which tools the agent sees.
me_getis always listed; every other tool appears only if the key has its scope (see Scopes). For analysis, use a key with the Read-only preset: it has no write or send tools. - A tool call that fails comes back as a tool result marked as an error, with the endpoint's error body (see Errors). Requests refused before any tool runs come back as JSON-RPC errors instead: an unknown tool or an
argumentsvalue that is not an object (-32602), a malformed request (-32600), and the HTTP errors above (401,403,429) and below (413). Formessages_send, a5xxcomes back as a short fixed message without the provider's details. After any5xxthe message may have been delivered: do not resend blindly; check the conversation or ask the user first. Calling again with the sameidempotencyKeywithin 24 hours does not send again (it returns the stored result); a new key, or no key, may send a duplicate.
Confirmation before writes and sends
Write and send tools (contacts_update, contacts_import, automations_enable, automations_disable, aiAgents_update, messages_send) are marked as destructive. Whether you are asked before they run depends on your client's approval settings. RabtCRM does not enforce a confirmation in this version. Turn on your client's approval for write and send tools, and do not add them to an auto-approve list.
Customer messages are untrusted input. Tool results include text your customers wrote, and such text can contain instructions aimed at the agent. Do not let an agent send messages or edit data because of something a message says without reading what each call does.
Limits, idempotency and audit
- Tool calls use the same scopes, rate limits and checks as the REST endpoints, and share the key's budgets: a tool call counts against the same per-key read or write limit as the matching REST request (see Rate limits).
messages_sendcounts against the key's write limit. Listing tools and the connection handshake have their own limit of 120 per minute per key. - An MCP request body is limited to 1 MB (1,048,576 bytes); a larger request gets HTTP
413before anything runs. Split largecontacts_importbatches. messages_sendandcontacts_importtake an optionalidempotencyKeyargument, sent as theIdempotency-Keyheader (see Idempotency). Reuse the same value only when retrying the exact same call. A keyed request's outcome is stored for 24 hours and returned again for the same key.- Successful writes appear in Settings → Activity "via API key" with the key's name, as REST writes do. An import's summary entry is written after its contacts are saved: if writing the entry fails, the import still succeeds. Sends are not listed there.
Endpoints
| Operation | Method and path | Scope |
|---|---|---|
| me.get | GET /me | none |
| instances.list | GET /instances | instances:read |
| instances.get | GET /instances/{id} | instances:read |
| tags.list | GET /tags | contacts:read |
| customFields.list | GET /custom-fields | contacts:read |
| contacts.list | GET /contacts | contacts:read |
| contacts.get | GET /contacts/{id} | contacts:read |
| contacts.update | PATCH /contacts/{id} | contacts:write |
| contacts.import | POST /contacts/import | contacts:write |
| chats.list | GET /chats | chats:read |
| messages.list | GET /chats/{id}/messages | messages:read |
| automations.list | GET /automations | automations:read |
| automations.get | GET /automations/{id} | automations:read |
| automations.enable | POST /automations/{id}/enable | automations:write |
| automations.disable | POST /automations/{id}/disable | automations:write |
| aiAgents.list | GET /ai-agents | ai_agents:read |
| aiAgents.get | GET /ai-agents/{id} | ai_agents:read |
| aiAgents.update | PATCH /ai-agents/{id} | ai_agents:write |
| templates.list | GET /templates | messages:send |
| messages.send | POST /send | messages:send |
| openapi.json | GET /openapi.json | public, no key |
Paths are relative to https://rabtcrm.com/api/v1. Every {id} is a whole number from 1 to 2147483647. Any other value returns 400 validation_failed.
me.get
GET /api/v1/me · Scope: none (any valid key)
Returns the team and the API key making the call. Use it to check a key and see its scopes.
Response
| Field | Type | Description |
|---|---|---|
team.id | integer | Your team's id |
team.name | string | Your team's name |
key.id | integer | The key's id |
key.name | string | The name you gave the key |
key.scopes | string array | The key's scopes. A legacy key shows ["messages:send"]. |
curl https://rabtcrm.com/api/v1/me \
-H "Authorization: Bearer $RABT_API_KEY"
{
"team": { "id": 42, "name": "Acme Store" },
"key": { "id": 7, "name": "Website", "scopes": ["contacts:read", "messages:send"] }
}
Errors: only the common ones (no 403).
instances.list
GET /api/v1/instances · Scope: instances:read
Lists your WhatsApp instances and your active Messenger and Instagram channels. WhatsApp instances come first, then channels, each ordered by id. Up to 500 items in total (past 500, the channels at the end are left out); nextCursor is always null.
Response: data holds two kinds of item, told apart by kind.
WhatsApp instance (kind: "whatsapp"):
| Field | Type | Description |
|---|---|---|
kind | string | "whatsapp" |
id | integer | Instance id. Use it in the instanceId filter of chats.list, the instance filter of contacts.list, and in contacts.import. |
instanceName | string | Pass this as instanceName to messages.send. |
displayName | string or null | The instance's display name |
integration | string | The connection type, for example META-CLOUD |
phoneNumber | string or null | The connected phone number |
lastConnectedAt | string (date-time) or null | When the instance last connected. This is not the live connection state. |
createdAt | string (date-time) |
Messenger or Instagram channel (kind: "channel"):
| Field | Type | Description |
|---|---|---|
kind | string | "channel" |
id | integer | Channel id. For a Messenger or Instagram recipient, pass it as a string in instanceName to messages.send. |
channelType | string | FACEBOOK-MESSENGER or INSTAGRAM-DM |
displayName | string or null | The channel's display name |
curl https://rabtcrm.com/api/v1/instances \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: only the common ones.
instances.get
GET /api/v1/instances/{id} · Scope: instances:read
Returns one WhatsApp instance, with the same fields as a kind: "whatsapp" item in instances.list. Messenger and Instagram channels are available only in the list.
| Path parameter | Description |
|---|---|
id | The WhatsApp instance id |
curl https://rabtcrm.com/api/v1/instances/12 \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: 400 validation_failed (invalid id), 404 not_found.
tags.list
GET /api/v1/tags · Scope: contacts:read
Lists your contact tags, ordered by id. Up to 500 items; nextCursor is always null.
| Field | Type | Description |
|---|---|---|
id | integer | Tag id. Use it in the tag filter of contacts.list and in tagIds of contacts.update. |
name | string | |
color | string or null |
curl https://rabtcrm.com/api/v1/tags \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: only the common ones.
customFields.list
GET /api/v1/custom-fields · Scope: contacts:read
Lists your contact custom fields, ordered by id. Up to 500 items; nextCursor is always null.
| Field | Type | Description |
|---|---|---|
id | integer | Field id. contacts.update takes values by this id. |
name | string | The label shown in the dashboard |
key | string | The key used in a contact's customData and in contacts.import |
type | string | The field type |
curl https://rabtcrm.com/api/v1/custom-fields \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: only the common ones.
contacts.list
GET /api/v1/contacts · Scope: contacts:read
Lists your contacts, most recently updated first. Paged (see Pagination).
| Query parameter | Description |
|---|---|
limit | 1 to 100, default 50 |
cursor | The previous page's nextCursor |
search | Free text matched against the name, phone and username |
tag | A tag id |
stage | A funnel stage id, or no_stage |
agent | An assigned user id, or unassigned |
department | A department id, or no_department |
instance | A WhatsApp instance id, or no_instance |
Any other query parameter returns 400 validation_failed.
Response: data is a list of contacts:
| Field | Type | Description |
|---|---|---|
id | integer | Contact id |
name | string | |
phone | string or null | The part of remoteJid before @: the phone number for WhatsApp, or the channel id (fb_..., ig_..., ...) for other channels. null for a WhatsApp contact whose number Meta hides, and for a website chat. |
remoteJid | string or null | The contact's chat id: a WhatsApp id, or a channel id such as fb_... (Messenger) or ig_... (Instagram) |
metaUsername | string or null | The customer's WhatsApp username, when Meta sent one (WhatsApp Cloud API only) |
instanceId | integer or null | The WhatsApp instance of the contact's chat |
notes | string or null | |
assignedUserId | integer or null | The team member assigned to the contact |
assignedDepartmentId | integer or null | |
funnelStageId | integer or null | |
tags | array | Each tag as { id, name, color } |
customData | object | Custom values, keyed by the custom field key (see customFields.list) |
createdAt | string (date-time) | |
updatedAt | string (date-time) |
curl "https://rabtcrm.com/api/v1/contacts?limit=50&tag=12&stage=no_stage" \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: 400 validation_failed (invalid limit, cursor or filter value, or an unknown query parameter).
contacts.get
GET /api/v1/contacts/{id} · Scope: contacts:read
Returns one contact, with the fields listed in contacts.list.
| Path parameter | Description |
|---|---|
id | The contact id |
curl https://rabtcrm.com/api/v1/contacts/1234 \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: 400 validation_failed (invalid id), 404 not_found.
contacts.update
PATCH /api/v1/contacts/{id} · Scope: contacts:write
Updates a contact's fields, tags and custom values. Send only the fields you want to change; fields you leave out stay as they are. The body must contain at least one field, and unknown fields are rejected.
| Body field | Type | Rule |
|---|---|---|
name | string | 1 to 100 characters. Cannot be null. |
notes | string or null | null clears the notes. |
assignedUserId | integer or null | A member of your team. null unassigns. |
assignedDepartmentId | integer or null | One of your departments. null clears. |
funnelStageId | integer or null | One of your funnel stages. null clears. |
tagIds | integer array | Up to 500 tag ids. Replaces the contact's tags; [] removes them all. Every id must be one of your tags. |
customFieldValues | object | Keys are custom field ids as strings (from customFields.list); values are a string, number, boolean or null. Only the fields you send change. null or "" clears a field. |
Custom value types: a boolean field takes true, false, "true" or "false"; other fields take a string or a number. Any other value returns 422 invalid_custom_value.
Response: the updated contact, as in contacts.list.
curl -X PATCH https://rabtcrm.com/api/v1/contacts/1234 \
-H "Authorization: Bearer $RABT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"funnelStageId": 3,
"tagIds": [12, 15],
"customFieldValues": { "8": "Gold", "9": true }
}'
Errors: 400 validation_failed / invalid_json, 400 invalid_reference (a user, department, stage, tag or custom field your team does not have), 402 team_suspended, 404 not_found, 409 custom_fields_changed, 415 unsupported_media_type, 422 invalid_custom_value.
contacts.import
POST /api/v1/contacts/import · Scope: contacts:write · Accepts Idempotency-Key
Creates or updates up to 200 contacts, matched by phone number. It works like the dashboard's contact import.
| Body field | Type | Rule |
|---|---|---|
instanceId | integer | Optional. One of your WhatsApp instance ids. Used only for chats the import creates. |
contacts | array | 1 to 200 rows |
Each row (unknown fields are rejected):
| Field | Type | Rule |
|---|---|---|
phone | string | Required, up to 32 characters |
name | string | Required, up to 255 characters |
notes | string | Up to 10,000 characters |
stage | string | A funnel stage name (not case-sensitive), up to 100 characters |
agentEmail | string | A team member's email (not case-sensitive), up to 255 characters |
department | string | A department name (not case-sensitive), up to 100 characters |
tags | string array | Up to 50 tag names, each up to 100 characters. Names are matched exactly (case-sensitive); tags that do not exist are created. |
customData | object | At most 50 keys. Keys are custom field keys (up to 100 characters); values are a string (up to 1,000 characters), number or boolean. |
How rows are applied:
- A row whose phone matches an existing contact updates it. Its
nameis replaced;notes,stage,agentEmailanddepartmentchange only when given and matched; custom values are merged; tags are added (existing tags stay). - Any other row creates a contact.
- A
stage,agentEmailordepartmentthat matches nothing is ignored. AcustomDatakey that is not one of your custom fields is ignored. For a boolean field,true,"true","yes","sim"and"1"mean true; anything else means false. On an existing contact,""for a text field clears it. - The same phone twice in one request: the first row is used and later rows count as duplicates.
- Only one API import per team runs at a time. A second one returns
409 import_in_progress. - Before writing anything, the import checks your plan: if it would create more contacts than your plan has left, it returns
403 contact_limit_reachedand writes nothing. - Rate limit: 6 imports per minute per key, on top of the write limit.
Response
| Field | Type | Description |
|---|---|---|
imported | integer | Rows created or updated |
failed | integer | Rows that could not be imported |
duplicates | integer | Rows skipped because their phone appeared earlier in the request |
failures | array | { row, reason } for failed and duplicate rows, at most 100 entries |
row is the item's position in contacts plus 2 (the first item is row 2), matching the dashboard's spreadsheet import, where row 1 is the header. reason is one of missing_name, missing_phone, invalid_phone, duplicate, save_error.
curl -X POST https://rabtcrm.com/api/v1/contacts/import \
-H "Authorization: Bearer $RABT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: import-2026-11-09-batch-1" \
-d '{
"instanceId": 12,
"contacts": [
{ "phone": "+201001234567", "name": "Sara Ali", "tags": ["VIP"], "customData": { "city": "Cairo" } },
{ "phone": "+966501234567", "name": "Omar Saleh", "stage": "Qualified" }
]
}'
{ "imported": 2, "failed": 0, "duplicates": 0, "failures": [] }
Errors: 400 validation_failed / invalid_json, 400 invalid_reference (instanceId), 400 invalid_idempotency_key, 402 team_suspended, 403 contact_limit_reached, 409 import_in_progress, 409 idempotency_conflict, 415 unsupported_media_type, 422 idempotency_key_reused. See Idempotency.
chats.list
GET /api/v1/chats · Scope: chats:read
Lists your direct-message conversations (comment threads are not included), the one with the most recent message first. Conversations with no messages are not listed. Paged (see Pagination).
| Query parameter | Description |
|---|---|
limit | 1 to 100, default 50 |
cursor | The previous page's nextCursor |
instanceId | Only chats of this WhatsApp instance |
Response: data is a list of chats:
| Field | Type | Description |
|---|---|---|
id | integer | Chat id. Use it in messages.list. |
name | string or null | |
remoteJid | string | The customer's id: a WhatsApp id, or a channel id such as fb_... (Messenger) or ig_... (Instagram). For Messenger and Instagram, pass it as number to messages.send. |
instanceId | integer or null | The WhatsApp instance, for WhatsApp chats |
channelConnectionId | integer or null | The connected channel (Messenger, Instagram, Telegram, website chat, ...), for chats that are not WhatsApp |
unreadCount | integer | |
lastMessageAt | string (date-time) |
curl "https://rabtcrm.com/api/v1/chats?limit=50&instanceId=12" \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: 400 validation_failed (invalid limit, cursor or instanceId, or an unknown query parameter), 400 invalid_reference (instanceId is not one of your instances).
messages.list
GET /api/v1/chats/{id}/messages · Scope: messages:read
Lists one conversation's messages, newest first. Paged (see Pagination).
| Parameter | Description |
|---|---|
id (path) | The chat id from chats.list |
limit (query) | 1 to 100, default 50 |
cursor (query) | The previous page's nextCursor |
Response: data is a list of messages:
| Field | Type | Description |
|---|---|---|
id | string | The provider's message id (for WhatsApp, the WAMID). Unique only within its chat. |
chatId | integer | |
fromMe | boolean | true when your side sent the message |
messageType | string or null | |
text | string or null | |
mediaCaption | string or null | |
mediaUrl | string or null | |
mediaMimetype | string or null | |
status | string or null | The delivery status |
timestamp | string (date-time) | |
isInternal | boolean | true for a note your team wrote inside the chat. The customer never saw it. |
eventType | string or null | Not null when the row is a system event recorded in the chat, not a message. The value names the event. |
isDeleted | boolean | true when the sender deleted the message |
To rebuild the conversation as the customer saw it, skip rows where isInternal is true or eventType is not null.
curl "https://rabtcrm.com/api/v1/chats/5678/messages?limit=100" \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: 400 validation_failed (invalid id, limit or cursor, or an unknown query parameter), 404 not_found.
automations.list
GET /api/v1/automations · Scope: automations:read
Lists your message automations, ordered by id. Up to 500 items; nextCursor is always null. The flow itself is not returned.
| Field | Type | Description |
|---|---|---|
id | integer | Automation id |
name | string | |
isActive | boolean | Whether the automation is on |
instanceId | integer or null | The WhatsApp instance it runs on |
channelConnectionId | integer or null | The connected channel (Messenger, Instagram, Telegram, ...) it runs on |
createdAt | string (date-time) | |
updatedAt | string (date-time) |
curl https://rabtcrm.com/api/v1/automations \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: only the common ones.
automations.get
GET /api/v1/automations/{id} · Scope: automations:read
Returns one message automation, with the fields listed in automations.list.
curl https://rabtcrm.com/api/v1/automations/31 \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: 400 validation_failed (invalid id), 404 not_found.
automations.enable
POST /api/v1/automations/{id}/enable · Scope: automations:write · Plan feature: flow builder
Turns a message automation on. No body. Returns the automation, as in automations.list.
curl -X POST https://rabtcrm.com/api/v1/automations/31/enable \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: 400 validation_failed (invalid id), 402 team_suspended, 403 feature_not_enabled, 404 not_found.
automations.disable
POST /api/v1/automations/{id}/disable · Scope: automations:write · Plan feature: flow builder
Turns a message automation off. No body. Returns the automation, as in automations.list.
curl -X POST https://rabtcrm.com/api/v1/automations/31/disable \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: 400 validation_failed (invalid id), 402 team_suspended, 403 feature_not_enabled, 404 not_found.
aiAgents.list
GET /api/v1/ai-agents · Scope: ai_agents:read
Lists your AI agents, ordered by id. Up to 500 items; nextCursor is always null. The agent's provider credentials are not returned.
| Field | Type | Description |
|---|---|---|
id | integer | Agent id |
name | string | |
description | string or null | |
persona | string or null | |
isDefault | boolean | |
isActive | boolean | |
provider | string | The AI provider |
model | string | The model name |
promptMode | string | manual when the prompt is written directly; otherwise the prompt is built by the setup wizard |
systemPrompt | string or null | |
temperature | number or null | |
maxOutputTokens | integer or null | |
ragEnabled | boolean | Whether knowledge retrieval is on |
createdAt | string (date-time) or null | |
updatedAt | string (date-time) or null |
curl https://rabtcrm.com/api/v1/ai-agents \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: only the common ones.
aiAgents.get
GET /api/v1/ai-agents/{id} · Scope: ai_agents:read
Returns one AI agent, with the fields listed in aiAgents.list.
curl https://rabtcrm.com/api/v1/ai-agents/4 \
-H "Authorization: Bearer $RABT_API_KEY"
Errors: 400 validation_failed (invalid id), 404 not_found.
aiAgents.update
PATCH /api/v1/ai-agents/{id} · Scope: ai_agents:write · Plan feature: AI
Updates an AI agent's basic settings. Send only the fields you want to change. The body must contain at least one field, and unknown fields are rejected.
| Body field | Type | Rule |
|---|---|---|
name | string | 1 to 100 characters |
description | string or null | null or "" clears it |
isActive | boolean | |
systemPrompt | string or null | Only for agents with promptMode manual. Otherwise the request returns 409 agent_uses_prompt_blocks and changes nothing. |
temperature | number | Between 0 and 2, in steps of 0.1 (for example 0.7; 0.25 is rejected) |
maxOutputTokens | integer | 1 to 100,000 |
Response: the updated agent, as in aiAgents.list.
curl -X PATCH https://rabtcrm.com/api/v1/ai-agents/4 \
-H "Authorization: Bearer $RABT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "isActive": true, "temperature": 0.5 }'
Errors: 400 validation_failed / invalid_json, 402 team_suspended, 403 feature_not_enabled, 404 not_found, 409 agent_uses_prompt_blocks, 415 unsupported_media_type.
templates.list
GET /api/v1/templates · Scope: messages:send
Lists your WhatsApp message templates, to send with messages.send (type: "template"). Archived templates are left out. Ordered by id, up to 500; nextCursor is always null.
| Query parameter | Description |
|---|---|
instanceId | Optional. Only this instance's templates. An id that is not one of your instances returns an empty list. |
Response: data holds one item per template.
| Field | Type | Description |
|---|---|---|
id | integer | Template id |
instanceId | integer | The instance the template belongs to |
instanceName | string | Pass this as instanceName to messages.send. |
name | string | Pass this as templateName. |
language | string | Pass this as templateLanguage, for example en_US. |
category | string | For example MARKETING or UTILITY |
status | string | Meta's review status. Only APPROVED templates can be sent. |
header | object or null | null when the template has no header. Otherwise format (TEXT, IMAGE, VIDEO, DOCUMENT or LOCATION), text (a text header's text, else null), needsVariable (a text header with {{1}}: send templateHeaderVariable) and needsMedia (send templateHeader with a mediaUrl). |
bodyText | string or null | The body text, with its variables as {{1}}, {{2}}, ... |
bodyVariableCount | integer | How many entries templateBodyVariables must have |
curl https://rabtcrm.com/api/v1/templates \
-H "Authorization: Bearer $RABT_API_KEY"
{
"data": [
{
"id": 31, "instanceId": 4, "instanceName": "main-store", "name": "order_shipped", "language": "en_US",
"category": "UTILITY", "status": "APPROVED", "header": null,
"bodyText": "Hi {{1}}, your order {{2}} has shipped.", "bodyVariableCount": 2
}
],
"nextCursor": null
}
To send it: "type": "template", "templateName": "order_shipped", "templateLanguage": "en_US", "templateBodyVariables": ["Sara", "#1042"].
Errors: 400 validation_failed (an invalid instanceId), 403 insufficient_scope (the key lacks messages:send), and the common ones.
messages.send
POST /api/v1/send · Scope: messages:send · Accepts Idempotency-Key
Sends a WhatsApp, Messenger or Instagram message. Legacy keys can call this endpoint. It also accepts the dashboard session when no Authorization header is sent.
This endpoint keeps its original behavior, which differs from the other endpoints:
- Errors are
{ "error": "<message>" }or{ "error": "<message>", "code": "<code>" }, not the envelope described in Errors. - It does not use the per-key rate limits, and sets
X-RateLimit-*only on the media-limit429(see Rate limits). X-Request-Idis set only when you call with an API key and send anIdempotency-Key.
Send Content-Type: application/json with the body below, or multipart/form-data with two parts: payload (the same JSON, as a string) and an optional file (up to 20 MB).
| Body field | Type | Rule |
|---|---|---|
instanceName | string | Required. The WhatsApp instanceName from instances.list. For a Messenger or Instagram recipient, the channel id as a string, for example "51". |
number | string | Required. The recipient's phone number, or for Messenger and Instagram the chat's remoteJid (fb_... or ig_...) |
type | string | Required. text, image, video, document, audio or template |
message | string | Required for text. With WhatsApp media, the caption. Not allowed with media on Messenger and Instagram (send the text as its own text message). |
mediaUrl | string (URL) | For media types: a link to the file. Do not combine with an uploaded file. |
fileName | string | |
mimetype | string | |
templateName | string | Required for template |
templateLanguage | string | Required for template |
templateHeaderVariable | string | template only |
templateBodyVariables | string array | template only |
templateHeader | object | template only: { type: "image" | "video" | "document", mediaUrl?, fileName? }. mediaUrl is required, except on a META-CLOUD instance, where a multipart request can upload the header media as file instead. WHATSAPP-BUSINESS refuses an uploaded header with 400: use mediaUrl. |
Rules:
- Media types need
mediaUrlor an uploadedfile, never both.textcannot have a file. - Template fields are allowed only with
type: "template". Templates work only on WhatsAppMETA-CLOUDandWHATSAPP-BUSINESSinstances. - Messenger and Instagram: the customer must message you first, and you can reply only within the platform's reply window. Outside it the response is
409with acodesuch asAUTOMATION_OUTSIDE_STANDARD_WINDOWandlastCustomerMessageAt. - WhatsApp
META-CLOUDandWHATSAPP-BUSINESS: a free-form message (text,image,video,document,audio) needs a message from the customer to this number in the last 24 hours. Otherwise the response is422withcode: "outside_service_window"andlastCustomerMessageAt(the customer's last message time, ornullwhen there is none since the number was connected), and nothing is sent. Send an approved template instead (templates.list); a template does not reopen the window. Meta may still refuse a send that races the window's end; that failure arrives later as a failed message status. - On these instances, the 24-hour check finds the customer's chat even when you write the number in another common form (Mexico
52/521, Brazil mobiles with or without the 9th digit, Argentina54/549and the national15), and through a hidden-number (BSUID) chat that WhatsApp linked to that number. If the only recent chat is reached through a link that points to more than one person (for example a BSUID linked to two phone numbers after a number change), a text or media send returns422recipient_ambiguousand nothing is sent; send to the customer's current phone number, or send a template.
Response (200), one of:
| Shape | When |
|---|---|
{ "success": true, "messageId": "...", "chatId": 123 } | WhatsApp Cloud API (META-CLOUD) sends and every template send |
{ "success": true, "messageId": "...", "chatId": 123, "channelType": "FACEBOOK-MESSENGER" } | Messenger and Instagram sends (channelType is FACEBOOK-MESSENGER or INSTAGRAM-DM) |
{ "success": true, "data": { ... } } | Other WhatsApp instances: the provider's response as returned; data.key.id is the message id |
messageId is the provider's message id. chatId is the RabtCRM chat the message was stored in.
curl -X POST https://rabtcrm.com/api/v1/send \
-H "Authorization: Bearer $RABT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-confirmation" \
-d '{
"instanceName": "main-store",
"number": "201001234567",
"type": "text",
"message": "Your order #1042 has shipped."
}'
Sending a file with multipart:
curl -X POST https://rabtcrm.com/api/v1/send \
-H "Authorization: Bearer $RABT_API_KEY" \
-F 'payload={"instanceName":"main-store","number":"201001234567","type":"document","fileName":"invoice.pdf"}' \
-F "[email protected]"
Errors (legacy shape):
| Status | Meaning |
|---|---|
| 400 | Invalid payload (details lists the field errors), a field combination the type does not allow, an invalid recipient, invalid_idempotency_key, a template that cannot be built, an uploaded template header on a WHATSAPP-BUSINESS instance, or a mediaUrl that cannot be downloaded |
| 401 | Missing or invalid API key |
| 402 | The team is suspended, or the storage limit is reached |
| 403 | insufficient_scope (the key lacks messages:send) or recipient_blocked |
| 404 | Instance, template or channel not found, or conversation_not_found (a Messenger or Instagram customer must message first) |
| 409 | idempotency_conflict, or a Messenger/Instagram reply-window refusal (with lastCustomerMessageAt) |
| 413 | The uploaded file is over 20 MB, or the media is over the channel's limit (error is media_too_large:<kind>:<MB>) |
| 415 | Content-Type is not application/json or multipart/form-data, or the media format is not supported (media_type_unsupported) |
| 422 | idempotency_key_reused, or outside_service_window: a free-form WhatsApp message while the 24-hour window is closed (with lastCustomerMessageAt; nothing is sent), or recipient_ambiguous: a free-form WhatsApp message whose only recent chat is linked to more than one person (nothing is sent) |
| 429 | Too many failed authentications from your IP address, or the team media limit (5 per minute; see Rate limits): wait Retry-After seconds. Or the provider rejected the send with 429, passed through without Retry-After. |
| 500 | Internal error, or the provider accepted the message but it could not be stored (it may have been delivered) |
| 502 | The provider failed or returned something unexpected |
| 504 | The provider timed out. The message may still have been delivered. |
| Other 4xx | The provider rejected the message; its status is passed through, with no Retry-After |
Do not resend blindly after a 500 or 504. Send with an Idempotency-Key so that a retry with the same key returns the stored outcome instead of sending again (see Idempotency).
openapi.json
GET /api/v1/openapi.json · Public, no key needed
Returns the OpenAPI 3.1 document for this API. It covers every other endpoint on this page, with request and response schemas. Each operation also carries x-required-scope. Responses may be cached for 5 minutes (Cache-Control: public, max-age=300).
curl https://rabtcrm.com/api/v1/openapi.json