The chat inbox — conversations, messages, contacts, and teams — is served by a v1
API under /api/v1/chat/. It predates the v2 conventions and does not share them. Read
this page once before you write against it; everything else in the API Reference describes
v2.
Base URL and authentication
curl -X GET "https://agent-studio.seeyu.ai/api/v1/chat/conversations?workspaceId=WORKSPACE_ID" \
-H "X-API-Key: YOUR_API_KEY"Authentication is the same X-API-Key header as the rest of the API — see
Authentication.
Every operation names a workspace
workspaceId is a required query parameter on every read and a required body field on
every write. There is no implicit workspace: a request without it is a 400.
A workspace-scoped API key can only ever reach its own workspace. If a request names a
different workspaceId, the API answers 403 before any query runs. See
Workspace scoping for the full rules and
the three distinct 403 messages.
Because resources are always resolved inside workspaceId, a conversation, contact, or
team that lives in another workspace is reported as 404 not found rather than as denied —
the API is never an existence oracle for a workspace you cannot reach.
Responses are bare objects, not { data }
v2 wraps every success in { "data": ... }. v1 does not:
{ "conversation": { "id": "conv_...", "status": "open" } }{ "conversations": [ ... ], "pagination": { "page": 1, "perPage": 25, "total": 42, "totalPages": 2 } }Errors are flat
v2 answers { "error": { "code": "...", "message": "..." } }. v1 answers a flat object
whose only guaranteed field is error:
| Status | Body | Meaning |
|---|---|---|
400 | { error, details } | The request failed validation. details lists the failing fields. |
401 | { error } | The X-API-Key header is missing, malformed, expired, or revoked. |
403 | { error } | The key may not reach this workspace. |
404 | { error } | No such resource in this workspace. |
409 | { error, code, ... } | The request collides with existing state. See below. |
429 | { error, message, retryAfter } | Rate limited. Prefer the Retry-After header, which is in seconds. |
500 | { error, requestId } | Unexpected server error. Quote requestId in a support request. |
Branch on the HTTP status and, for a 409, on code. The error string is
human-readable and is not a stable contract.
Recovering from a 409
Two operations can conflict, and both hand you the blocking record so you can reuse it instead of retrying.
POST /api/v1/chat/conversations → code: "active_conversation_exists". The contact
already has a conversation in progress in that inbox. The conversation field carries it
— continue the thread there. Conversations in the contact's other inboxes do not block.
POST /api/v1/chat/contacts → code: "PHONE_ALREADY_EXISTS". Another contact in the
workspace already holds that phone number, normalized to digits. The response carries
contactId, contactName, phone, and conversationId — the existing contact's most
recently active conversation, or null.
Pagination
Most v1 chat lists page by offset, with page and perPage (both integers; perPage
caps at 100), and return a pagination object:
{ "page": 2, "perPage": 25, "total": 130, "totalPages": 6 }Offset pagination is not a stable snapshot. Conversations are ordered by most recent
activity, so a conversation whose activity changes between two requests can appear on two
pages or on none. For a consistent sweep, filter to a fixed status or process pages
back to front.
Two endpoints differ, both preserved from the original release:
GET /api/v1/chat/contactsuseslimitinstead ofperPage, and itspaginationobject carrieslimitinstead ofperPage.GET /api/v1/chat/conversations/{conversationId}/messagesis cursor-paginated instead. Omitbeforefor the latest page, then passmeta.beforefrom each response to walk backwards untilmeta.hasMoreisfalse. Messages come back oldest-first within a page.
Interactive WhatsApp messages
On a WhatsApp inbox, a message can give the contact options to tap instead of a reply to
type. Send them as contentAttributes.items on
Send Message, or on the first message of
Create Conversation:
curl -X POST "https://agent-studio.seeyu.ai/api/v1/chat/conversations/CONVERSATION_ID/messages" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workspaceId": "WORKSPACE_ID",
"content": "Selecione o setor com o qual deseja falar:",
"contentAttributes": {
"button": "Menu",
"sectionTitle": "Setores",
"items": [
{ "title": "Financeiro", "value": "1" },
{ "title": "Comercial", "value": "2" },
{ "title": "Secretaria", "value": "3" },
{ "title": "Livraria", "value": "4" }
]
}
}'content is the text shown above the options. Each item is { title, value, description? }:
the contact sees title, and value comes back when they tap it, so give every item a
different value. How many items you send decides how they appear:
| Items | Sent as | title | description | value |
|---|---|---|---|---|
| 1 to 3 | Reply buttons | up to 20 characters | not shown | up to 256 characters |
| 4 to 10 | A list the contact opens with a button | up to 24 characters | up to 72 characters | up to 200 characters |
A list sends the first 10 items and drops the rest. Two more keys set its labels, and reply buttons ignore both:
| Key | What it sets | Limit | Default |
|---|---|---|---|
button | The label of the button that opens the list | 20 characters | Selecionar |
sectionTitle | The title above the options | 24 characters | Opções |
A title, description or label over its limit is cut to fit rather than rejected, and a
blank label falls back to its default. A value is sent as is, so keep it within its limit.
Leave contentType at its default: WhatsApp reads items whatever the type.
The contact's tap arrives as an incoming message whose content is the option's title.
Its contentAttributes.interactive holds WhatsApp's reply, with your value as
button_reply.id or list_reply.id. Match on that value rather than on the title.
Options are a free-form message, so WhatsApp only delivers them inside the 24-hour window that opens each time the contact writes to you. Outside it, start with an approved template.
Rate limits
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and
X-RateLimit-Reset. A 429 adds Retry-After in seconds — add jitter rather than
retrying at exactly that offset. Limits are per plan and shared with the rest of the API.