Skip to content
MirageCHATEngagePublishGrow

The MirageCHAT API

REST · JSON · signed webhooksMessaging · contacts · publishing · resellersUpdated

In short

One base URL — https://miragechat.io/api/ext/v1 — and a key. Read and answer conversations, keep the Customers book in step, receive signed webhooks, read your team, automations, WhatsApp forms, calls and numbers, and post to Instagram, Facebook and TikTok. A reseller’s key acts as any of its client workspaces with one header, X-Workspace-Id.

Start here

Every request goes to https://miragechat.io/api/ext/v1 with the key in the Authorization header. Bodies are JSON; times are ISO 8601 in UTC. GET /me tells you which workspace a key belongs to.

curl https://miragechat.io/api/ext/v1/me \
  -H "Authorization: Bearer mck_live_…"

200 OK
{ "tenant_id": 42, "tenant": { "id": 42, "name": "Seaside Villa", "slug": "seaside-villa" }, "scopes": ["read", "write"] }

Pages. A long list answers { "data": […], "next_cursor": "…" }. Send next_cursor back as cursor for the next page; it is null on the last one, and limit is at most 100. The cursor is opaque: keep it as it came.

User-Agent. Send your own User-Agent header (for example AcmeCRM/2.1). A library’s default, such as Python-urllib, can be refused by our CDN with 403 (error 1010).

Keys and scopes

The owner or an admin creates keys under Security › Developer › API Keys — how. A key is shown once; we keep only its hash. Keep it on your server, never in a browser or an app. A messaging key made there has read and write.

ScopeWhat it allows
readEvery GET of the API below: channels, conversations, messages, contacts, webhooks, team, automations, forms, calls, analytics and insights.
writeEverything that sends or changes something: messages, a conversation’s status and assignee, contacts and notes, webhooks, the automation switch.
resellerA reseller’s key: the /reseller/ routes, and X-Workspace-Id to act as one of its client workspaces.
publish · publish:readThe Publishing API — a key of its own. A messaging key never posts, and a publishing key never reads conversations.

Your package. The API is in every paid package; the Publishing API is in Pro. A key made while the package had the API keeps reading after a move to one without it, and every write answers 402 module_required.

Resellers: X-Workspace-Id

A reseller’s key carries the reseller scope. Add X-Workspace-Id with the id of one of your client workspaces to any request on this page, and it acts as that workspace: its channels, conversations, contacts, webhooks, posts and package. Leave the header out and the key acts as your own workspace. The ids are in GET /reseller/workspaces, and under each name in Reseller › Client workspaces.

KeyMade inScopesWhat it does
Reseller keyReseller › Settings › Create reseller keyread · write · resellerThe /reseller/ routes — and, inside any client workspace, messages, conversations, contacts, webhooks and the workspace tools. It never posts.
Publishing keyReseller › Settings › Create publishing key, after ticking «Posts sent with this key are published on the selected accounts in my name.»publish · resellerPosts to every Instagram, Facebook and TikTok account of the client workspace X-Workspace-Id names, accounts connected later included. It reads no conversations.
curl "https://miragechat.io/api/ext/v1/conversations?status=open" \
  -H "Authorization: Bearer mck_live_…" \
  -H "X-Workspace-Id: 1304"

Without the reseller scope, the header answers 403 reseller_scope_required; an id that is not a number, 400 invalid_workspace_id; an id that is not one of your client workspaces, 404 workspace_not_found — never another reseller’s. The /reseller/ routes do not read the header.

Reseller API

Your own software does what the Reseller menu does: it creates and closes client workspaces, makes and revokes connect links, adds their people, sets your name and logo on the connect page, and reads your balance, statement and paid channels. Every route needs the reseller scope, with read to read and write to change. Fields, answers and errors: the Reseller API reference.

