API Reference
Chat Conversations API
💬Get free consultation

Chat Conversations API

Who can use this feature?

  • You need the Pro or Plus plan to create and manage API keys.
  • Full read and write access: read conversations and messages, send replies, resolve, assign, tag, and annotate.

Overview

The Chat Conversations API lets you work with your store's live chat from your own systems. Mirror conversations into a helpdesk, build a custom inbox, run reporting, or reply from a tool your team already uses.

Base URLhttps://app.chatty.net
Auth headerX-Api-Key: <your key>
FormatJSON in, JSON out (Content-Type: application/json on writes)

Every request is scoped to your store. You never send a store ID. Your key resolves to one store, and every read and write is limited to it automatically.

!

Don't copy paths out of the Chatty inbox in your browser. The admin inbox calls a near-identical set of paths under /api/chat/..., and those are authenticated with an admin session rather than an API key. An X-Api-Key request to /api/chat/conversations fails. The public API carries no /api prefix: https://app.chatty.net/chat/conversations.


Get your API key

Go to SettingsGeneralManage keys, then click Generate key.

RuleDetail
Formatsk_ followed by 32 hex characters
Key nameRequired, up to 50 characters. Name it after the integration that will use it
LimitUp to 5 active keys per store
VisibilityShown once, at creation
!

Copy your key right away. We only store a hash of it, so it can never be displayed again. Put it in your secrets manager immediately. If you lose it, delete the key and create a new one.

Create one key per integration so you can revoke them individually. Check that a key works:

curl -s "https://app.chatty.net/chat/conversations?limit=1" \
  -H "X-Api-Key: sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

A wrong or revoked key returns 401 INVALID_API_KEY.

Rotating a key: create the new key, deploy it, then delete the old one. Deletion takes effect within about 5 minutes.


Response format

Every successful response carries a data key. Paginated endpoints add paging, and a few reads add meta.

{
  "data": [ "..." ],
  "paging": { "nextCursor": "eyJ...", "hasMore": true },
  "meta": { "source": "postgresql" }
}
  • data: the resource, or an array of resources on list endpoints. Always present.
  • paging: on the paginated endpoints only, conversations list, related, customer conversations, and messages list. Pass nextCursor back as the cursor query parameter to fetch the next page. nextCursor: null means you reached the last page, and hasMore says the same thing as a boolean. Cursors are opaque, so don't parse them.

Every paginated endpoint follows the same two rules. A limit above the maximum is clamped down rather than rejected, and a limit that isn't a whole number of 1 or more returns 400 INVALID_PARAMS. A cursor Chatty cannot decode returns 400; a hand-built one is not checked against the query it claims to continue, so pass back only what paging.nextCursor gave you.

  • meta.source: informational only. The response shape never depends on it, so you can ignore it.

Errors

{ "error": { "code": "NOT_FOUND", "message": "Conversation not found" } }
StatusCodeMeaning
400INVALID_PARAMSInvalid parameter or body
401UNAUTHENTICATED / INVALID_API_KEYMissing, unknown, or revoked key
403FORBIDDENReserved for a future permission split on state, assign, read, and typing. Every key issued today carries full access, so you should not see this
404NOT_FOUNDResource does not exist, or belongs to another store
409CHANNEL_UNAVAILABLEThe channel can't deliver this message right now
429RATE_LIMITEDRate limit exceeded

Requesting a conversation that belongs to another store returns the same 404 as one that doesn't exist. This is deliberate. It keeps stores from probing each other.

Rate limits

LimitValue
Per API key120 requests/min
Per source IP300 requests/min

Every response carries X-RateLimit-Limit and X-RateLimit-Remaining, so you can throttle before you hit the wall.

The counter is not a window you can keep calling through. Every request pushes its 60-second expiry back, so once you are over the limit you have to stop for a full 60 seconds before the count resets. Retrying straight away keeps you blocked.


Conversations

