Messaging API
Read the team inbox and write reply drafts with the Cooledge API. Covers the messages:read and messages:draft scopes, listing and reading conversations, creating, updating and deleting drafts, the recipient rule, the draft caps and how to follow a draft after a person sends it.
Messaging API
The messaging endpoints let an integration or an AI agent read a business's conversations and write replies as drafts. A draft waits in its conversation in Cooledge until a person on the team reviews it and sends or schedules it. Nothing in this API sends a message. There is no send or schedule endpoint. No scope grants one.
Every path below is relative to the API base URL https://app.cooledge.com.au/api, so /v1/conversations is the full URL https://app.cooledge.com.au/api/v1/conversations. Authenticate every request with your key in the X-Api-Key header, as covered in Getting Started. The same operations are available to AI assistants as MCP tools.
Scopes
| Scope | What it allows |
|---|---|
messages:read | List and read conversations, their messages and calls and the drafts waiting in them. Also needed to subscribe to the message webhooks. |
messages:draft | Create, update and delete drafts this credential wrote. It never sends. |
Neither scope is part of any existing key or connection. Keys created before these scopes existed gain nothing. An OAuth app that connects without asking for particular scopes gets the default set, which leaves both out. On the key screen in Settings, tick the Messages row (Read and Write drafts (never sends)) when you create the key. The Full access and Read-only presets do not include it. See Authentication.
How the API sees the inbox
A key or connected app acts as the business and sees what an owner sees, conversations from unmatched senders included. It is not a person, so the per-person parts of the inbox (unread, Mine, who has seen a conversation) are not on the API. Filter by state and by whether a conversation is waiting on a reply instead.
List conversations
GET /v1/conversations returns conversations newest activity first. Paging works as everywhere else in the API: limit (1 to 100, default 25) and the opaque next_cursor. See Pagination and errors.
| Parameter | Values | Notes |
|---|---|---|
state | open (default), snoozed, archived | Trash and spam are never listed. |
channel | sms, email, voice, calls | Only conversations with a message on that channel. voice is voicemail. calls is calls and voicemail. |
search | text, up to 200 characters | Matches the customer's name, phone or email, a contact's email or an unknown sender's address. It does not search message text. |
unreplied | true or false | With true, only conversations whose latest inbound message has no reply after it. |
unmatched | include (default), exclude, only | Conversations from senders not matched to a customer. |
With unreplied=true the list ends at the last conversation waiting on a reply, so next_cursor is null on the page where they run out. Paging stops at an offset of 10,000, where next_cursor is null. Narrow the filters to reach further back.
curl "https://app.cooledge.com.au/api/v1/conversations?unreplied=true&limit=10" \
-H "X-Api-Key: ck_live_AB12CD_your_secret_here"
{
"data": [
{
"id": "8f3a1c52-6d47-4b9e-a210-5c7e9d2b4f18",
"customer": { "id": "c4a9e1b2-7d3f-4a8c-9e2b-1f6a0d5c3b7e", "name": "Priya Nair" },
"sender_address": null,
"is_unmatched": false,
"state": "open",
"snoozed_until": null,
"assignee": { "id": "5b2e7d90-1c34-4f8a-9e61-0d3c7a2b5e49", "name": "Jess Taylor" },
"last_message_at": "2026-10-11T03:21:00.000Z",
"last_inbound_at": "2026-10-11T03:21:00.000Z",
"last_reply_at": "2026-10-10T22:05:12.000Z",
"is_unreplied": true,
"latest_message": {
"id": "2d6b9e14-7a35-4c80-b1f2-9e4a6c3d8b57",
"direction": "in",
"channel": "sms",
"preview": "Hi, can you come Thursday instead? The unit is still making that noise.",
"at": "2026-10-11T03:21:00.000Z"
},
"draft_count": 0
}
],
"next_cursor": null
}
draft_count counts the drafts waiting in the conversation, from people and from apps. sender_address is an unknown sender's phone number or email address. It is null once the conversation is matched to a customer.
Read a conversation
GET /v1/conversations/{id} returns one conversation in the same shape, plus waiting_drafts: the drafts waiting in it, oldest first. A read by id returns the conversation in any state, so it can return one the list hides, with state of trashed or spam. Another business's id is a 404.
Read the messages
GET /v1/conversations/{id}/messages returns the messages and calls in a conversation, newest last, as a thread reads. The page size defaults to 30 (up to 100). next_cursor pages back to older messages.
typeismessage,callorcomment. Internal team comments are left out unless you passinclude=comments.channelissms,email,voice(a voicemail) orcall.htmlis an email's HTML. It is null for anything else. Inbound HTML is the sender's content. Treat it as untrusted and sanitise it before you render it anywhere.attachmentslists each file'sid, name, content type and size only. Inline images are left out. Downloading files through the API is not available yet.callcarries a call'soutcomeandduration_seconds. It is null for anything else.sent_byis the team member who sent the message or wrote the comment.
Drafts
Create a draft
POST /v1/drafts saves an SMS or email draft. It needs messages:draft.
| Field | Required | Notes |
|---|---|---|
conversation_id | One of these two | The conversation to draft into. |
customer_id | One of these two | A customer, for a first message when they have no conversation yet. Pass one or the other, not both. |
channel | Yes | sms or email. |
contact_id | No | One of the customer's contacts, to draft to them instead of the customer. |
text | SMS: yes | SMS: the message, up to 459 characters after surrounding spaces are trimmed. Email: the plain text body. |
subject | No | Email only. |
html | No | Email only, instead of text. Up to 100 KB. It is sanitised with the same rules as an email a person sends before it is stored. |
reply_to_message_id | No | Email only. An email in the same conversation to reply to. With no subject, the subject becomes Re: and the original subject. |
You never choose the recipient. Cooledge resolves it from the conversation's own party: the contact's phone or email when you pass contact_id, otherwise the customer's own phone or email, or for an unmatched conversation the sender's own address (the channel must match it, so an SMS draft needs a phone number). The body is strict: to, cc, bcc, attachments, send_at, status, kind or any other field not in the table is a 400. An SMS draft with subject, html or reply_to_message_id is a 400 too.
curl https://app.cooledge.com.au/api/v1/drafts \
-H "X-Api-Key: ck_live_AB12CD_your_secret_here" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "8f3a1c52-6d47-4b9e-a210-5c7e9d2b4f18",
"channel": "sms",
"text": "Hi Priya, Thursday works. We can be there between 8 and 10 am. Does that suit?"
}'
A created draft answers 201 with the draft and a Location header pointing at /v1/drafts/{id}:
{
"id": "e71c4b28-3f95-4d0a-8c6e-2a9b5d7f1e30",
"conversation_id": "8f3a1c52-6d47-4b9e-a210-5c7e9d2b4f18",
"customer_id": "c4a9e1b2-7d3f-4a8c-9e2b-1f6a0d5c3b7e",
"channel": "sms",
"to": "+61498765432",
"subject": null,
"text": "Hi Priya, Thursday works. We can be there between 8 and 10 am. Does that suit?",
"html": null,
"reply_to_message_id": null,
"drafted_by": { "kind": "api_key", "name": "Office assistant" },
"status": "draft",
"created_at": "2026-10-11T03:24:41.208Z",
"updated_at": "2026-10-11T03:24:41.208Z"
}
drafted_by.name is the API key's name or the connected app's name, saved with the draft, so it still reads right after the key is revoked. In Cooledge the draft shows as a card in the thread reading Drafted by Office assistant.
| Status | When |
|---|---|
| 400 | The body has a field it does not take, the SMS is over 459 characters, the email body is over 100 KB or there are no words. Also contact_id on a conversation with an unknown sender, which has no contacts. |
| 404 | The conversation, customer or contact is not found in your business, the contact is not this customer's, reply_to_message_id is not an email in this conversation or the conversation is in trash or spam. |
| 409 | The customer, contact or sender has no phone number or email address for the channel, the number cannot receive SMS (a landline, or any number that is not an Australian mobile), the saved email address is not a valid address or the conversation's customer changed while the draft was being saved. Fix the address in Cooledge, or read the conversation again and retry. |
| 429 | Too many drafts are waiting (urn:cooledge:error:draft-cap), or the rate limit was hit (urn:cooledge:error:rate-limited). |
Draft caps
At most 3 drafts from apps can wait in one conversation and 200 in a business. Drafts people save are not counted or capped. Past a cap, the create answers 429 with the type urn:cooledge:error:draft-cap and one of these details:
This conversation already has 3 drafts waiting. Send or delete one first.Your business has 200 drafts waiting. Send or delete some first.
A draft cap is not a rate limit. Retrying with backoff will not clear it, because it clears only when a person sends or deletes drafts (or you delete your own). Check the type before you retry any 429.
Update a draft
PATCH /v1/drafts/{id} changes the words of a draft. It takes subject, text and html (at least one of them, never both text and html), with the same limits as a create. An SMS draft takes text only. Any other field is a 400.
People can edit a waiting draft too. An update replaces the draft's subject, text or html with what you send while it is still a draft, including any words a person saved there in the meantime. For an email, the text or html you send replaces the whole body. Send text and the html is built from it. Send html and the text is read from it. What you leave out keeps what the draft has: the subject when you send only a body, the body when you send only a subject. Read the draft first if you want to build on a person's changes.
A credential edits only the drafts it wrote, while they are still waiting:
| Status | When |
|---|---|
| 404 | Not a draft this API key or connected app wrote, or its conversation is now in trash or spam. |
| 409 | A person has already scheduled, sent or deleted the draft, or a person changed it at the same moment (read it again and retry). |
Delete a draft
DELETE /v1/drafts/{id} deletes a draft this credential wrote while it is still waiting. It answers 204. It is idempotent, so deleting a draft that is already deleted still answers 204. It answers 404 for a draft this credential did not write and 409 once a person has scheduled or sent it, or changed it at the same moment.
List and read drafts
GET /v1/drafts lists the drafts waiting across the business, newest first, from people and apps. Pass conversation_id for one conversation's drafts. That includes drafts saved for its customer before the conversation existed. It needs messages:read.
GET /v1/drafts/{id} reads any draft of the business by id, including one a person has since sent, scheduled or deleted, so you can see what became of it.
Following a draft
A person can send, schedule, edit or delete any draft, including yours. The draft's status tells you where it is:
status | Meaning |
|---|---|
draft | Waiting for a person. |
scheduled | A person scheduled it with Send later. |
sending | A person pressed Send and it is going out, or being held for undo send. |
sent | It was sent. |
failed | A person tried to send it and the send failed. |
deleted | It was deleted, or a person sent it and then pressed Undo. |
An undone send comes back with a new id. When a person sends a draft and presses Undo, the draft returns to the conversation as a new draft with a new id. The old id reads as deleted. List the conversation's drafts to find it.
There are two ways to follow your drafts:
- Poll
GET /v1/drafts/{id}and watchstatus. - Subscribe to the
message.sentwebhook. When the message was a draft, itsdraftfield carries the id the draft had when it was sent and who drafted it.
Webhooks
Two webhook events cover the inbox:
message.receivedwhen a customer or an unknown sender texts or emails the business.message.sentwhen someone on the team sends a text or email from Messages, including a draft.
Through the API, creating a subscription that includes either event needs messages:read as well as webhooks:manage. So does an update that leaves either event on the subscription, rotating its secret or deleting it. See the events index.
An example agent loop
An agent that drafts replies for a person to approve does three things on a schedule:
- Find the conversations waiting on a reply.
GET /v1/conversations?unreplied=true. Skip any with adraft_countabove 0, because someone (a person or your agent) has already drafted a reply. - Read each one.
GET /v1/conversations/{id}/messagesfor the thread, newest last. - Write a draft.
POST /v1/draftswith theconversation_id, the channel the customer last used and your reply. For an email, pass theidof the email you are answering asreply_to_message_idso the reply threads.
# 1. Conversations waiting on a reply
curl "https://app.cooledge.com.au/api/v1/conversations?unreplied=true" \
-H "X-Api-Key: $COOLEDGE_KEY"
# 2. The thread of one of them
curl "https://app.cooledge.com.au/api/v1/conversations/8f3a1c52-6d47-4b9e-a210-5c7e9d2b4f18/messages" \
-H "X-Api-Key: $COOLEDGE_KEY"
# 3. A draft reply for a person to review
curl https://app.cooledge.com.au/api/v1/drafts \
-H "X-Api-Key: $COOLEDGE_KEY" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "8f3a1c52-6d47-4b9e-a210-5c7e9d2b4f18",
"channel": "email",
"reply_to_message_id": "4a8e2c71-9b06-4d3f-a5e8-7c1d0f6b2a93",
"text": "Hi Priya,\n\nThursday works. We can be there between 8 and 10 am.\n\nThanks"
}'
Nobody is notified when an agent saves a draft. The team finds it through the Drafts count in Messages, the Draft label on the conversation, the card in the thread and the dashboard. If a 409 says the customer has no address for the channel, try the other channel or leave the conversation for a person.
Where to go next
- The API reference has every field of the conversation, message and draft shapes.
- MCP server lists the six messaging tools for AI assistants.
- Rate limits covers the 120 requests a minute every credential shares.
Need a hand with an integration? Contact support