Skip to content
Miss Blue

Reference

Messages

Sending is the whole point. A send is accepted here and goes out from your number shortly after — or, if the number is temporarily unavailable, as soon as it is back. See queued and notice below, and Delivery for how you find out what happened to it.

Send an iMessage.

Text, files, or a reply quoting an earlier message. Sending to an existing group is the same call with the group's chat_id as the recipient. A send the number cannot take right now is still accepted, not refused: 201 with status pending, queued: true, and a notice — { "code": "number_unavailable", "message": "This number is temporarily unavailable. Your message is queued and will be sent automatically as soon as it's back." }. It goes out by itself when the number is back, so do not send it again; until then POST /v1/messages/{id}/cancel calls it off. A notice code of send_retrying means the send was turned down for now and will be tried again automatically. A notice code of daily_new_contact_limit means the number has messaged as many new people as it may today: the first message waits and goes out by itself at send_after, as room opens — do not send it again. Only a first message that could not go within five days is refused, with 429 and a Retry-After of when a try would be taken. GET /v1/projects/{id}/sending-stats shows each number's allowance: new_contacts_used, new_contacts_limit, new_contacts_waiting, and next_new_contact_at, when the next new person can be messaged if not now; messages_used and messages_limit, every message the number sent in the last 24 hours against its daily total (150 unless raised); and warmup, its week and limit while it climbs back after sending as texts. A paced send — or an automated one past the daily total — is queued with no notice: the message's send_after says when; one past the daily total that could not go within five days is refused with 429 daily_message_limit and a Retry-After. Pass manual: true for one message a person asked to send now: like a send typed in the inbox, it goes past the daily total and still counts toward it; opt-outs, blocks and the new-people limit still apply. Not for bulk. See /docs/rate-limits. notice.message is written to be shown to your own users as it is; switch on notice.code. link_code and url are the conversation's short link, the same one GET /v1/threads and its webhooks give it; they are absent while a first message to somebody new is still waiting for its number, and message.sent carries them once it has gone.

number_id*uuidWhich of your numbers to send from. GET /v1/numbers lists them.
recipient*stringA phone number, an Apple ID email, or a chat_id to send into an existing conversation.
textstringOptional only if you are sending attachments — a message must carry one or the other.
attachment_idsuuid[]From POST /v1/attachments. This call accepts text or attachments, not both; send a caption separately.
native_voice_notebooleanAudio-only sends default to a native voice note. Set false to send an ordinary file.
reply_to_message_iduuidOur id for the message being quoted, not Apple's.
status_callbackhttps urlWhere to POST this one message's outcome. See Delivery.
allow_duplicatebooleanSend anyway, though the same text went to the same person moments ago.
manualbooleanOne message a person asked to send now: not held by the number's daily total, as a send from the inbox is not. It still counts toward it; opt-outs, blocks and the new-people limit still apply. Not for bulk.
curl -X POST https://api.missblue.dev/v1/messages \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d",
    "recipient": "+15555550100",
    "text": "Your table is confirmed for 7pm."
  }'

Response

{
  "id": "0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77",
  "request_id": "d0cc8f6e-8d57-4bd2-bb27-a3c86ca810e8",
  "agent_id": "3e814ce3-d92b-4d93-9baf-33459eacb264",
  "status": "sent",
  "external_message_id": "p:0/4AEE9B2A-7E3C-4E7C-88EA-5F071974624B",
  "queued": false,
  "chat_id": "iMessage;-;+15555550100",
  "link_code": "k7Qm3xT9pa",
  "url": "https://missblue.dev/c/k7Qm3xT9pa"
}
  • 400No text and no attachments, or neither number_id nor agent_id.
  • 400invalid_recipient: a phone number nobody can have — a +1 number without ten digits, an area code or exchange starting with 0 or 1 or an N11 code, a fictional 555-0100–0199, or not 8 to 15 digits after the +. Nothing is written or sent. error.reason is invalid.
  • 422recipient_undeliverable: the number cannot get texts — a landline, not in service, or not a valid number, as its carrier, Twilio or the check before a first text on a Twilio line said. error.reason is landline, not_in_service, invalid or other.
  • 404A number_id your key does not hold. Not 403 — whether it exists is not your business.
  • 409The same text went to the same person moments ago. Pass allow_duplicate to mean it.
  • 409A native voice note while the number is temporarily unavailable (agent_offline). Voice notes are sent live and never queued; ordinary sends are queued instead — see queued and notice.

Send an uploaded audio file as a native voice note.