MethodPathScopeWhat it does
GET/reseller/meread · resellerYour account: status, prices, balance, what a month costs at today’s channels, and what falls due in the next 7 days.
GET/reseller/workspacesread · resellerYour client workspaces, newest first. include_closed=0 leaves the closed ones out; external_id finds one by your reference.
POST/reseller/workspaceswrite · resellerCreate a client workspace: name, timezone, external_id, return_url. With return_url, the answer carries its connect link. Send external_id and a retry is safe: the same one again is 409 external_id_taken with the workspace_id, never a second workspace.
GET/reseller/workspaces/{id}read · resellerOne client workspace.
PATCH/reseller/workspaces/{id}write · resellerRename it, or change your reference (external_id; null clears it).
DELETE/reseller/workspaces/{id}write · resellerClose it: read-only at once, its channels switched off, its data kept for 90 days.
POST/reseller/workspaces/{id}/connect-linkwrite · resellerA new connect link with your name and logo: return_url, and hours from 1 to 168 (24 when left out; another number is 400).
POST/reseller/workspaces/{id}/connect-link/revokewrite · resellerEvery open connect link of the workspace stops working at once.
GET/reseller/workspaces/{id}/usersread · resellerThe people who sign in to it: role, switched off or not, last sign-in.
POST/reseller/workspaces/{id}/userswrite · resellerAdd a person: email, password, role (admin or agent) and name. Nothing is emailed to them.
PATCH/reseller/workspaces/{id}/users/{userId}write · resellerSwitch a person off or on, or change their role, password or name.
GET/reseller/channelsread · resellerEvery paid channel of your client workspaces, with paid_until and monthly_usd — to bill your own clients by.
GET/reseller/statementread · resellerWhat was taken from your balance, newest first: limit (1 to 500, 100 when left out; another number is 400), and before for older lines — next_before is null on the last page.
GET/reseller/settingsread · resellerYour name and logo on the connect page.
PATCH/reseller/settingswrite · resellerChange display_name and logo_url (an https:// address).
curl -X POST https://miragechat.io/api/ext/v1/reseller/workspaces \
  -H "Authorization: Bearer mck_live_…" \
  -H "Content-Type: application/json" \
  -d '{"name": "Seaside Villa", "timezone": "Europe/London", "external_id": "client-42",
       "return_url": "https://your-app.example.com/clients/42"}'

201 Created
{
  "workspace": {
    "id": 1304, "name": "Seaside Villa", "slug": "seaside-villa-3fa91c", "external_id": "client-42",
    "timezone": "Europe/London", "created_at": "2026-10-15T09:12:03.000Z", "closed_at": null,
    "status": "no_channels", "channels": [], "chats_30d": 0, "monthly_usd": 0
  },
  "connect_url": "https://miragechat.io/connect/c_…",
  "connect_expires_at": "2026-10-16T09:12:03.000Z"
}

Conversations and messages

What the Inbox does with a conversation, your system does here — and it lands where a person’s work would: the same list and order, the same status and assignee, the same person’s name from the Customers book.

MethodPathScopeWhat it does
GET/meany messaging keyThe key’s workspace and its scopes.
GET/accountsreadYour connected channels: id, name, platform, number or username — the id is the account_id you send with. YouTube channels are not listed.
GET/conversationsreadThe Inbox’s list, newest message first. Filters: status, account_id, platform, assigned_to (a team member’s id or email, or none), updated_since, q, snoozed (only or all), limit (up to 100), cursor. Snoozed conversations are left out unless you send snoozed — but q finds them too, as the Inbox’s search does.
GET/conversations/{id}readOne conversation as the list shows it, with sender_name, sender_username, name and name_source beside it. 404 conversation_not_found when it is not this workspace’s.
GET/conversations/{id}/messagesreadThe newest messages, oldest first: limit (up to 100, 100 when left out). next_cursor goes back to older ones — or send before with a message’s id.
PATCH/conversations/{id}writeChange status (open, pending or closed) and assigned_to (a team member’s id or email, or null), as the Inbox does.
POST/messageswriteSend through a channel: account_id and to, or conversation_id. Telegram, SMS and website chat take text only; SMS and website chat take an optional idempotency_key.
GET/templatesreadYour WhatsApp message templates, with their status.
POST/messages/templatewriteSend an approved WhatsApp template, also to a number that never wrote. A marketing template needs opt_in: true.
POST/messages/readwriteMark a WhatsApp message as read (blue ticks), with a typing indicator if you like.
POST/conversations/nameswriteFill in WhatsApp names by phone number — only where the conversation has no real name yet.
POST/ai/replywriteAn AI reply written with the account’s prompt and knowledge, for your own system to send.
GET/optouts · /optouts/{platform}/{psid}readThe opt-out list, or whether one person is on it.
POST · DELETE/optouts · /optouts/{platform}/{psid}writeRecord an opt-out, or take one back.