MethodPathReturns
GET/chat/conversationsList, newest activity first
GET/chat/conversations/{id}Full conversation detail
GET/chat/conversations/{id}/relatedOther conversations from the same customer. limit default 6, max 50.
GET/chat/conversations/{id}/browsed-pagesPages the visitor viewed in this session
GET/chat/conversations/{id}/summaryAI summary of a resolved conversation
POST/chat/conversations/{id}/stateResolve or reopen
POST/chat/conversations/{id}/assignAssign to a team member. {"memberId": "..."}
POST/chat/conversations/{id}/readMark read on the agent side
POST/chat/conversations/{id}/typingAgent typing indicator. {"memberId": "...", "active": true}. Nothing on the server expires it. The inbox that receives it hides the indicator after a few seconds, so send {"active": false} when the agent stops typing rather than relying on that.

List filters

ParamExampleDescription
statusopenopen or resolved
channelwhatsapponline_store, email, facebook, instagram, whatsapp. Matches the channel the conversation started on, not every channel it later used.
assigneeIdunassignedMember id, or unassigned
isReadfalseRead state on the agent side
tagpre-saleRepeatable. A conversation matches if it carries any of the tags you pass.
customerUuidc_abc123One customer's conversations
email[email protected]Customer email
limit20Default 20, max 100. A larger value is clamped to 100, not rejected.
cursoreyJ...From paging.nextCursor

{id} accepts either form of conversation id. The response carries all of them, id, firestoreId, and pgConvoId, so you can match a conversation against records you stored earlier.

The last four rows are the only endpoints that check for a write permission on your key. Every key you can create today carries it, so any valid key works on every endpoint on this page. See What a key can and cannot do.


Messages

MethodPathNotes
GET/chat/conversations/{id}/messageslimit (default 50, max 100), cursor, order (asc or desc, default asc)
GET/chat/conversations/{id}/messages/{messageId}Accepts the numeric id or the message UUID
POST/chat/conversations/{id}/messagesSend a reply, an internal note, or a message as the customer. memberId is required unless you pass asCustomer. text up to 5000 characters. Returns 201.

List endpoints exclude deleted messages. The single-message endpoint returns them with "isDeleted": true, so you can reconcile your own copy.

Editing and deleting messages is not available on this API. Those actions stay in the Chatty inbox.


Tags, notes, and attributes

MethodPathNotes
GET/chat/tagsYour store's tag catalog
POST/chat/conversations/{id}/tags{"tags": ["vip"]}. Appends, deduped, returns the resulting list. Tags over 100 characters are dropped from the request rather than rejected.
DELETE/chat/conversations/{id}/tags/{tag}URL-encode the tag; idempotent
POST/chat/conversations/{id}/notesChecklist note, text up to 150 characters
PUT / DELETE/chat/conversations/{id}/notes/{noteId}Update / remove
GET/chat/conversations/{id}/attributesCustom key/value pairs
POST/chat/conversations/{id}/attributes{"name": "...", "value": "..."}. Upserts by name. name up to 255 characters, value up to 1000.
PUT / DELETE/chat/conversations/{id}/attributes/{attributeId}Update / remove

Customers and team members

MethodPathReturns
GET/chat/customers/{customerUuid}Contact profile: identity, location, device, order stats, tags, custom attributes
GET/chat/customers/{customerUuid}/conversationsThat customer's conversations. limit default 20, max 100, plus cursor
GET/chat/membersTeam members. Resolve memberId and assignee ids against this list
GET/chat/members/{memberId}One member

Member records are sanitized: credentials, tokens, and notification internals are never returned.


Webhooks

Manage subscriptions with the same X-Api-Key.

MethodPath
GET/chat/webhooks
POST/chat/webhooks
PUT/chat/webhooks/{id}
DELETE/chat/webhooks/{id}

See Webhooks for events, payloads, and signature verification.


Common recipes

List open conversations, then page through

curl -s "https://app.chatty.net/chat/conversations?status=open&limit=50" \
  -H "X-Api-Key: $CHATTY_KEY"
 
curl -s "https://app.chatty.net/chat/conversations?status=open&limit=50&cursor=$NEXT_CURSOR" \
  -H "X-Api-Key: $CHATTY_KEY"

Read a full thread, oldest first