Upload one audio/* file first. An attachment-only audio send defaults to Apple's native voice-note bubble for every number-scoped customer call. Pass native_voice_note: false only when you deliberately want a normal file. If native delivery is unavailable before sending, Miss Blue automatically sends the original audio as a regular playable attachment. An uncertain delivery is kept pending to avoid duplicate messages.

number_id*uuidThe dedicated number sending the voice note.
recipient*stringPhone number, Apple ID email, or existing chat id.
attachment_ids*uuid[1]Exactly one uploaded audio attachment.
native_voice_notebooleanDefaults to true for this request shape; false sends a generic file.
curl -X POST https://api.missblue.dev/v1/messages \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d",
    "recipient": "+15555550100",
    "attachment_ids": [
      "4c9103dd-ae62-4888-9a17-c762efe8ddcb"
    ]
  }'

Response

{
  "id": "f30fd81d-79a4-4795-9794-b58a49cbb825",
  "request_id": "2ebf14c9-2952-4931-adc1-2e80852ce239",
  "agent_id": "3e814ce3-d92b-4d93-9baf-33459eacb264",
  "status": "sent",
  "external_message_id": "p:0/2A3E52BC-6922-4608-94E9-F120285AE812",
  "chat_id": "iMessage;-;+15555550100",
  "queued": false
}
  • 400More than one attachment, a non-audio attachment, text, or a quoted reply.
  • 409The number is temporarily unavailable (agent_offline). Native voice notes are sent live, not queued for later; retry shortly.

Send two through twenty images as one native carousel.

Upload every image first and preserve the order in attachment_ids. All files must be images. A native carousel cannot carry a caption or quote another message; send text separately if you need it.

curl -X POST https://api.missblue.dev/v1/messages \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d",
    "recipient": "+15555550100",
    "attachment_ids": [
      "88de9745-30e0-4b93-bfcb-dbb7f3325dde",
      "45793a35-0a1c-431d-8806-38f71ce658ff",
      "fe74c17c-f3cd-4547-a32b-cf271845d95f"
    ]
  }'

Response

{
  "id": "9396bdad-9e14-4caa-8f01-20188aedbb23",
  "request_id": "3cd8cab6-1e17-4169-866d-eac13765ef58",
  "agent_id": "3e814ce3-d92b-4d93-9baf-33459eacb264",
  "status": "sent",
  "external_message_id": "p:0/F4AF7E62-FC00-4805-A9A9-86557DB11137",
  "chat_id": "iMessage;-;+15555550100",
  "queued": false
}
  • 400More than twenty files, a non-image file, text, or a quoted reply.

List messages across your project's numbers, newest first.

curl -X GET https://api.missblue.dev/v1/messages \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "total": 1,
  "data": [{
    "id": "0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77",
    "chat_id": "iMessage;-;+15555550100",
    "direction": "outbound",
    "recipient": "+15555550100",
    "text": "Your table is confirmed for 7pm.",
    "status": "delivered",
    "service": "imessage",
    "message_at": "2026-08-28T10:04:00Z",
    "created_at": "2026-08-28T10:04:00Z",
    "updated_at": "2026-08-28T10:04:02Z",
    "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d"
  }]
}

One message, including its current delivery state.

Polling this works, but a webhook or a status_callback tells you sooner and costs you nothing. `agent_version` and `macos_version` say which Mac build handled it, stamped when the message was written rather than looked up now — a Mac that is updated must not rewrite what it was running last week, which is exactly the week you are trying to explain. `read` means the person has seen it: an iMessage read receipt reads the conversation up to that message, so earlier delivered iMessages to the same person turn `read` with it, as they do on the sender's iPhone — Apple itself stamps only some of them. A failed message carries `reason` (landline, not_in_service, unreachable, invalid, opted_out, blocked, spam_filtered or other) and, when a carrier gave one, `error_code` (Twilio's 30006, say), as message.failed webhooks do.

curl -X GET https://api.missblue.dev/v1/messages/0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77 \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "id": "0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77",
  "chat_id": "iMessage;-;+15555550100",
  "direction": "outbound",
  "recipient": "+15555550100",
  "text": "Your table is confirmed for 7pm.",
  "status": "delivered",
  "service": "imessage",
  "message_at": "2026-08-28T10:04:00Z",
  "created_at": "2026-08-28T10:04:00Z",
  "updated_at": "2026-08-28T10:04:02Z",
  "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d"
}
  • 404No such message, or one on a number your key does not hold.

Every message in one conversation, oldest first.

Ordered by the platform's own sort key, so a message the Mac picked up late still lands where it belongs rather than at the end. message_at is the original Messages.app time; created_at is the later control-plane ingestion time. link_code and url are the conversation's short link, the same one GET /v1/threads gives it, made from the messages you are shown; absent when you are shown none.

curl -X GET https://api.missblue.dev/v1/messages/thread/iMessage%3B-%3B%2B15555550100 \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "total": 14,
  "link_code": "k7Qm3xT9pa",
  "url": "https://missblue.dev/c/k7Qm3xT9pa",
  "data": [{
    "id": "0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77",
    "chat_id": "iMessage;-;+15555550100",
    "direction": "inbound",
    "sender_handle": "+15555550100",
    "sender_name": "Ari Chen",
    "text": "Perfect, see you then",
    "status": "received",
    "service": "imessage",
    "message_at": "2026-08-28T10:04:00Z",
    "created_at": "2026-08-28T10:04:00Z",
    "updated_at": "2026-08-28T10:04:00Z"
  }]
}

Add or remove a tapback.

reaction_key*stringheart, like, dislike, laugh, emphasize, or question.
removebooleanTake it back rather than add it.
curl -X POST https://api.missblue.dev/v1/messages/0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77/react \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reaction_key": "heart"
  }'
  • 409The number is temporarily unavailable (agent_offline). Nothing was changed; retry shortly.

Messages that failed, or are still queued.

The queue and the failures in one list, because from a caller's side they are the same question: what have I sent that has not arrived. Worth polling on a schedule even if you take webhooks — a webhook that never fired is exactly the case a webhook cannot tell you about.

onlystringqueued or failed. Omit for both.
curl -X GET https://api.missblue.dev/v1/messages/problems \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "total": 1,
  "data": [{
    "id": "0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77",
    "status": "failed",
    "recipient": "+15555550100",
    "error": "This message could not be sent. Try again or contact support.",
    "message_at": "2026-08-28T10:04:00Z",
    "created_at": "2026-08-28T10:04:00Z",
    "updated_at": "2026-08-28T10:04:30Z"
  }]
}

Cancel a message that is still queued.

For a send that has not gone yet — held while its number is temporarily unavailable, paced, or waiting to be tried again. Only an outbound message that is pending, queued, and has no external_message_id can be cancelled. It ends failed, with the error "Cancelled before it was sent.", and the usual message.failed webhook and status_callback fire: an integration learns this outcome the way it learns every other. Once a message has been handed over for sending it may already be on somebody's phone, so it can no longer be cancelled.

curl -X POST https://api.missblue.dev/v1/messages/0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77/cancel \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "id": "0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77",
  "chat_id": "iMessage;-;+15555550100",
  "direction": "outbound",
  "recipient": "+15555550100",
  "text": "Your table is confirmed for 7pm.",
  "status": "failed",
  "error": "Cancelled before it was sent.",
  "service": "imessage",
  "message_at": "2026-08-28T10:04:00Z",
  "created_at": "2026-08-28T10:04:00Z",
  "updated_at": "2026-08-28T10:06:12Z",
  "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d"
}
  • 404No such message on a number your key holds.
  • 409It is already on its way and can't be cancelled — or it was never queued: sent, failed, or inbound.

Unsend a message.

Apple's two-minute window and its own rules apply. We do not re-check them against our clock — the Mac reports the platform's answer, so a refusal here is Apple's refusal, not ours. The recipient is left with "You unsent a message" where the bubble was; unsending is not the same as them never having seen it. Afterwards the message reads is_deleted: true with no text, and the Mac writes a separate action row carrying the notice.

curl -X POST https://api.missblue.dev/v1/messages/0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77/unsend \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
  • 409The number is temporarily unavailable (agent_offline). Nothing was changed; retry shortly.

Change what a message already sent says.

202, not 204, and the difference matters: Messages.app accepts an edit and reports nothing. The change arrives back through ordinary sync moments later, exactly as one typed on the Mac would, so "accepted" is the honest answer and the transcript is what confirms it. Poll GET /v1/messages/{id} and watch for edited_at — until it is set, the edit has not landed. Apple allows this for roughly fifteen minutes after sending and shows the recipient that the message was edited; there is no silent correction.

text*stringThe replacement. An empty or whitespace-only edit is a 400 rather than an unsend — the two are different enough that guessing which was meant would be the wrong kind of helpful.
curl -X POST https://api.missblue.dev/v1/messages/0b5ed7e8-f7a1-4f6e-9c2a-2a1f0d9e8b77/edit \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Tuesday at 10, not Thursday."
  }'
  • 400The text is empty, or the Mac refused the edit — most often the window has closed.
  • 404No such message on a number your key holds.
  • 409The number that sent it is temporarily unavailable (agent_offline). Nothing was changed; retry shortly.
NextConversationsThreads, history sync, read receipts, and typing indicators.