Chat API (v1)

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:

StatusBodyMeaning
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/contacts uses limit instead of perPage, and its pagination object carries limit instead of perPage.
  • GET /api/v1/chat/conversations/{conversationId}/messages is cursor-paginated instead. Omit before for the latest page, then pass meta.before from each response to walk backwards until meta.hasMore is false. 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:

ItemsSent astitledescriptionvalue
1 to 3Reply buttonsup to 20 charactersnot shownup to 256 characters
4 to 10A list the contact opens with a buttonup to 24 charactersup to 72 charactersup 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:

KeyWhat it setsLimitDefault
buttonThe label of the button that opens the list20 charactersSelecionar
sectionTitleThe title above the options24 charactersOpçõ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.