Sending. POST /messages sends on WhatsApp, Instagram, Messenger, Telegram, SMS and website chat. Send account_id and to, or conversation_id — never both. WhatsApp, Instagram, Messenger and Telegram take text, or a type with media, location, contacts, reaction, interactive or quick_replies where the channel has it. A type the channel does not have is 400 unsupported_type, and the message lists the ones it has; a media type needs media with a link or an uploaded id. Telegram takes text only (400 text_only): it would drop media and buttons. SMS goes out the way the Inbox sends it, through the workspace’s MirageTEL or Twilio account. SMS and website chat take text only (400 text_only otherwise; a website chat message is up to 4,000 characters, and is answered only once its visitor has written). There an idempotency_key makes a retry safe: the same key again answers deduped: true and sends nothing, and the same key with other words is 409 idempotency_key_reused. A key belongs to one conversation (one person on one channel) and lasts as long as the message it sent; two requests with one key at once send once — the second waits for the first, or answers 409 idempotency_key_in_use if the first is still being sent after 15 seconds. Outside WhatsApp’s 24-hour window, send an approved template with POST /messages/template.

The WhatsApp number. Write to as you like — +1 555 123 4567, 15551234567 — it is sent, checked against the opt-outs and kept as its digits, the way the person’s own conversation keeps it, so your send lands in that conversation. A number without its country code is refused by WhatsApp (400 invalid_recipient).

The answer. Once the channel has taken the message, the answer is 200 with status: "sent" — whatever happens after. Its copy in the Inbox carries id and conversation_id; if the copy could not be written, id is null and the message has still been sent: do not send it again. A refusal is named: 409 outside_window, 422 recipient_unavailable, 400 invalid_recipient, 409 channel_disconnected, 402 payment_issue, 403 policy_refused, 429 rate_limited — each with provider_code, the channel’s own code. Anything else is 424 send_failed: not sent. With maybe_sent: true the channel did not answer in time and may have sent it — read the conversation before you send it again. A disconnected channel is 409 channel_disconnected (409 no_channel through conversation_id), and sends nothing. A message sent by the API counts as the conversation’s first response, as a person’s reply does, unless it opened the conversation.

Status and assignee. PATCH /conversations/{id} runs through the Inbox’s own steps, as when a person does it, and fires conversation.closed or conversation.assigned. A change that changes nothing is not made. It answers the conversation as the list shows it.

curl "https://miragechat.io/api/ext/v1/conversations?status=open&assigned_to=none&limit=1" \
  -H "Authorization: Bearer mck_live_…"

200 OK
{
  "data": [
    {
      "id": 88123, "account_id": 77, "platform": "whatsapp", "status": "open", "assigned_to": null,
      "contact": { "id": "15551234567", "name": "Dana Reyes", "name_source": "edited", "handle": null, "phone": "+15551234567" },
      "last_message": { "text": "Is Thursday at 10:30 free?", "direction": "inbound", "at": "2026-10-15T08:41:09.512Z" },
      "unread_count": 1, "snoozed_until": null,
      "created_at": "2026-10-12T14:03:55.000Z", "updated_at": "2026-10-15T08:41:09.000Z"
    }
  ],
  "next_cursor": "eyJ0IjoiMjAyNi0xMC0xNVQwODo0MTowOS41MTIwMDAiLCJpZCI6ODgxMjN9"
}

curl -X POST https://miragechat.io/api/ext/v1/messages \
  -H "Authorization: Bearer mck_live_…" \
  -H "Content-Type: application/json" \
  -d '{"conversation_id": 88123, "text": "Yes, Thursday at 10:30 is yours."}'

200 OK
{ "id": 912044, "conversation_id": 88123, "status": "sent", "provider_response": { "id": "wamid.HBgL…" } }

Older messages. GET /conversations/{id}/messages gives the newest messages, oldest first, with next_cursor: send it back as cursor for the page before them, until it is null at the first message. Or send before with a message’s id — the messages older than that one. limit is 1 to 100 (100 when left out).

curl "https://miragechat.io/api/ext/v1/conversations/88123/messages?limit=2" \
  -H "Authorization: Bearer mck_live_…"

200 OK
{
  "data": [
    { "id": 912040, "sender_type": "customer", "content": "Is Thursday at 10:30 free?", "created_at": "2026-10-15T08:41:09.000Z" },
    { "id": 912044, "sender_type": "agent", "content": "Yes, Thursday at 10:30 is yours.", "created_at": "2026-10-15T08:42:30.000Z" }
  ],
  "next_cursor": "eyJiIjo5MTIwNDB9"
}

curl "https://miragechat.io/api/ext/v1/conversations/88123/messages?limit=2&cursor=eyJiIjo5MTIwNDB9" \
  -H "Authorization: Bearer mck_live_…"

