Publishing API
Post to Instagram, Facebook and TikTok from your own systems: every endpoint with curl, TikTok routes, signed webhooks and error codes.
Read the Publishing API reference — Publishing APIREST · JSON · signed webhooksMessaging · contacts · publishing · resellersUpdated
Post to Instagram, Facebook and TikTok from your own systems: every endpoint with curl, TikTok routes, signed webhooks and error codes.
Read the Publishing API reference — Publishing APIClient workspaces, connect links, their people, your statement and paid channels — one key for every client.
Read the Reseller API reference — Reseller APIMake a key, limit it, rotate or revoke it, and send events to your server — step by step.
Open the Help guide — API keys and webhooksIn 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.
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).
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.
| Scope | What it allows |
|---|---|
read | Every GET of the API below: channels, conversations, messages, contacts, webhooks, team, automations, forms, calls, analytics and insights. |
write | Everything that sends or changes something: messages, a conversation’s status and assignee, contacts and notes, webhooks, the automation switch. |
reseller | A reseller’s key: the /reseller/ routes, and X-Workspace-Id to act as one of its client workspaces. |
publish · publish:read | The 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.
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.
| Key | Made in | Scopes | What it does |
|---|---|---|---|
| Reseller key | Reseller › Settings › Create reseller key | read · write · reseller | The /reseller/ routes — and, inside any client workspace, messages, conversations, contacts, webhooks and the workspace tools. It never posts. |
| Publishing key | Reseller › Settings › Create publishing key, after ticking «Posts sent with this key are published on the selected accounts in my name.» | publish · reseller | Posts 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.
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.
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /reseller/me | read · reseller | Your account: status, prices, balance, what a month costs at today’s channels, and what falls due in the next 7 days. |
GET | /reseller/workspaces | read · reseller | Your client workspaces, newest first. include_closed=0 leaves the closed ones out; external_id finds one by your reference. |
POST | /reseller/workspaces | write · reseller | Create 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 · reseller | One client workspace. |
PATCH | /reseller/workspaces/{id} | write · reseller | Rename it, or change your reference (external_id; null clears it). |
DELETE | /reseller/workspaces/{id} | write · reseller | Close it: read-only at once, its channels switched off, its data kept for 90 days. |
POST | /reseller/workspaces/{id}/connect-link | write · reseller | A 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/revoke | write · reseller | Every open connect link of the workspace stops working at once. |
GET | /reseller/workspaces/{id}/users | read · reseller | The people who sign in to it: role, switched off or not, last sign-in. |
POST | /reseller/workspaces/{id}/users | write · reseller | Add a person: email, password, role (admin or agent) and name. Nothing is emailed to them. |
PATCH | /reseller/workspaces/{id}/users/{userId} | write · reseller | Switch a person off or on, or change their role, password or name. |
GET | /reseller/channels | read · reseller | Every paid channel of your client workspaces, with paid_until and monthly_usd — to bill your own clients by. |
GET | /reseller/statement | read · reseller | What 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/settings | read · reseller | Your name and logo on the connect page. |
PATCH | /reseller/settings | write · reseller | Change 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"
}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.
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /me | any messaging key | The key’s workspace and its scopes. |
GET | /accounts | read | Your connected channels: id, name, platform, number or username — the id is the account_id you send with. YouTube channels are not listed. |
GET | /conversations | read | The 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} | read | One 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}/messages | read | The 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} | write | Change status (open, pending or closed) and assigned_to (a team member’s id or email, or null), as the Inbox does. |
POST | /messages | write | Send 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 | /templates | read | Your WhatsApp message templates, with their status. |
POST | /messages/template | write | Send an approved WhatsApp template, also to a number that never wrote. A marketing template needs opt_in: true. |
POST | /messages/read | write | Mark a WhatsApp message as read (blue ticks), with a typing indicator if you like. |
POST | /conversations/names | write | Fill in WhatsApp names by phone number — only where the conversation has no real name yet. |
POST | /ai/reply | write | An AI reply written with the account’s prompt and knowledge, for your own system to send. |
GET | /optouts · /optouts/{platform}/{psid} | read | The opt-out list, or whether one person is on it. |
POST · DELETE | /optouts · /optouts/{platform}/{psid} | write | Record 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_…"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.
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /accounts/{id}/media/{mediaId}/comments | read | Instagram: the comments on a post. |
POST | /accounts/{id}/comments/{cid}/reply | write | Instagram: reply under a comment. |
POST | /accounts/{id}/comments/{cid}/private-reply | write | Instagram: answer a comment in a direct message. |
POST | /accounts/{id}/comments/{cid}/hide | write | Instagram: hide a comment (send hide: false to show it again). |
DELETE | /accounts/{id}/comments/{cid} | write | Instagram: delete a comment. |
GET · PUT | /accounts/{id}/greeting | read · write | Instagram: the ice breakers and the persistent menu. |
GET · PUT | /accounts/{id}/business-profile | read · write | WhatsApp: the business profile. |
GET · POST · DELETE | /accounts/{id}/qr · /accounts/{id}/qr/{code} | read · write | WhatsApp: QR codes that open a chat with a message ready. |
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.
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /contacts | read | Find people: q, tag, platform, updated_since, limit (up to 100), cursor. |
GET | /contacts/lists | read | Your lists, with how many people each one holds. |
GET | /contacts/{id} | read | One person, with their notes. |
POST | /contacts | write | Add 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} | write | name (null gives the name back to their phone or profile), email, vip, tags_add, tags_remove, consent, lists_add, lists_remove. |
POST | /contacts/{id}/notes | write | A note on one of their conversations: content, pinned, conversation_id. |
DELETE | /contacts/{id} | write | Remove 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".
PATCH name does; name_kept says when a name you sent was not used.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.400 not_editable). An existing number without upsert is 409 contact_exists.423 conversation_locked).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
}Your server gets each event as a signed POST. Make endpoints under Security › Developer › Outbound Webhooks, or with these routes.
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /webhooks | read | Your webhook endpoints — never their signing secret. |
GET | /webhooks/event-types | read | Every event an endpoint can subscribe to. |
POST | /webhooks | write | Make one: url (a public https:// address), events, description, include_clients. The answer shows its signing secret, once. |
PATCH | /webhooks/{id} | write | Change url, events, description, active or include_clients. |
DELETE | /webhooks/{id} | write | Delete it. |
POST | /webhooks/{id}/test | write | Deliver one signed test.ping now: delivered, status_code and error say how it went. |
GET | /webhooks/{id}/deliveries | read | Its 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. |
POST /webhooks only. The list never shows it again — in the API or in the web app.active: false) never do — nor does a hold.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.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).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.| Event | When | data |
|---|---|---|
conversation.created | A 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.assigned | A 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.closed | A conversation is closed, in the Inbox or by the API. | conversation_id, account_id, platform, previous_status, via (web or api) |
message.received | A customer writes. | conversation_id, account_id, account_name, platform, sender_id, sender_name, message, media_url, provider_message_id |
message.sent | A 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.added | Can be subscribed to. MirageCHAT does not send them yet. | — |
post.queued · post.awaiting_approval · post.sent_to_drafts · post.published · post.failed · post.cancelled | A post’s item changes status — made through the API, on the web or on the phone. | See the Publishing API reference |
account.connected · account.disconnected | A channel is connected (or switched back on), or disconnected. | workspace_id, external_id, account_id, platform, name |
contact.created | A 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.updated | A 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.deleted | A 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"
}What the web’s screens show, read through the same services, so a number here is the number on the screen.
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /team | read | Team › Agents: id, name, email, role, active, and account_ids — the channels each person sees (null = every channel). |
GET | /automations | read | Automation: 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} | write | The switch on each row: { "enabled": true } or false. Nothing else changes here. |
GET | /forms | read | Your WhatsApp forms, with their fields and how many were sent, answered and are waiting. |
GET | /forms/{id}/responses | read | Every answer one form received, newest first, worded by the form’s own labels: limit, cursor. |
GET | /calls | read | WhatsApp calls (Growth and Pro): direction, status, account_id, conversation_id, since, until, limit, cursor. |
GET | /analytics/overview | read | The 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}/insights | read | Social Studio › Insights for one Instagram, Facebook Page or TikTok account: days 7, 28 or 90. |
GET | /accounts/{id}/insights/profile | read | The account’s own numbers: Instagram and Facebook (Meta’s last 28 days), TikTok (days 7, 28 or 60). |
GET | /tags | read | The tags of Customers, with how many people carry each one. |
GET | /quick-replies | read | Team › Quick replies, as written. |
402 module_required otherwise).platforms needs Analytics Pro.409 account_not_connected; one with no insights, 422 unsupported_by_platform.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"
}
}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.
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /capabilities | publish:read | The accounts this key can post to: formats, music, what is left today, missing permissions, TikTok routes. |
POST | /media | publish | Upload a file (multipart) or give a public https URL. Returns a media_id and what each platform accepts. |
GET | /media/{id} | publish | One uploaded file and its checks. |
POST | /posts | publish | Create a post on one or more accounts — now or at schedule_at. Send an Idempotency-Key. |
GET | /posts | publish:read | Your posts, newest first: source (api, or all — the web’s and the phone’s too), since, status, limit, cursor. |
GET | /posts/{id} | publish:read | One post and the status of each item (one item per account and format). |
DELETE | /posts/{id} | publish | Cancel what has not been published yet. |
GET | /posts/{id}/insights | publish:read | The numbers of each published item, per platform. |
GET | /music | publish:read | Sounds you can use, per platform: trending, genre, duration, commercial_ok. |
GET | /accounts/{id}/hashtags | publish:read | The account’s own hashtags (the «Own» list Instant Post puts first). |
PUT | /accounts/{id}/hashtags | publish | Replace 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.
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).
| Code | HTTP | Meaning |
|---|---|---|
missing_api_key | 401 | No key was sent. |
invalid_api_key | 401 | The key is wrong, revoked or expired. |
insufficient_scope | 403 | The 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_required | 403 | X-Workspace-Id, or a /reseller/ route, with a key that has no reseller scope. |
not_a_reseller | 403 | A /reseller/ route with a key whose workspace is not a reseller now. |
invalid_workspace_id | 400 | X-Workspace-Id is not a workspace id. |
workspace_not_found | 404 | The id is not one of your client workspaces. |
invalid_request | 400 | A 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_json | 400 | The body is not valid JSON. |
unknown_parameter · invalid_parameter · invalid_limit · invalid_platform · invalid_cursor | 400 | GET /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_available | 400 | platform=youtube — YouTube data is not available through the API. |
unsupported_type · text_only | 400 | POST /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_recipient | 400 | to 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_required | 400 | A message tag Meta removed · idempotency_key on a channel that does not take it · a marketing template without opt_in: true. |
module_required | 402 | Your package does not include this. The body adds module_code, required_plan and upgrade_url. |
on_hold · workspace_closed | 402 | The workspace is read-only — see Holds below. billing_url says where to renew. |
payment_issue | 402 | WhatsApp 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_out | 403 | Meta refuses this number for spam or a policy (provider_code says which) · the person opted out · the person stopped marketing messages in WhatsApp. |
not_found | 404 | There is no such endpoint in the API. |
conversation_not_found · account_not_found · template_not_found · …_not_found | 404 | No such conversation, channel, template, contact, webhook, automation or form in this workspace. |
no_channel | 409 | The conversation has no channel to send through, or its channel is disconnected. |
channel_disconnected | 409 | The channel (account_id) is disconnected — or WhatsApp says the business number is not registered: connect it again in Channels. |
outside_window | 409 | More 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_taken | 409 | The 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_large | 413 | The body is too large: 256 KB on the Publishing API’s paths, 5 MB elsewhere. |
recipient_unavailable · marketing_us_blocked · authentication_needs_phone | 422 | The 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_failed | 424 | The 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_failed | 424 | WhatsApp did not take the read receipt · the AI could not answer. Try again. |
rate_limited | 429 | Too many requests (retry_after_seconds and Retry-After say how long to wait), or the channel asked to slow down. |
internal_error · …_failed | 500 | Something 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"
}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.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.| Who | Limit |
|---|---|
| Every route | About 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 key | 3,000 requests a minute in all, whatever X-Workspace-Id says (the same «about»). |
| Unknown keys | 120 answers of 401 a minute from one address — then 429 until the minute has passed. |
| Publishing API | 60 requests a minute per key — per key and client workspace with X-Workspace-Id: 429 rate_limited, with Retry-After. |
| The channel | When WhatsApp or another platform asks to slow down, the send answers 429 rate_limited with retry_after_seconds. |
| Bodies | The Publishing API’s paths take JSON bodies up to 256 KB (413 payload_too_large); files go as multipart to POST /media. |
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.
POST /ai/reply answers 402 module_required there.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