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 URL | https://app.chatty.net |
| Auth header | X-Api-Key: <your key> |
| Format | JSON 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 Settings → General → Manage keys, then click Generate key.
| Rule | Detail |
|---|---|
| Format | sk_ followed by 32 hex characters |
| Key name | Required, up to 50 characters. Name it after the integration that will use it |
| Limit | Up to 5 active keys per store |
| Visibility | Shown 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. PassnextCursorback as thecursorquery parameter to fetch the next page.nextCursor: nullmeans you reached the last page, andhasMoresays 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" } }| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_PARAMS | Invalid parameter or body |
| 401 | UNAUTHENTICATED / INVALID_API_KEY | Missing, unknown, or revoked key |
| 403 | FORBIDDEN | Reserved for a future permission split on state, assign, read, and typing. Every key issued today carries full access, so you should not see this |
| 404 | NOT_FOUND | Resource does not exist, or belongs to another store |
| 409 | CHANNEL_UNAVAILABLE | The channel can't deliver this message right now |
| 429 | RATE_LIMITED | Rate 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
| Limit | Value |
|---|---|
| Per API key | 120 requests/min |
| Per source IP | 300 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
| Method | Path | Returns |
|---|---|---|
GET | /chat/conversations | List, newest activity first |
GET | /chat/conversations/{id} | Full conversation detail |
GET | /chat/conversations/{id}/related | Other conversations from the same customer. limit default 6, max 50. |
GET | /chat/conversations/{id}/browsed-pages | Pages the visitor viewed in this session |
GET | /chat/conversations/{id}/summary | AI summary of a resolved conversation |
POST | /chat/conversations/{id}/state | Resolve or reopen |
POST | /chat/conversations/{id}/assign | Assign to a team member. {"memberId": "..."} |
POST | /chat/conversations/{id}/read | Mark read on the agent side |
POST | /chat/conversations/{id}/typing | Agent 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
| Param | Example | Description |
|---|---|---|
status | open | open or resolved |
channel | whatsapp | online_store, email, facebook, instagram, whatsapp. Matches the channel the conversation started on, not every channel it later used. |
assigneeId | unassigned | Member id, or unassigned |
isRead | false | Read state on the agent side |
tag | pre-sale | Repeatable. A conversation matches if it carries any of the tags you pass. |
customerUuid | c_abc123 | One customer's conversations |
email | [email protected] | Customer email |
limit | 20 | Default 20, max 100. A larger value is clamped to 100, not rejected. |
cursor | eyJ... | 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
| Method | Path | Notes |
|---|---|---|
GET | /chat/conversations/{id}/messages | limit (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}/messages | Send 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
| Method | Path | Notes |
|---|---|---|
GET | /chat/tags | Your 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}/notes | Checklist note, text up to 150 characters |
PUT / DELETE | /chat/conversations/{id}/notes/{noteId} | Update / remove |
GET | /chat/conversations/{id}/attributes | Custom 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
| Method | Path | Returns |
|---|---|---|
GET | /chat/customers/{customerUuid} | Contact profile: identity, location, device, order stats, tags, custom attributes |
GET | /chat/customers/{customerUuid}/conversations | That customer's conversations. limit default 20, max 100, plus cursor |
GET | /chat/members | Team 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.
| Method | Path |
|---|---|
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_UNAVAILABLEon 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-pagesis session-scoped, capped at the 50 most recent pages, and kept for about 24 hours. Older conversations return an empty list.summaryreturns 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 .../messagesandPOST .../notescreate 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 .../attributesupdates the attribute when thenamealready 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
openback. - 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 MCPupdate_tagstool 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
textand 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'sfilesarray.
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.
Chatty Help Center