Instagram and WhatsApp tools

These need the platform’s own API in your package, for reads too. The three Instagram handover routes answer 410 handover_retired: Meta moved every business to Conversation Routing.

MethodPathScopeWhat it does
GET/accounts/{id}/media/{mediaId}/commentsreadInstagram: the comments on a post.
POST/accounts/{id}/comments/{cid}/replywriteInstagram: reply under a comment.
POST/accounts/{id}/comments/{cid}/private-replywriteInstagram: answer a comment in a direct message.
POST/accounts/{id}/comments/{cid}/hidewriteInstagram: hide a comment (send hide: false to show it again).
DELETE/accounts/{id}/comments/{cid}writeInstagram: delete a comment.
GET · PUT/accounts/{id}/greetingread · writeInstagram: the ice breakers and the persistent menu.
GET · PUT/accounts/{id}/business-profileread · writeWhatsApp: the business profile.
GET · POST · DELETE/accounts/{id}/qr · /accounts/{id}/qr/{code}read · writeWhatsApp: QR codes that open a chat with a message ready.

Contacts

The Customers book — the people of Customers › People, one name per person, the name the Inbox shows. A contact’s id is text: for WhatsApp, the number’s digits. A contact carries its name and where it comes from, phone, email, vip, tags, lists, marketing consent, opt-outs and channels.

MethodPathScopeWhat it does
GET/contactsreadFind people: q, tag, platform, updated_since, limit (up to 100), cursor.
GET/contacts/listsreadYour lists, with how many people each one holds.
GET/contacts/{id}readOne person, with their notes.
POST/contactswriteAdd a person by their WhatsApp number (phone; country for a number written without its code). Without + and without country, the number is read as a US or Canada number. upsert: true merges into the person you already have.
PATCH/contacts/{id}writename (null gives the name back to their phone or profile), email, vip, tags_add, tags_remove, consent, lists_add, lists_remove.
POST/contacts/{id}/noteswriteA note on one of their conversations: content, pinned, conversation_id.
DELETE/contacts/{id}writeRemove someone who never wrote from your client list. A person who wrote stays (409 contact_wrote).

A number without + is read as a US or Canada number. POST /contacts reads phone with the country in country (two letters); without +, 00 and country it is read as a US or Canada number. An Iraqi mobile written 770 451 2345 becomes +1 770 451 2345 — someone in Georgia, USA. Send +964 770 451 2345, or the number with "country": "IQ".

  • One name. Adding or merging never renames a person — only PATCH name does; name_kept says when a name you sent was not used.
  • Consent is subscribed, not_asked or unsubscribed; subscribed takes consent_source and consent_at. Someone who said STOP is never made subscribed: consent_not_recorded: "said_stop" on POST, 409 said_stop on PATCH.
  • The number is who they are: it is not changed (400 not_editable). An existing number without upsert is 409 contact_exists.
  • Notes of a conversation locked with a PIN are not shown, and cannot be written (423 conversation_locked).
  • Both ways. Subscribe a webhook to contact.created, contact.updated and contact.deleted: every change made in Customers — on the web, in the app, by an import — or by this API arrives once, with the fields that changed in changed and the whole contact. A change your own key makes comes back with via: "api", so your system can tell its own. Someone who writes to you first is conversation.created (its sender_id is their contact id).
curl -X POST https://miragechat.io/api/ext/v1/contacts \
  -H "Authorization: Bearer mck_live_…" \
  -H "Content-Type: application/json" \
  -d '{"phone": "+1 555 123 4567", "name": "Dana Reyes", "email": "dana@example.com", "tags": ["booking"],
       "consent": "subscribed", "consent_source": "Booking form", "upsert": true}'

201 Created
{
  "contact": {
    "id": "15551234567", "name": "Dana Reyes", "name_source": "added", "phone": "+15551234567",
    "email": "dana@example.com", "vip": false, "tags": ["booking"], "lists": [],
    "marketing": { "status": "subscribed", "source": "Booking form", "at": "…", "whatsapp": true },
    "channels": [], "conversation_ids": [], "notes_count": 0, …
  },
  "created": true, "matched_by": null, "name_kept": false, "consent_not_recorded": null
}

Webhooks

Your server gets each event as a signed POST. Make endpoints under Security › Developer › Outbound Webhooks, or with these routes.