curl -s "https://app.chatty.net/chat/conversations/$CONVO_ID/messages?order=asc&limit=100" \
  -H "X-Api-Key: $CHATTY_KEY"

Reply as an agent

curl -s -X POST "https://app.chatty.net/chat/conversations/$CONVO_ID/messages" \
  -H "X-Api-Key: $CHATTY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Your order ships tomorrow.", "memberId": "MEMBER_ID"}'

memberId must be an active member from GET /chat/members. An active AI bot hands off to the human. If auto-assignment is switched on in your inbox settings, the conversation is also assigned to that member, unless the AI agent is still holding it. Delivery to Messenger, Instagram, WhatsApp, or email happens automatically.

!

Sending a message fires a message.created webhook. If your integration both listens to message.created and replies through this endpoint, it will answer its own reply and loop. Check data.senderType and act only on customer.

Add an internal note

Never delivered to the customer. Works on any channel and in any state, and it doesn't hand the conversation off from the AI. It does mark the conversation as read on the agent side.

curl -s -X POST "https://app.chatty.net/chat/conversations/$CONVO_ID/messages" \
  -H "X-Api-Key: $CHATTY_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Refund approved by finance.", "memberId": "MEMBER_ID", "isNote": true}'

Relay an inbound message as the customer

For example, from your own channel bridge. Send asCustomer: true; memberId and isNote are ignored in that mode.

curl -s -X POST "https://app.chatty.net/chat/conversations/$CONVO_ID/messages" \
  -H "X-Api-Key: $CHATTY_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Where is my order?", "asCustomer": true}'

Resolve a conversation

curl -s -X POST "https://app.chatty.net/chat/conversations/$CONVO_ID/state" \
  -H "X-Api-Key: $CHATTY_KEY" -H "Content-Type: application/json" \
  -d '{"status": "resolved"}'

Tag it

curl -s -X POST "https://app.chatty.net/chat/conversations/$CONVO_ID/tags" \
  -H "X-Api-Key: $CHATTY_KEY" -H "Content-Type: application/json" \
  -d '{"tags": ["vip", "refund-request"]}'

Things worth knowing

  • The list only covers conversations that have moved to the new storage. Older conversations that have not been migrated yet are still reachable by id, and so are their messages, but they won't show up in GET /chat/conversations. Don't treat the list as a full history of your store.
  • Freshness. A read issued milliseconds after a live send may not include the newest message yet. Webhooks are the reliable way to react in real time.
  • 409 CHANNEL_UNAVAILABLE on send means the channel can't deliver: no recipient, no customer email, or you're outside the platform's 24-hour messaging window on Messenger, Instagram, or WhatsApp. An internal note (isNote: true) is always accepted.
  • browsed-pages is session-scoped, capped at the 50 most recent pages, and kept for about 24 hours. Older conversations return an empty list.
  • summary returns 404 until the AI has generated a summary for that conversation. Summaries are normally produced when a conversation is resolved.
  • Repeating a write is not always safe. There is no idempotency key. POST .../messages and POST .../notes create another message or note every time you call them, so a retry after a timeout can double-post. The rest absorb a repeat: adding tags appends and dedupes, removing a tag is a no-op when it isn't there, POST .../attributes updates the attribute when the name already exists, and setting a state you are already in returns that same state.
  • Deletes are permanent. Removing a note, an attribute, a tag, or a webhook subscription has no undo and no trash. Resolving a conversation is the one write you can reverse, by posting open back.
  • A tag longer than 100 characters is dropped from the write rather than rejected. If every tag in the request is too long, nothing is left to write and the call returns 400. There is no cap on how many tags one conversation can hold on this endpoint. The 100-character limit is specific to this API: the MCP update_tags tool silently truncates every tag to 50 characters and keeps at most 100 tags, so the same tag written through both surfaces can land twice under two different names.
  • You can only send text. The message body takes text and nothing else, so this API cannot attach a file. Files sent from the widget or an email channel still come back on reads, in the message's files array.

Need help?

Contact the Chatty support team from your dashboard. Include the endpoint, the timestamp of a failing request, and the returned error.code.

To read contacts and customer data instead of conversations, see the GraphQL Customer API.