Guides

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.

Updated 11/10/2026

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

ScopeWhat it allows
messages:readList and read conversations, their messages and calls and the drafts waiting in them. Also needed to subscribe to the message webhooks.
messages:draftCreate, 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.

ParameterValuesNotes
stateopen (default), snoozed, archivedTrash and spam are never listed.
channelsms, email, voice, callsOnly conversations with a message on that channel. voice is voicemail. calls is calls and voicemail.
searchtext, up to 200 charactersMatches the customer's name, phone or email, a contact's email or an unknown sender's address. It does not search message text.
unrepliedtrue or falseWith true, only conversations whose latest inbound message has no reply after it.
unmatchedinclude (default), exclude, onlyConversations 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.

  • type is message, call or comment. Internal team comments are left out unless you pass include=comments.
  • channel is sms, email, voice (a voicemail) or call.
  • html is 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.
  • attachments lists each file's id, name, content type and size only. Inline images are left out. Downloading files through the API is not available yet.
  • call carries a call's outcome and duration_seconds. It is null for anything else.
  • sent_by is 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.

FieldRequiredNotes
conversation_idOne of these twoThe conversation to draft into.
customer_idOne of these twoA customer, for a first message when they have no conversation yet. Pass one or the other, not both.
channelYessms or email.
contact_idNoOne of the customer's contacts, to draft to them instead of the customer.
textSMS: yesSMS: the message, up to 459 characters after surrounding spaces are trimmed. Email: the plain text body.
subjectNoEmail only.
htmlNoEmail 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_idNoEmail 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.

StatusWhen
400The 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.
404The 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.
409The 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.
429Too 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:

StatusWhen
404Not a draft this API key or connected app wrote, or its conversation is now in trash or spam.
409A 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:

statusMeaning
draftWaiting for a person.
scheduledA person scheduled it with Send later.
sendingA person pressed Send and it is going out, or being held for undo send.
sentIt was sent.
failedA person tried to send it and the send failed.
deletedIt 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 watch status.
  • Subscribe to the message.sent webhook. When the message was a draft, its draft field carries the id the draft had when it was sent and who drafted it.

Webhooks

Two webhook events cover the inbox:

  • message.received when a customer or an unknown sender texts or emails the business.
  • message.sent when 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:

  1. Find the conversations waiting on a reply. GET /v1/conversations?unreplied=true. Skip any with a draft_count above 0, because someone (a person or your agent) has already drafted a reply.
  2. Read each one. GET /v1/conversations/{id}/messages for the thread, newest last.
  3. Write a draft. POST /v1/drafts with the conversation_id, the channel the customer last used and your reply. For an email, pass the id of the email you are answering as reply_to_message_id so 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