MethodPathScopeWhat it does
GET/webhooksreadYour webhook endpoints — never their signing secret.
GET/webhooks/event-typesreadEvery event an endpoint can subscribe to.
POST/webhookswriteMake one: url (a public https:// address), events, description, include_clients. The answer shows its signing secret, once.
PATCH/webhooks/{id}writeChange url, events, description, active or include_clients.
DELETE/webhooks/{id}writeDelete it.
POST/webhooks/{id}/testwriteDeliver one signed test.ping now: delivered, status_code and error say how it went.
GET/webhooks/{id}/deliveriesreadIts delivery log, newest first — every attempt of the last 7 days: event_type, success, status_code, error. Filters: event_type, success; limit (up to 100), cursor.
  • The secret is in the answer of POST /webhooks only. The list never shows it again — in the API or in the web app.
  • Your package. Making, changing and testing an endpoint need the API in your package; reading, deleting and switching one off (active: false) never do — nor does a hold.
  • The address must be a public https:// one: a private or local address, or one of MirageCHAT’s own servers, is 400 blocked_destination, and is checked again at every delivery.
  • Every client in one endpoint. include_clients: true also sends it the events of every client workspace, each with workspace_id. Only a reseller’s own key, without X-Workspace-Id, can set it (403 reseller_only).
  • The delivery log. GET /webhooks/{id}/deliveries lists every attempt of the last 7 days, newest first: event_id is the id your server received, workspace_id the client workspace an include_clients delivery came from. It is a read, so it works whatever your package and while a workspace is on hold.
EventWhendata
conversation.createdA new conversation starts — when a person writes first, and when a send of this API opens one (via: api).conversation_id, account_id, account_name, platform, sender_id, sender_name, first_message; from the API also via
conversation.assignedA conversation is given to someone, or taken from them.conversation_id, assigned_to (an email, or null), reason (web, api or handoff_template); from the Inbox and the API also account_id, assigned_name, previous_assigned_to
conversation.closedA conversation is closed, in the Inbox or by the API.conversation_id, account_id, platform, previous_status, via (web or api)
message.receivedA customer writes.conversation_id, account_id, account_name, platform, sender_id, sender_name, message, media_url, provider_message_id
message.sentA message goes out through the API or from the AI — and the first reply a person (or a scheduled message) sends in a conversation.conversation_id, message, via (api, agent, scheduled, ai_auto_reply …); a send of this API adds account_id and to (a template also its name in template); a first reply — a person’s or the API’s — adds first_response_minutes
csat.received · tag.addedCan be subscribed to. MirageCHAT does not send them yet.—
post.queued · post.awaiting_approval · post.sent_to_drafts · post.published · post.failed · post.cancelledA post’s item changes status — made through the API, on the web or on the phone.See the Publishing API reference
account.connected · account.disconnectedA channel is connected (or switched back on), or disconnected.workspace_id, external_id, account_id, platform, name
contact.createdA person is added to Customers — on the web, in the app, by an import or by API.id, via (web or api), contact — as GET /contacts/{id} gives it, without its notes
contact.updatedA person’s name, email, VIP, tags, lists, marketing consent, opt-outs or notes change — in Customers or by API.id, changed (the fields that changed), via, contact
contact.deletedA person leaves Customers: removed from your client list, or their data erased.id, via, reason (removed or erased)

Each delivery is a POST with X-Mirage-Event and X-Mirage-Signature: t=…,v1=… — an HMAC-SHA256 of t + "." + raw body with the endpoint’s secret (code for Node.js and PHP). Answer with any 2xx within 8 seconds. A failed delivery is tried 3 times; after 20 failures in a row the endpoint is switched off until you switch it back on. Use id to ignore a repeat. tenant_id is the workspace the endpoint belongs to.

curl -X POST https://miragechat.io/api/ext/v1/webhooks \
  -H "Authorization: Bearer mck_live_…" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://crm.example.com/hooks/mirage", "events": ["message.received", "conversation.closed"],
       "description": "CRM sync"}'

201 Created
{
  "webhook": {
    "id": 41, "url": "https://crm.example.com/hooks/mirage", "events": ["message.received", "conversation.closed"],
    "description": "CRM sync", "active": true, "include_clients": false, "failures_count": 0,
    "last_delivery_at": null, "last_success_at": null, "last_failure_at": null, "created_at": "2026-10-15T09:30:00.000Z",
    "secret": "whsec_…"
  }
}
POST https://crm.example.com/hooks/mirage
Content-Type: application/json
X-Mirage-Event: message.received
X-Mirage-Signature: t=1792053669,v1=7c4e…

{
  "id": "evt_3b9f0c1d…",
  "type": "message.received",
  "created": 1792053669,
  "tenant_id": 42,
  "data": {
    "conversation_id": 88123, "account_id": 77, "account_name": "Seaside Villa", "platform": "whatsapp",
    "sender_id": "15551234567", "sender_name": "Dana Reyes", "message": "Is Thursday at 10:30 free?",
    "media_url": null, "provider_message_id": "wamid.HBgL…"
  }
}
POST https://crm.example.com/hooks/mirage
X-Mirage-Event: contact.updated

{
  "id": "evt_9a71e2…",
  "type": "contact.updated",
  "created": 1792054210,
  "tenant_id": 42,
  "data": {
    "id": "15551234567",
    "changed": ["vip", "tags"],
    "via": "web",
    "contact": { "id": "15551234567", "name": "Dana Reyes", "vip": true, "tags": ["booking", "gold"], "lists": [], … }
  }
}
curl "https://miragechat.io/api/ext/v1/webhooks/41/deliveries?success=false&limit=1" \
  -H "Authorization: Bearer mck_live_…"

200 OK
{
  "data": [
    {
      "id": 99812, "event_id": "evt_3b9f0c1d…", "event_type": "message.received", "attempt": 3,
      "success": false, "status_code": 503, "duration_ms": 412, "error": "HTTP 503", "workspace_id": null,
      "created_at": "2026-10-15T09:31:12.000Z"
    }
  ],
  "next_cursor": "eyJiIjo5OTgxMn0"
}

Workspace tools

What the web’s screens show, read through the same services, so a number here is the number on the screen.

MethodPathScopeWhat it does
GET/teamreadTeam › Agents: id, name, email, role, active, and account_ids — the channels each person sees (null = every channel).
GET/automationsreadAutomation: the rules (rule_12) and the auto-replies (auto_reply_3), with their runs, and paused when your package holds one back.
PATCH/automations/{id}writeThe switch on each row: { "enabled": true } or false. Nothing else changes here.
GET/formsreadYour WhatsApp forms, with their fields and how many were sent, answered and are waiting.
GET/forms/{id}/responsesreadEvery answer one form received, newest first, worded by the form’s own labels: limit, cursor.
GET/callsreadWhatsApp calls (Growth and Pro): direction, status, account_id, conversation_id, since, until, limit, cursor.
GET/analytics/overviewreadThe numbers of Analytics: from and to (YYYY-MM-DD, in the workspace’s time zone), platforms, compare (previous or year), unit (day or hour).
GET/accounts/{id}/insightsreadSocial Studio › Insights for one Instagram, Facebook Page or TikTok account: days 7, 28 or 90.
GET/accounts/{id}/insights/profilereadThe account’s own numbers: Instagram and Facebook (Meta’s last 28 days), TikTok (days 7, 28 or 60).
GET/tagsreadThe tags of Customers, with how many people carry each one.
GET/quick-repliesreadTeam › Quick replies, as written.
  • Calls need WhatsApp calls in your package, which are in Growth and Pro (402 module_required otherwise).
  • Analytics gives every package a period of up to 31 days that ends today or yesterday, without filters. A longer period, an older one, or platforms needs Analytics Pro.
  • Insights of one account need Social Studio’s insights in your package. The profile numbers of an Instagram account or a Facebook Page need Analytics Pro; TikTok’s are in every package. An account that is not connected answers 409 account_not_connected; one with no insights, 422 unsupported_by_platform.
  • Switching an auto-reply on or off is written to the Activity log in the key’s name, as on the web.
curl -X PATCH https://miragechat.io/api/ext/v1/automations/rule_12 \
  -H "Authorization: Bearer mck_live_…" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

200 OK
{
  "data": {
    "id": "rule_12", "kind": "rule", "name": "After-hours reply", "description": null,
    "trigger_type": "outside_business_hours", "steps": ["send_reply", "add_tag"], "account_ids": null,
    "enabled": false, "priority": 0, "run_count": 318, "last_run_at": "2026-10-14T21:07:44.000Z", "paused": null,
    "created_at": "2026-09-02T10:15:00.000Z", "updated_at": "2026-10-15T09:41:02.000Z"
  }
}

Publishing

Posting has a key of its own: publish, or publish:read for reads, made under Security › Developer › API Keys with the confirmation «Posts sent with this key are published on the selected accounts in my name.» The Publishing API is in Pro. A reseller makes one for every client workspace in Reseller › Settings and names the workspace with X-Workspace-Id.

MethodPathScopeWhat it does
GET/capabilitiespublish:readThe accounts this key can post to: formats, music, what is left today, missing permissions, TikTok routes.
POST/mediapublishUpload a file (multipart) or give a public https URL. Returns a media_id and what each platform accepts.
GET/media/{id}publishOne uploaded file and its checks.
POST/postspublishCreate a post on one or more accounts — now or at schedule_at. Send an Idempotency-Key.
GET/postspublish:readYour posts, newest first: source (api, or all — the web’s and the phone’s too), since, status, limit, cursor.
GET/posts/{id}publish:readOne post and the status of each item (one item per account and format).
DELETE/posts/{id}publishCancel what has not been published yet.
GET/posts/{id}/insightspublish:readThe numbers of each published item, per platform.
GET/musicpublish:readSounds you can use, per platform: trending, genre, duration, commercial_ok.
GET/accounts/{id}/hashtagspublish:readThe account’s own hashtags (the «Own» list Instant Post puts first).
PUT/accounts/{id}/hashtagspublishReplace the account’s own hashtags, and/or whether it also takes its group’s.
curl -X POST https://miragechat.io/api/ext/v1/posts \
  -H "Authorization: Bearer mck_live_…" \
  -H "Idempotency-Key: 2026-10-16-offer" \
  -H "Content-Type: application/json" \
  -d '{"account_ids": [12, 45], "format": "photo", "media": ["med_4821"], "caption": "Spring offer: 20% off your first visit."}'

201 Created
{
  "id": "pst_1204",
  "items": [
    { "id": "itm_88213", "account_id": 12, "platform": "instagram", "format": "photo", "status": "queued", "scheduled_at": "2026-10-16T09:00:00Z" },
    { "id": "itm_88214", "account_id": 45, "platform": "facebook",  "format": "photo", "status": "queued", "scheduled_at": "2026-10-16T09:00:00Z" }
  ]
}

Limits, TikTok’s routes, what each platform refuses, idempotency and every error code: the Publishing API reference, and its openapi.json.

Errors

Every refusal is JSON: { "error": "<code>", "code": "<the same code>", "message": "…" } — a snake_case code to branch on, the same word in code, a message that says what to do, and details where they help (field, required, provider_code, billing_url, retry_after_seconds …). A path the API does not have is 404 not_found, a body that is not JSON 400 invalid_json, and an id that is not a whole number 400 invalid_request in a body or 404 in a path — never a 500. A channel’s refusal that reaches you as 424 carries the same body (we send 424 where a 502 would be replaced by our CDN’s page).

CodeHTTPMeaning
missing_api_key401No key was sent.
invalid_api_key401The key is wrong, revoked or expired.
insufficient_scope403The key lacks the scope this needs (required names it) — or it is the other kind of key: a publishing key on the messaging API, a messaging key on the Publishing API.
reseller_scope_required403X-Workspace-Id, or a /reseller/ route, with a key that has no reseller scope.
not_a_reseller403A /reseller/ route with a key whose workspace is not a reseller now.
invalid_workspace_id400X-Workspace-Id is not a workspace id.
workspace_not_found404The id is not one of your client workspaces.
invalid_request400A field, a filter or an id is wrong (an id is a whole number); field names it where it can. A NUL character (\u0000) anywhere in the path, the query or the body is refused this way too.
invalid_json400The body is not valid JSON.
unknown_parameter · invalid_parameter · invalid_limit · invalid_platform · invalid_cursor400GET /contacts and the thread: a query parameter the route does not take (parameters lists it), one sent twice (parameter names it), a limit out of range, a platform that is no channel, a cursor this list did not give.
platform_not_available400platform=youtube — YouTube data is not available through the API.
unsupported_type · text_only400POST /messages: a type the channel does not have (the message lists the ones it has), or anything but text on Telegram, SMS or website chat.
invalid_recipient400to is not a number or id the channel can send to: for WhatsApp, the country code and the number, e.g. +355 69 123 4567.
tag_removed · idempotency_not_supported · opt_in_required400A message tag Meta removed · idempotency_key on a channel that does not take it · a marketing template without opt_in: true.
module_required402Your package does not include this. The body adds module_code, required_plan and upgrade_url.
on_hold · workspace_closed402The workspace is read-only — see Holds below. billing_url says where to renew.
payment_issue402WhatsApp would not send because of the payment method of the WhatsApp Business account. Fix it in WhatsApp Manager, then send again.
policy_refused · opted_out · marketing_opted_out403Meta refuses this number for spam or a policy (provider_code says which) · the person opted out · the person stopped marketing messages in WhatsApp.
not_found404There is no such endpoint in the API.
conversation_not_found · account_not_found · template_not_found · …_not_found404No such conversation, channel, template, contact, webhook, automation or form in this workspace.
no_channel409The conversation has no channel to send through, or its channel is disconnected.
channel_disconnected409The channel (account_id) is disconnected — or WhatsApp says the business number is not registered: connect it again in Channels.
outside_window409More than 24 hours since the person last wrote: on WhatsApp send an approved template (POST /messages/template); Instagram and Messenger refuse until they write again.
template_not_approved · idempotency_key_reused · idempotency_key_in_use · external_id_taken409The template is not approved yet · the key was used for other words · a request with this key is still being sent · your reference names another client workspace.
payload_too_large413The body is too large: 256 KB on the Publishing API’s paths, 5 MB elsewhere.
recipient_unavailable · marketing_us_blocked · authentication_needs_phone422The channel cannot deliver to this person (for WhatsApp: not on WhatsApp, or their app cannot receive it) · a marketing template to a US number · an authentication template to a username.
send_failed424The channel refused or failed, and the message was not sent — provider_code is the channel’s own code. With maybe_sent: true the channel did not answer in time and may have sent it: read the conversation before you send it again.
read_failed · ai_failed424WhatsApp did not take the read receipt · the AI could not answer. Try again.
rate_limited429Too many requests (retry_after_seconds and Retry-After say how long to wait), or the channel asked to slow down.
internal_error · …_failed500Something failed on our side; nothing was sent. Try again in a minute.
{
  "error": "invalid_request",
  "code": "invalid_request",
  "message": "status is open, pending or closed.",
  "field": "status"
}

Holds and your package

  • On hold or closed. A workspace on hold (a package or a reseller’s renewal not paid) or closed is read-only, as in the web app. Sends, changes to conversations, contacts, webhooks and automations, and posts answer 402 on_hold or 402 workspace_closed with billing_url — an address, https://miragechat.io/app/billing (null in a client workspace: its provider renews it). Switching a webhook off and deleting it always work, held or not. A held package grants no module, so the other writes — and the reads behind a module of their own, such as calls or insights — answer 402 module_required.
  • A client workspace pauses with its reseller, and is told it is «paused by its provider» — never about the reseller’s balance.
  • A route of its own module answers 402 module_required when your package does not hold it: sending on a channel needs that channel’s API, POST /ai/reply the AI API, GET /calls WhatsApp calls, and longer analytics Analytics Pro.

Rate limits

WhoLimit
Every routeAbout 300 requests a minute for each key, counted on its own for each workspace it acts for (X-Workspace-Id) — about, because each of our two servers counts on its own. Over it: 429 rate_limited, with a Retry-After header.
Every key3,000 requests a minute in all, whatever X-Workspace-Id says (the same «about»).
Unknown keys120 answers of 401 a minute from one address — then 429 until the minute has passed.
Publishing API60 requests a minute per key — per key and client workspace with X-Workspace-Id: 429 rate_limited, with Retry-After.
The channelWhen WhatsApp or another platform asks to slow down, the send answers 429 rate_limited with retry_after_seconds.
BodiesThe Publishing API’s paths take JSON bodies up to 256 KB (413 payload_too_large); files go as multipart to POST /media.

What the API does not do

YouTube data is not available through the API. YouTube’s rules for apps (YouTube API Services Developer Policies) do not allow it: no YouTube conversation, comment, automation or number is read through the API, and nothing is posted to YouTube with it — platform=youtube answers 400 platform_not_available, a YouTube account in POST /posts answers 422 unsupported_by_platform. Post to YouTube from Instant Post in MirageCHAT.

  • Contacts. No external ids or custom fields, and no erasing of a person’s data — that stays in the web app.
  • Contact events come from Customers and from this API. They are not sent yet for a tag or VIP an automation sets, a STOP or START the person sends, or a name synced from the WhatsApp Business app on your phone.
  • AI in a client workspace. Client workspaces have no AI assistant, so POST /ai/reply answers 402 module_required there.
  • Prices and packages, and switching a workspace on as a reseller, are ours. The API does not change them.

Try MirageCHAT free for 14 days

No credit card is required, and then Free — 2 accounts, the inbox and Instant Post. Provider charges such as WhatsApp or SMS stay on your own provider account.

Start free trial