API Reference

Wapisimo API Documentation

RESTful API for WhatsApp messaging integration

Wapisimo is a multi-tenant WhatsApp API. Create phone connections, pair them by scanning a QR code, then send and receive WhatsApp messages over HTTP. Inbound activity is pushed to your webhooks and retained in a queryable event history.

Base URL:https://api.wapisimo.dev/v1

Authentication

All requests authenticate with a bearer token in the Authorization header.

Authorization: Bearer YOUR_API_KEY

There are three kinds of key, and which one a route accepts is part of that route's contract:

  • Account key — found in dashboard Settings. Covers everything the account owns. Required for creating and deleting phones, and for the account-wide event read. Rotate it from the same screen if it leaks: the replacement is live immediately and the old key starts returning 401 on the next request, with no grace period.
  • Phone key — returned when you create a phone. Scoped to that one phone.
  • Group key — belongs to a phone group, and is scoped to that group's members.

Routes shaped /v1/{phone_id}/... accept either that phone's key or the account key. Routes shaped /v1/{group_id}/... accept either that group's key or the account key. /v1/phones and /v1/events accept the account key only, because a per-phone key must not be able to widen its own scope to the rest of the account.

X-API-Key is used internally by Wapisimo. It is not a customer-facing header and will not authenticate you.

A missing, unknown, or wrong-scope key returns 401. A malformed instance id in the path also returns 401 rather than 404, so a wrong id and a wrong key are indistinguishable to the caller by design.

Account Endpoints

Manage the phones in your account. These require the account key.

POST/v1/phonesAccount key

Create Phone

Create a new phone in your account. It starts disconnected and must be paired by fetching a QR code and scanning it from WhatsApp on the handset.

Body Parameters

NameTypeDescription
phone_number*string

Digits only, country code included, no "+".

namestring

Label shown in the dashboard.

Request Body

{
  "phone_number": "5215512345678",
  "name": "Support line"
}

Response (201)

{
  "phone_number": "5215512345678",
  "name": "Support line",
  "id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "api_key": "a91c4e77-2b58-4f03-9d16-6c8e2a4b5f90"
}
  • The number comes back as phone_number here, but as number everywhere else. This response is assembled by hand rather than returned from the row, and the naming has never been reconciled.

  • The api_key in the response is this phone's key. You do not have to store it — List Phones returns it too — but treat it as a credential either way.

  • Creating a phone consumes a subscription slot.

GET/v1/phonesAccount key

List Phones

List every phone in the account that has not been deleted.

Response (200)

[
  {
    "id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
    "number": "5215512345678",
    "name": "Support line",
    "status": "connected",
    "provider": "cloud_api",
    "api_key": "a91c4e77-2b58-4f03-9d16-6c8e2a4b5f90",
    "user_id": "d2b8f1a6-3c07-4e95-b8d4-0f6a2c9e1b53"
  }
]
  • status is connected or disconnected. Deleted phones are omitted from this list.

  • provider is cloud_api for official WhatsApp Cloud API connections, or baileys for QR-linked (WhatsApp Web) phones.

  • A newly created phone is disconnected until somebody scans its QR code.

  • Each phone's api_key is included, so a lost phone key can be recovered here with the account key.

  • The response can include fields not listed above. Anything undocumented may change without notice and should not be built on.

DELETE/v1/phones/{phone_id}Account key

Delete Phone

Close the WhatsApp session and soft-delete the phone. It stops appearing in List Phones, stops counting toward active usage, and frees a subscription slot.

Path Parameters

NameTypeDescription
phone_id*uuid

The phone to delete.

Response (200)

{
  "message": "Phone deleted"
}
  • Not reversible. Reconnecting that number means creating a new phone and pairing it again.

Phone Endpoints

Send messages and manage a single phone. These accept that phone's key or the account key.

POST/v1/{phone_id}/sendPhone or account key

Send Text Message

Queue a text message. The response means accepted for delivery, not delivered — watch the event history or your webhook for the outcome.

Path Parameters

NameTypeDescription
phone_id*uuid

Sending phone. A group id also works; see Send via Group.

Body Parameters

NameTypeDescription
to*string

Recipient in international format.

message*string

Text body. Required unless location is given.

Request Body

{
  "to": "+5215512345678",
  "message": "Hello from Wapisimo!"
}

Response (202)

{
  "status": "queued",
  "message": "Message has been queued for delivery",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34"
}
  • 202 means queued. A message can still fail afterwards — WhatsApp may reject it, or the phone may not be connected.

  • QR-linked phones: messages drain at roughly one per second per phone to stay under WhatsApp rate limiting, and with Natural Mode on (the default) delivery is deliberately delayed further. Cloud API phones bypass both — the send goes straight to Meta, which does its own throttling.

  • Official Cloud API phones: inside the 24-hour customer-service window the text sends as-is. Outside the window, if the text matches one of your approved templates it is converted to that template automatically (variables extracted from the text); otherwise WhatsApp rejects it. See Send Template Message to send a template explicitly.

  • The text is matched against a template two ways: its BODY on its own, and the whole rendered message (header, body, footer). So both the body text you would have sent before, and a message copied verbatim out of a conversation, resolve to the same template. A dynamic TEXT header is only recoverable from the second form, because that is the only one that contains its value.

  • Some templates can never be reached from text alone: a media header needs an asset, and a dynamic-URL, copy-code, flow, catalog or MPM button needs a parameter. None of those appear anywhere in a sentence, so those templates are skipped rather than sent half-filled for WhatsApp to reject. To reach one, either send it explicitly (see Send Template Message) or pass a template object alongside your message carrying just those values — with no name, it supplies the un-inferable parts for whichever template the text matches.

POST/v1/{phone_id}/sendPhone or account key

Send Media Message

Queue an image, video, audio file, or document. The file is fetched from your URL and forwarded to WhatsApp.

Path Parameters

NameTypeDescription
phone_id*uuid

Sending phone or group id.

Body Parameters

NameTypeDescription
to*string

Recipient in international format.

mediaUrl*string

Publicly reachable URL. Wapisimo fetches it server-side.

mediaType*string

One of image, video, audio, document.

messagestring

Caption.

Request Body

{
  "to": "+5215512345678",
  "mediaUrl": "https://example.com/invoice.pdf",
  "mediaType": "document",
  "message": "Your invoice"
}

Response (202)

{
  "status": "queued",
  "message": "Message has been queued for delivery",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34"
}
  • mediaUrl must be reachable without authentication — the fetch carries no credentials of yours.

POST/v1/{phone_id}/sendPhone or account key

Send Location Message

Queue a location pin.

Path Parameters

NameTypeDescription
phone_id*uuid

Sending phone or group id.

Body Parameters

NameTypeDescription
to*string

Recipient in international format.

location*object

latitude and longitude as numbers; optional name and address labels.

Request Body

{
  "to": "+5215512345678",
  "location": {
    "latitude": 19.4326,
    "longitude": -99.1332,
    "name": "Zócalo",
    "address": "Ciudad de México"
  }
}

Response (202)

{
  "status": "queued",
  "message": "Message has been queued for delivery",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34"
}
  • A send must carry message, location, or a named template. Sending none of them returns 400. (A template with no name only supplies extra values for a text send, so it does not count on its own.)

GET/v1/verifyAny key

Verify Number

Check whether a number is registered on WhatsApp.

Query Parameters

NameTypeDescription
phone*string

Number to check, digits with country code.

Response (200)

{
  "hasWhatsApp": true,
  "phone": "[email protected]",
  "details": [
    { "jid": "[email protected]", "exists": true }
  ]
}
  • verify sits where an instance id normally goes, so the path is /v1/verify, not /v1/{phone_id}/verify.

GET/v1/{phone_id}/qrPhone or account key

Get QR Code

Fetch the pairing QR code for a phone. Scan it from WhatsApp on the handset (Settings → Linked devices) to move the phone to `connected`.

Path Parameters

NameTypeDescription
phone_id*uuid

Phone to pair.

Response (200)

{
  "status": "disconnected",
  "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."
}
  • QR codes are short-lived. Poll for a fresh one if the user does not scan in time.

  • Rate limited to 10 requests per minute.

GET/v1/{phone_id}/groupsPhone or account key

List WhatsApp Groups

List the WhatsApp group chats this phone belongs to, with metadata and participants.

Path Parameters

NameTypeDescription
phone_id*uuid

Connected phone to read from.

Response (200)

[
  {
    "id": "[email protected]",
    "subject": "Team chat",
    "owner": "[email protected]",
    "creation": 1672531200,
    "size": 25,
    "participants": [
      { "id": "[email protected]", "admin": "superadmin" },
      { "id": "[email protected]", "admin": null }
    ]
  }
]
  • These are WhatsApp group chats, which are a different thing from Wapisimo phone groups. WhatsApp group ids end in @g.us.

  • Requires the phone to be connected.

GET/v1/{phone_id}/webhookPhone or account key

List Webhooks

List the webhooks registered for a phone.

Path Parameters

NameTypeDescription
phone_id*uuid

Phone whose webhooks to list.

Response (200)

[
  {
    "id": "8c2d5f19-7a3b-4e60-9f81-2d4c6b8a0e75",
    "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
    "url": "https://example.com/hooks/whatsapp",
    "name": "Order updates",
    "type": "new_messages",
    "createdAt": "2026-07-30T05:38:03.105Z",
    "status": "active",
    "lastSeen": null
  }
]
  • status and lastSeen are legacy columns that nothing currently maintains. They are returned because the row is selected whole, but do not treat them as delivery health — read the event history instead.

POST/v1/{phone_id}/webhookPhone or account key

Add Webhook

Register a URL to receive this phone's events.

Path Parameters

NameTypeDescription
phone_id*uuid

Phone to attach the webhook to.

Body Parameters

NameTypeDescription
url*string

HTTPS endpoint that receives POSTed events.

typestring

new_messages (default) for inbound messages only, all for every event, or proxy (official Cloud API phones) to forward Meta's raw webhook payload verbatim.

namestring

Label shown in the dashboard.

Request Body

{
  "url": "https://example.com/hooks/whatsapp",
  "type": "new_messages",
  "name": "Order updates"
}

Response (200)

{
  "success": true,
  "webhook": {
    "id": "8c2d5f19-7a3b-4e60-9f81-2d4c6b8a0e75",
    "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
    "url": "https://example.com/hooks/whatsapp",
    "name": "Order updates",
    "type": "new_messages",
    "createdAt": "2026-07-30T05:38:03.105Z",
    "status": null,
    "lastSeen": null
  }
}
  • The created row is nested under webhook, not returned at the top level.

  • type is constrained to new_messages, all, or proxy; anything else is rejected with 400. Omitting it gives new_messages.

  • proxy (official Cloud API phones only) delivers Meta's Cloud API webhook payload rather than the normalized Wapisimo shape, so an existing Cloud API integration keeps working as-is. new_messages/all deliver the normalized shape.

  • A proxy delivery only carries a verifiable X-Hub-Signature-256 when Meta's delivery covered exactly one of your numbers. When Meta batches several numbers into one delivery, each phone receives a rebuilt body containing only its own entry.changes — which no longer matches Meta's signature, so the header is omitted rather than sent wrong. Verify it when present; do not require it.

  • Connection events reach every webhook on the phone regardless of type.

  • Rate limited to 60 requests per hour.

DELETE/v1/{phone_id}/webhook/{webhook_id}Phone or account key

Delete Webhook

Remove a registered webhook.

Path Parameters

NameTypeDescription
phone_id*uuid

Phone the webhook belongs to.

webhook_id*uuid

Webhook to remove.

Response (200)

{
  "success": true
}
  • Idempotent in effect: deleting an id that does not exist on this phone still returns success: true.

POST/v1/{phone_id}/resyncPhone or account key

Resync Phone

Tear down the WhatsApp session, clear stored pairing credentials, and start a fresh one so a new QR code can be issued.

Path Parameters

NameTypeDescription
phone_id*uuid

Phone to resync.

Response (200)

{
  "message": "Phone resynced successfully"
}
  • Destructive: this discards pairing credentials, so somebody has to scan a new QR code before the phone can send again.

  • To recover a phone that merely dropped its connection, call Get QR Code first — it resumes the stored session without forcing a re-scan.

Official Cloud API

A phone is either QR-linked (provider: "baileys", paired by scanning a code) or an official Cloud API connection (provider: "cloud_api", your own Meta app and WhatsApp Business Account). List Phones reports which. Everything in this reference works on both unless it says otherwise; what follows is only where they diverge.

QR-linkedOfficial Cloud API
ConnectingGet QR Code, scanned from the handsetcallback URL verified in Meta's dashboard
Checking healthphone status, re-pair with Get QR CodeCheck Cloud Connection
Messaging outside the 24-hour windowno restrictionan approved template is required
Send pacing~1/second per phone, plus Natural Modestraight to Meta, which does its own throttling
Inbound mediamirrored to a Wapisimo URLa Meta media_id you fetch from Graph yourself

The callback URL

Each Cloud API connection gets its own callback URL, which the dashboard generates when you add the phone:

https://api.wapisimo.dev/v1/cloud/wh/{callback_secret}

You do not call this endpoint — Meta does. Paste it into your app's WhatsApp webhook configuration along with the verify token from the same screen, and subscribe the app to your WABA. The {callback_secret} is what identifies the connection, so treat the whole URL as a credential: anyone holding it knows where your events go.

Verification (`GET`). Meta sends hub.mode, hub.verify_token and hub.challenge. A matching verify token echoes the challenge back with 200; anything else is 403. A successful handshake also marks the phone connected, so a verified webhook is what moves a new connection out of disconnected.

Delivery (`POST`). Inbound messages and delivery statuses arrive here. When the connection has an App Secret stored, the X-Hub-Signature-256 header is verified and a bad signature is rejected with 401. Every other outcome — including an unrecognized secret — answers 200, so Meta never retries into a backlog or disables the subscription.

Deliveries are routed per number by value.metadata.phone_number_id, not by whichever phone owns the callback URL, so one app webhook can serve every number on the account. A number belonging to a different account is dropped rather than delivered.

POST/v1/cloud/check/{phone_id}Phone or account key

Check Cloud Connection

Ask Meta whether the stored token and phone number id still work, and reconcile the phone's status with the answer. Official Cloud API phones only.

Path Parameters

NameTypeDescription
phone_id*uuid

Cloud phone to check.

Response (200)

{
  "ok": true,
  "status": "connected",
  "display_phone_number": "+52 1 55 1234 5678",
  "verified_name": "Acme Support",
  "quality_rating": "GREEN",
  "warnings": []
}
  • This endpoint writes. A successful check sets the phone to connected and refreshes the stored display number; a rejection from Meta sets it to disconnected.

  • A rejection is still 200 — read ok, not the status code. {"ok": false, "status": "disconnected", "error": "...", "code": 190} is what a revoked token looks like. Missing credentials come back the same way with "reason": "missing_credentials".

  • The one case that does not touch the phone's status is 502 graph_unreachable, which means Wapisimo could not reach Meta. Treat it as unknown and retry.

  • warnings flags a connection that authenticates but will not work end to end: an App Secret from a different app than the token (inbound signatures will fail), or a WABA with no subscribed app or one subscribed via a different app (no inbound at all). Empty is the healthy case.

  • 404 means the phone id is not a Cloud API connection. QR-linked phones use Get QR Code instead.

Message Templates

Official WhatsApp Cloud API phones send pre-approved message templates — the only way to message a contact outside the 24-hour customer-service window. Create and get them approved in WhatsApp Manager, list them, then send by name. (Plain Send Text Message also auto-converts matching text to an approved template when you are outside the window.)

POST/v1/{phone_id}/sendPhone or account key

Send Template Message

Queue an approved WhatsApp message template. Official Cloud API phones only. Templates deliver both inside and outside the 24-hour customer-service window — they are the only way to reach a contact who has not messaged you in the last 24 hours.

Path Parameters

NameTypeDescription
phone_id*uuid

Sending cloud phone.

Body Parameters

NameTypeDescription
to*string

Recipient in international format.

template*object

name and language (e.g. es_MX) of an APPROVED template, plus the values its blocks need: variables (body placeholders {{1}}, {{2}}… in order), header (a TEXT header's single variable as a string, or { "image": { "link": "…" } } for a media header), and buttons (parameters for buttons that take one). components is still accepted as a raw Graph components array for full control.

Request Body

{
  "to": "+5215512345678",
  "template": {
    "name": "order_update",
    "language": "es_MX",
    "header": "1234",
    "variables": ["Ana", "hoy"],
    "buttons": [
      { "index": 0, "type": "url", "parameter": "1234" }
    ]
  }
}

Response (202)

{
  "status": "queued",
  "message": "Message has been queued for delivery",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34"
}
  • The template must already be APPROVED (list them with GET /v1/cloud/templates/{phone_id}).

  • variables fills the body placeholders in order. Only pass header when the template actually needs it: a static TEXT header is baked into the approved template and takes no parameter, while a media header always needs one.

  • buttons entries are { index, type, parameter }. index is the button's position in the template (0-based) and defaults to its position in the array. type is url for a dynamic URL suffix, or copy_code, quick_reply, flow, catalog, mpm. Buttons that take no parameter (a static URL, a phone number) are omitted.

  • A FOOTER never takes a parameter, so there is no footer field.

  • Meta rejects a partially filled template, so send every value the template asks for.

  • Prefer the plain Send Text Message when you can: outside the window, matching text is auto-converted to an approved template, so the ordinary text call keeps working.

GET/v1/cloud/templates/{phone_id}Phone or account key

List Templates

List the message templates on a cloud phone's WhatsApp Business Account, with their approval status. Official Cloud API phones only.

Path Parameters

NameTypeDescription
phone_id*uuid

Cloud phone whose WABA templates to list.

Response (200)

{
  "templates": [
    {
      "name": "order_update",
      "status": "APPROVED",
      "category": "UTILITY",
      "language": "es_MX",
      "components": [
        { "type": "BODY", "text": "Tu pedido {{1}} va en camino, llega el {{2}}." }
      ]
    }
  ]
}
  • Only templates whose status is APPROVED can be sent.

  • Create and edit templates in WhatsApp Manager; new ones start PENDING until Meta reviews them.

  • Returns at most 100 templates and has no pagination. A WABA with more than that gets a partial list with no indication it was truncated. Template *sending* is unaffected — the matcher behind Send Text Message pages through the whole set separately.

Event History

Every phone keeps a queryable history of what happened on it: inbound and outbound messages, and connection state changes. The same reader backs three scopes, so the filters and the response shape are identical across all of them and only the breadth changes:

ScopePathKeyCovers
Phone/v1/{phone_id}/eventsphone or accountone phone
Group/v1/{group_id}/eventsgroup or accountevery member of that group
Account/v1/eventsaccount onlyevery phone you own

Results are paginated with a keyset cursor. Pass the next_cursor from a response as cursor on the next request, and stop when has_more is false. Do not try to parse the cursor: it is an opaque base64 token whose contents are an implementation detail.

Because group and account reads merge several phones into one timeline, read phone_id on each row to tell them apart. On a single-phone read that field is constant and can be ignored.

Rate limit: 60 requests per minute. Group reads are charged to the account rather than the group, so fanning out over many groups does not multiply your budget.

GET/v1/{phone_id}/eventsPhone or account key

List Events for a Phone

Read one phone's event history, newest first by default.

Path Parameters

NameTypeDescription
phone_id*uuid

Phone whose history to read.

Query Parameters

NameTypeDescription
conversation_idstring

Exact WhatsApp conversation JID, e.g. [email protected]. Max 200 characters. Cloud API rows store a bare wa_id here instead, so match on digits alone for those.

phone_numberstring

Filter by contact number, digits with country code. Does not match group conversations.

typeenum

message or connection.

subtypestring

Event subtype such as text or send_failed. Letters, digits, _ and -; max 50 characters.

from_meboolean

true for outbound, false for inbound.

sinceISO 8601

Only events at or after this timestamp.

untilISO 8601

Only events at or before this timestamp.

phone_statusstring

Group and account reads only. Comma-separated connected and/or disconnected; defaults to both. Rejected with 400 on a phone read.

orderenum

desc (default) or asc, by timestamp then event_id.

limitinteger

Page size. Default 50, maximum 200. Out-of-range and unparseable values fall back to 50.

cursorstring

Opaque token from the previous response's next_cursor.

Response (200)

{
  "data": [
    {
      "event_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
      "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
      "conversation_id": "[email protected]",
      "message_id": "3EB01234567890ABCDEF",
      "type": "message",
      "subtype": "text",
      "from_jid": "[email protected]",
      "from_me": false,
      "content": "Hello!",
      "media_url": null,
      "thumbnail": null,
      "timestamp": "2026-07-30T10:30:00.000Z"
    }
  ],
  "next_cursor": "eyJ0cyI6IjIwMjYtMDctMzBUMTA6MzA6MDAuMDAwWiIsImlkIjoiYTFiMmMzZDQtZTVmNi00ODkwLWFiY2QtZWYxMjM0NTY3ODkwIn0=",
  "has_more": true
}
  • phone_status is rejected here with 400 — it only means something on the group and account reads.

  • Event rows do not include the details object that webhook payloads carry.

  • All three scopes return the same row. Event Samples shows a complete row per subtype, including which fields are empty on each.

GET/v1/{group_id}/eventsGroup or account key

List Events for a Phone Group

Read the merged event history of every phone in a group, interleaved into one timeline. Use `phone_id` on each row to tell members apart.

Path Parameters

NameTypeDescription
group_id*uuid

Phone group whose members to read.

Query Parameters

NameTypeDescription
conversation_idstring

Exact WhatsApp conversation JID, e.g. [email protected]. Max 200 characters. Cloud API rows store a bare wa_id here instead, so match on digits alone for those.

phone_numberstring

Filter by contact number, digits with country code. Does not match group conversations.

typeenum

message or connection.

subtypestring

Event subtype such as text or send_failed. Letters, digits, _ and -; max 50 characters.

from_meboolean

true for outbound, false for inbound.

sinceISO 8601

Only events at or after this timestamp.

untilISO 8601

Only events at or before this timestamp.

phone_statusstring

Group and account reads only. Comma-separated connected and/or disconnected; defaults to both. Rejected with 400 on a phone read.

orderenum

desc (default) or asc, by timestamp then event_id.

limitinteger

Page size. Default 50, maximum 200. Out-of-range and unparseable values fall back to 50.

cursorstring

Opaque token from the previous response's next_cursor.

Response (200)

{
  "data": [
    {
      "event_id": "b2c3d4e5-f6a7-4901-bcde-f12345678901",
      "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
      "conversation_id": "[email protected]",
      "message_id": "3EB0234567890ABCDEF1",
      "type": "message",
      "subtype": "text",
      "from_jid": "[email protected]",
      "from_me": true,
      "content": "On our way",
      "media_url": null,
      "thumbnail": null,
      "timestamp": "2026-07-30T10:31:00.000Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
  • Disconnected members are included by default — a phone that dropped off last week still has history worth reading. Deleted phones never are.

  • Narrow the member set with phone_status.

  • This read fans out across every member, so its rate limit is keyed on the account rather than the group. Group and account reads share one 60/minute budget.

  • Unlike a send, an events read never selects a single member and never advances the round-robin pointer.

GET/v1/eventsAccount key

List Events Across the Account

Read the merged event history of every phone in the account. The widest read available, and the right one for building an inbox or syncing to your own store.

Query Parameters

NameTypeDescription
conversation_idstring

Exact WhatsApp conversation JID, e.g. [email protected]. Max 200 characters. Cloud API rows store a bare wa_id here instead, so match on digits alone for those.

phone_numberstring

Filter by contact number, digits with country code. Does not match group conversations.

typeenum

message or connection.

subtypestring

Event subtype such as text or send_failed. Letters, digits, _ and -; max 50 characters.

from_meboolean

true for outbound, false for inbound.

sinceISO 8601

Only events at or after this timestamp.

untilISO 8601

Only events at or before this timestamp.

phone_statusstring

Group and account reads only. Comma-separated connected and/or disconnected; defaults to both. Rejected with 400 on a phone read.

orderenum

desc (default) or asc, by timestamp then event_id.

limitinteger

Page size. Default 50, maximum 200. Out-of-range and unparseable values fall back to 50.

cursorstring

Opaque token from the previous response's next_cursor.

Response (200)

{
  "data": [
    {
      "event_id": "c3d4e5f6-a7b8-4012-cdef-123456789012",
      "phone_id": "9e1d4c73-5a28-4b60-8f31-7c2e5a9d0b46",
      "conversation_id": "[email protected]",
      "message_id": "3EB0345678901ABCDEF2",
      "type": "message",
      "subtype": "image",
      "from_jid": "[email protected]",
      "from_me": false,
      "content": "",
      "media_url": "https://files.wapisimo.dev/eyJtZWRpYUtleSI6IjxvcGFxdWUtdG9rZW4-In0",
      "thumbnail": null,
      "timestamp": "2026-07-30T10:32:00.000Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
  • Account key only. A per-phone key is rejected with 401 rather than being narrowed to its own phone.

  • An account with no phones returns an empty data array, not an error.

Event Samples

Every row in data carries the same twelve fields, whichever scope you read and whatever kind of event it describes. What changes between one row and the next is only which fields hold something. The samples below are complete rows, so a field that is empty here is empty in the response too.

Four things to handle when you parse one:

  • Fields that do not apply to an event come back empty, as either null or "". Treat both as absent.
  • `thumbnail` is reserved and currently returns null on every row.
  • History rows do not carry the `details` object that webhook payloads include. Where an event has a cause, such as a disconnect or a rejected send, content states it.
  • `from_jid` is the author of the event, not the other party. On an outbound row it is your own number with a device suffix; in a group chat it is the participant who spoke. The other side of the conversation is conversation_id.

Inbound Text Message

Someone messaged the phone. The baseline row that most of a history read consists of.

typemessagesubtypetext
{
  "event_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "conversation_id": "[email protected]",
  "message_id": "3EB01234567890ABCDEF",
  "type": "message",
  "subtype": "text",
  "from_jid": "[email protected]",
  "from_me": false,
  "content": "Hello!",
  "media_url": null,
  "thumbnail": null,
  "timestamp": "2026-07-30T10:30:00.000Z"
}
  • message_id is WhatsApp's own id and is the same value the webhook carries, so it is what you reconcile the two against.

  • Message kinds without a subtype of their own, such as polls and system messages, also arrive as text with an empty content. An empty content does not mean an empty message.

Outbound Message

A message the phone sent, whether through the API or from the handset itself.

typemessagesubtypetextimagevideoaudiodocumentstickerlocationcontact
{
  "event_id": "b2c3d4e5-f6a7-4901-bcde-f12345678901",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "conversation_id": "[email protected]",
  "message_id": "3EB0234567890ABCDEF1",
  "type": "message",
  "subtype": "text",
  "from_jid": "5215500001111:[email protected]",
  "from_me": true,
  "content": "On our way",
  "media_url": null,
  "thumbnail": null,
  "timestamp": "2026-07-30T10:31:00.000Z"
}
  • from_jid is your own number with a device suffix (:12 above), which is why it should never be parsed as the recipient. Read conversation_id for that.

  • The row appears when the message actually goes out, not when you call send. A 202 means queued, so a send that is still in the queue has no row yet.

  • Messages typed on the handset land here too. from_me does not distinguish API traffic from human traffic.

Media Message

An image, video, audio note, document, or sticker. The only rows that carry a media URL.

typemessagesubtypeimagevideoaudiodocumentsticker
{
  "event_id": "c3d4e5f6-a7b8-4012-cdef-123456789012",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "conversation_id": "[email protected]",
  "message_id": "3EB0345678901ABCDEF2",
  "type": "message",
  "subtype": "image",
  "from_jid": "[email protected]",
  "from_me": false,
  "content": "Optional caption",
  "media_url": "https://files.wapisimo.dev/eyJtZWRpYUtleSI6IjxvcGFxdWUtdG9rZW4-In0",
  "thumbnail": null,
  "timestamp": "2026-07-30T10:32:00.000Z"
}
  • The path is one opaque token, not a filename, and carries no extension. GET the URL and you get the decrypted file back with its real content type.

  • Media is fetched from WhatsApp when you request it rather than archived, so a link stops resolving once WhatsApp expires the file. Download anything you need to keep.

  • media_url can be null on a media row when WhatsApp provides no decryptable file, which happens with view-once placeholders and channel media. The subtype still tells you what arrived.

  • A caption lands in content. Audio, video, documents and stickers normally have none, so an empty content beside a populated media_url is the common case.

Group Chat Message

A message in a WhatsApp group conversation. For most accounts these are a large share of the history, so they are worth handling explicitly rather than as an edge case.

typemessagesubtypetextimagevideoaudiodocumentsticker
{
  "event_id": "d4e5f6a7-b8c9-4123-def0-234567890123",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "conversation_id": "[email protected]",
  "message_id": "3EB0456789012ABCDEF3",
  "type": "message",
  "subtype": "text",
  "from_jid": "[email protected]",
  "from_me": false,
  "content": "I'll take that one",
  "media_url": null,
  "thumbnail": null,
  "timestamp": "2026-07-30T10:33:00.000Z"
}
  • conversation_id ends in @g.us and identifies the group. from_jid is the participant who spoke, so the two differ on every inbound group row.

  • The phone_number filter never matches these. It keys off the contact number behind a one-to-one chat, which a group conversation does not have. Filter groups with conversation_id instead.

  • A participant can appear as ...@lid rather than a phone number when WhatsApp hides their number. Compare JIDs as opaque strings rather than assuming digits before the @.

Reaction

Somebody reacted to a message. The history records that it happened, not what it was.

typemessagesubtypereaction
{
  "event_id": "e5f6a7b8-c9d0-4234-ef01-345678901234",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "conversation_id": "[email protected]",
  "message_id": "3EB0567890123ABCDEF4",
  "type": "message",
  "subtype": "reaction",
  "from_jid": "[email protected]",
  "from_me": false,
  "content": "",
  "media_url": null,
  "thumbnail": null,
  "timestamp": "2026-07-30T10:34:00.000Z"
}
  • The history does not store the emoji: content is empty on every reaction row. Read it from the webhook payload if you need it.

  • message_id belongs to the reaction, not to the message being reacted to. The history does not link the two.

Location and Contact Shares

A location pin or a contact card. Both are recorded as having happened, with their payload left to the webhook.

typemessagesubtypelocationcontact
{
  "event_id": "f6a7b8c9-d0e1-4345-f012-456789012345",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "conversation_id": "[email protected]",
  "message_id": "3EB0678901234ABCDEF5",
  "type": "message",
  "subtype": "location",
  "from_jid": "[email protected]",
  "from_me": false,
  "content": "",
  "media_url": null,
  "thumbnail": null,
  "timestamp": "2026-07-30T10:35:00.000Z"
}
  • Coordinates are not in the row. latitude and longitude ride on the webhook payload only.

  • A contact row is the same shape with a different subtype. displayName and vcards are likewise webhook-only.

  • To keep the contents, take them from a webhook and store them yourself. The history records that a share happened and when.

Send Failed

A send was refused because the phone was not connected. History only, never pushed to a webhook.

typemessagesubtypesend_failed
{
  "event_id": "a7b8c9d0-e1f2-4456-0123-567890123456",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "conversation_id": "",
  "message_id": "",
  "type": "message",
  "subtype": "send_failed",
  "from_jid": "",
  "from_me": false,
  "content": "Message send failed - phone not connected",
  "media_url": null,
  "thumbnail": null,
  "timestamp": "2026-07-30T10:36:00.000Z"
}
  • The row does not identify the intended recipient, so match these against your own send log by timestamp.

  • These carry from_me: false even though the attempt was outbound, so a from_me=true filter will not return them.

  • A run of these within a few minutes is what flips the phone to disconnected and emails the account owner.

Send Rejected by WhatsApp

The message left the socket and WhatsApp refused it on the acknowledgement. The failure behind "connected but nothing arrives". History only.

typemessagesubtypesend_rejected
{
  "event_id": "b8c9d0e1-f2a3-4567-1234-678901234567",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "conversation_id": "[email protected]",
  "message_id": "3EB0789012345ABCDEF6",
  "type": "message",
  "subtype": "send_rejected",
  "from_jid": "[email protected]",
  "from_me": true,
  "content": "Message rejected by WhatsApp",
  "media_url": null,
  "thumbnail": null,
  "timestamp": "2026-07-30T10:37:00.000Z"
}
  • Unlike a send failure this one names the message: message_id is the id of the send that was refused, so it joins straight onto the outbound row you already have.

  • Rare, and worth alerting on: a phone can look connected while every send is refused.

  • Often accompanied by a reachout_timelocked connection row around the same time, which explains why the sends are being refused.

Connection Lost

The phone stopped being usable. The same event also reaches every webhook on the phone.

typeconnectionsubtypedisconnected
{
  "event_id": "c9d0e1f2-a3b4-4678-2345-789012345678",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "conversation_id": null,
  "message_id": null,
  "type": "connection",
  "subtype": "disconnected",
  "from_jid": null,
  "from_me": false,
  "content": "Phone logged out - device disconnected",
  "media_url": null,
  "thumbnail": null,
  "timestamp": "2026-07-30T10:38:00.000Z"
}
  • Every disconnect uses the subtype disconnected, and content is what tells the causes apart: logged out from the handset, blocked by WhatsApp, a multidevice mismatch, or repeated send failures. The webhook payload names the trigger in a dedicated field.

  • Filter on type=connection to get an uptime timeline for a phone without paging through its messages.

Sending Restricted

WhatsApp time-locked the number out of starting conversations. The phone stays connected and keeps receiving, so nothing else in the history looks wrong.

typeconnectionsubtypereachout_timelockedreachout_timelock_lifted
{
  "event_id": "d0e1f2a3-b4c5-4789-3456-890123456789",
  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
  "conversation_id": "",
  "message_id": "",
  "type": "connection",
  "subtype": "reachout_timelocked",
  "from_jid": "",
  "from_me": false,
  "content": "Account restricted from sending messages by WhatsApp",
  "media_url": null,
  "thumbnail": null,
  "timestamp": "2026-07-30T10:39:00.000Z"
}
  • The expiry is not in the row. timestamp tells you when the lock was detected; details.time_enforcement_ends on the webhook payload is the only place the end date appears.

  • A matching reachout_timelock_lifted row, same shape, arrives when WhatsApp releases the number. Until it does, assume the lock still holds.

  • Since the phone never disconnects, a monitor built on disconnected rows alone will not notice this. Watch for the subtype, or for a run of send_rejected.

Cloud API Message

A message on an official Cloud API phone. Same twelve columns as every other row, but several of them are filled differently because the identifiers come from Meta rather than WhatsApp Web.

typemessagesubtypetextimagevideoaudiodocumentstickerlocationcontactreactionbuttoninteractivetemplateunknown
{
  "event_id": "e5f6a7b8-c9d0-4234-ef01-345678901234",
  "phone_id": "5c9a2f18-6d40-4e7b-9a13-8b4f0c2e7d51",
  "conversation_id": "5215512345678",
  "message_id": "wamid.HBgNNTIxNTUxMjM0NTY3OBUCABIYEjBBMUIyQzNENEU1RjYwNzg5",
  "type": "message",
  "subtype": "text",
  "from_jid": null,
  "from_me": false,
  "content": "Hola!",
  "media_url": null,
  "thumbnail": null,
  "timestamp": "2026-08-15T10:30:00.000Z"
}
  • conversation_id is a bare wa_id (digits, no @s.whatsapp.net), not a JID. A conversation_id filter written for QR-linked phones will not match these rows. Filtering by phone_number works on both, so prefer it.

  • from_jid is never populated on Cloud API rows. Use from_me to tell direction.

  • message_id is Meta's wamid., which is also what a Cloud API status callback references, so the two reconcile directly.

  • media_url is always null, including on media rows. Meta hands us a media id rather than a file, and the history read does not expose it — take media_id off the webhook payload and fetch it from Graph with your own token.

  • Outbound template sends log subtype: "template" with content set to the template BODY as it was delivered, variables filled in. details carries template (the name), template_language, template_variables, and the rendered template_header / template_footer / template_buttons — which is how you tell a send was a template, and which one. Sends made before this was recorded have a content of [template] your_template_name instead.

  • button and interactive are Cloud-only subtypes (a reply button or a list selection); unknown covers a Meta message type Wapisimo does not translate yet, and arrives with an empty content.

  • A contact who has no phone number visible (a BSUID-only identity) puts the BSUID in conversation_id instead of a number.

Phone Groups

Phone groups combine several WhatsApp numbers behind one sending endpoint. Send to the group id and Wapisimo picks which member delivers the message.

Not to be confused with WhatsApp group chats, which are conversations with several participants and are covered by List WhatsApp Groups.

  • Load is spread across the members instead of one number carrying everything.
  • Contact affinity keeps a given recipient talking to the same number.
  • Only connected members are eligible to send.
  • A group has its own API key, and webhooks configured on it apply to every member.
  • A group needs at least one connected phone, or a send returns 404.

Selection Strategies

Round robin picks the member used least recently, tracked by a last_used timestamp; members never used go first. This spreads volume evenly.

Random picks any connected member. Useful when even distribution matters less than unpredictability.

Both are subordinate to contact affinity: if the recipient already has a history with one of the members, that member is chosen and the strategy does not run.

Contact Affinity

Recipients should not see your business jumping between numbers mid-conversation, so before applying the strategy Wapisimo checks whether any member has recently talked to this recipient.

  1. Look up the most recent event between the recipient and any member of the group.
  2. If one exists within the last 30 days, that member sends, and the strategy is skipped.
  3. Otherwise the strategy picks, and that choice becomes the new affinity.
  4. After sending, the chosen member's last_used timestamp is updated.

Consequences worth knowing:

  • A contact sticks to one number for up to 30 days of silence.
  • Affinity counts inbound events too, so a reply naturally goes back out through the number the customer messaged.
  • If a member disconnects, its contacts move to another member on the next send. When it reconnects those contacts stay where they were moved until affinity expires on its own.

Send via Group

Sending through a group uses the same request shape as a phone send — text, media, and location all work identically. Substitute the group id for the phone id and authenticate with the group's key:

curl -X POST https://api.wapisimo.dev/v1/{group_id}/send \
  -H "Authorization: Bearer YOUR_GROUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": "+5215512345678", "message": "Hello from the group!"}'

The response is the same 202 as a phone send. It does not tell you which member was chosen; read the event history and check phone_id if you need to know.

Media Types

mediaType accepts:

  • image — JPEG, PNG, GIF, WebP
  • video — MP4, AVI, MOV, WebM
  • audio — MP3, WAV, OGG, M4A
  • document — PDF, DOC, TXT and others

mediaUrl and mediaType are both required, and message becomes the caption. The URL must be publicly reachable: Wapisimo fetches it server-side with no credentials of yours attached. Files are processed transiently, not archived.

Webhook Events

Register a URL with Add Webhook and Wapisimo POSTs a JSON body to it as things happen. type: "new_messages" delivers inbound messages; type: "all" delivers everything below. Connection events ignore that setting and always reach every webhook on the phone, because a phone going down is something every integration needs to know about.

On an official Cloud API phone a third type, proxy, forwards Meta's own payload instead of the shapes below; see Add Webhook. The normalized Cloud payloads are catalogued here alongside the QR-linked ones and are marked as such.

Two details worth designing around:

  • Webhook payloads and event-history rows are not the same shape. Webhooks use camelCase (subType, fromMe) and a Unix timestamp; history rows use snake_case (subtype, from_me) and an ISO 8601 timestamp. Each event below is labelled with where it surfaces.
  • Some events are history-only. send_failed and send_rejected are recorded but never pushed, so an integration that only listens to webhooks will not notice a phone that has silently stopped delivering. Poll the event history for those.

Respond 2xx promptly. Deliveries are not retried, and a slow endpoint delays the events queued behind it for that phone.

Text Message

Webhook + event history

A text message was sent or received.

typemessagesubtypetext
{
  "type": "message",
  "subType": "text",
  "from": "[email protected]",
  "message": "Hello!",
  "timestamp": 1753876800,
  "fromMe": false
}

Media Message

Webhook + event history

An image, video, audio file, document, or sticker arrived. The file is mirrored to a Wapisimo URL.

typemessagesubtypeimagevideoaudiodocumentsticker
{
  "type": "message",
  "subType": "image",
  "from": "[email protected]",
  "message": "Optional caption",
  "url": "https://files.wapisimo.dev/eyJtZWRpYUtleSI6IjxvcGFxdWUtdG9rZW4-In0",
  "timestamp": 1753876800,
  "fromMe": false
}
  • The path is one opaque token rather than a filename, and it is the same URL the event history returns as media_url.

Location Message

Webhook + event history

A contact shared a location pin. Coordinates are hoisted onto the payload.

typemessagesubtypelocation
{
  "type": "message",
  "subType": "location",
  "from": "[email protected]",
  "latitude": 19.4326,
  "longitude": -99.1332,
  "timestamp": 1753876800,
  "fromMe": false
}

Contact Message

Webhook + event history

A contact card was shared. A single share populates `displayName` and a one-element `vcards` array; a multi-contact share fills the array.

typemessagesubtypecontact
{
  "type": "message",
  "subType": "contact",
  "from": "[email protected]",
  "displayName": "Example Contact",
  "vcards": [
    "BEGIN:VCARD\nVERSION:3.0\nFN:Example Contact\nTEL;type=Mobile;waid=5215500000000:+52 155 0000 0000\nEND:VCARD"
  ],
  "timestamp": 1753876800,
  "fromMe": false
}

Reaction

Webhook + event history

Someone reacted to a message. An empty `reaction` means the reaction was removed.

typemessagesubtypereaction
{
  "type": "message",
  "subType": "reaction",
  "conversation": "[email protected]",
  "reaction": "👍",
  "fromMe": false
}
  • The webhook is the only place the emoji appears. A reaction is recorded in the event history too, but with an empty content — see the reaction sample under Event Samples.

Message Edited

Webhook only

An already-delivered message was edited by its sender.

typemessagesubtypeedit
{
  "type": "message",
  "subType": "edit",
  "0": {
    "key": {
      "remoteJid": "[email protected]",
      "id": "3EB01234567890ABCDEF",
      "fromMe": false
    },
    "update": { "message": { "conversation": "Corrected text" } }
  }
}
  • The raw update array is spread onto the payload, so its entries appear as numeric keys alongside type and subType.

Delivery Status Update

Webhook only

A delivery receipt moved an outbound message along: sent, delivered, then read. Useful for confirming that a `202` from `/send` actually landed.

typemessagesubtypestatus_update
{
  "type": "message",
  "subType": "status_update",
  "0": {
    "key": {
      "remoteJid": "[email protected]",
      "id": "3EB01234567890ABCDEF",
      "fromMe": true
    },
    "update": { "status": 3 }
  }
}
  • These fire constantly. Register type: "all" only if you intend to consume them.

Send Failed

Event history only

A send was refused before it reached WhatsApp, because the phone was not connected. The `/send` call itself returns `400`.

typemessagesubtypesend_failed
{
  "type": "message",
  "subtype": "send_failed",
  "content": "Message send failed - phone not connected",
  "from_me": false,
  "timestamp": "2026-07-30T10:30:00.000Z"
}
  • History only — this is recorded in the event log and is not pushed to webhooks.

  • A run of these is the signal that a phone needs re-pairing.

Send Rejected by WhatsApp

Event history only

WhatsApp accepted the message from the socket and then rejected it on the acknowledgement. This is the failure mode behind "connected but nothing goes out".

typemessagesubtypesend_rejected
{
  "type": "message",
  "subtype": "send_rejected",
  "conversation_id": "[email protected]",
  "message_id": "3EB01234567890ABCDEF",
  "content": "Message rejected by WhatsApp",
  "from_me": true,
  "timestamp": "2026-07-30T10:30:00.000Z"
}
  • History only — not pushed to webhooks.

  • Only terminal errors are recorded. Intermediate delivery statuses are not, or they would swamp the log.

  • Frequently paired with a reach-out time-lock; check for a reachout_timelocked connection event around the same time.

Chat Events

Webhook only

A conversation appeared, changed, or was deleted on the linked handset.

typechatssubtypeupsertnewupdatedelete
{
  "type": "chats",
  "subType": "upsert",
  "0": {
    "id": "[email protected]",
    "conversationTimestamp": 1753876800,
    "unreadCount": 1
  }
}
  • upsert is a chat WhatsApp has just synced to the device. update is a change to one already known.

  • new is a Wapisimo refinement of update: an update carrying a sender key hash, which in practice marks a genuinely new conversation.

  • chats events are not part of the type enum accepted by the event history filters — they reach webhooks only.

Connection Lost

Webhook + event history

The phone stopped being usable and needs attention. Delivered to every webhook regardless of its `type`.

typeconnectionsubtypedisconnectedlogged_outforbiddenmultidevice_mismatch
{
  "type": "connection",
  "subType": "disconnected",
  "content": "Phone logged out - device disconnected",
  "timestamp": 1753876800,
  "details": {
    "trigger": "logged_out",
    "statusCode": 401
  }
}
  • details.trigger carries the underlying cause: logged_out (unlinked from the handset), forbidden (blocked by WhatsApp, possibly banned), multidevice_mismatch (a fresh QR scan is required), or consecutive_send_failures.

  • Recovery is usually Get QR Code, which resumes the stored session. Reach for Resync Phone only when that fails.

Sending Restricted

Webhook + event history

WhatsApp has time-locked this number out of starting new conversations. The socket stays healthy and inbound messages keep flowing, so this is not a disconnect.

typeconnectionsubtypereachout_timelocked
{
  "type": "connection",
  "subType": "reachout_timelocked",
  "content": "Account restricted from sending messages by WhatsApp",
  "timestamp": 1753876800,
  "details": {
    "enforcement_type": "companion_reachout",
    "time_enforcement_ends": "2026-08-06T10:30:00.000Z",
    "detected_at": "2026-07-30T10:30:00.000Z",
    "source": "notification"
  }
}
  • details.time_enforcement_ends is when the lock expires, and is null when WhatsApp did not tell us.

  • While the lock holds, /send fails fast with the expiry rather than accepting a message that would die silently.

  • Typically lifts after about seven days.

Sending Restriction Lifted

Webhook + event history

WhatsApp released the restriction and the phone can start conversations again.

typeconnectionsubtypereachout_timelock_lifted
{
  "type": "connection",
  "subType": "reachout_timelock_lifted",
  "content": "Sending restriction lifted by WhatsApp",
  "timestamp": 1753876800,
  "details": { "source": "notification" }
}

Cloud API Message

Webhook + event history

An inbound message on an official Cloud API phone, normalized. Close to the QR-linked payload but not identical — check the differences below before reusing a parser.

typemessagesubtypetextimagevideoaudiodocumentstickerlocationcontactreactionbuttoninteractiveunknown
{
  "type": "message",
  "subType": "text",
  "from": "5215512345678",
  "from_phone": "5215512345678",
  "from_name": "Ana",
  "content": "Hola!",
  "timestamp": 1755253800,
  "fromMe": false,
  "message_id": "wamid.HBgNNTIxNTUxMjM0NTY3OBUCABIYEjBBMUIyQzNENEU1RjYwNzg5",
  "provider": "cloud_api",
  "phone_id": "5c9a2f18-6d40-4e7b-9a13-8b4f0c2e7d51"
}
  • The text lives in content, not message — that is the one difference most likely to break a parser written against the QR-linked payload.

  • from is a bare wa_id, not a JID. from_phone repeats it, and from_user_id carries the BSUID when Meta sends one; for a BSUID-only contact from is the BSUID and from_phone is absent.

  • provider: "cloud_api" is on every Cloud payload, so one endpoint can serve both kinds of phone by branching on it.

  • Media arrives as media_id (Meta's id) with no Wapisimo URL — fetch it from Graph with your own token. Location, contacts, and interactive replies put their structured payload in data.

  • Redeliveries are suppressed: Meta delivers at least once, and a repeat of a wamid already stored does not fire the webhook again. A proxy webhook is the exception and mirrors Meta's own at-least-once behavior.

Cloud API Delivery Status

Webhook only

Meta reported a delivery transition for a message you sent from a Cloud API phone.

typemessagesubtypestatus
{
  "type": "message",
  "subType": "status",
  "status": "delivered",
  "mapped_status": "DELIVERY_ACK",
  "message_id": "wamid.HBgNNTIxNTUxMjM0NTY3OBUCABIYEjBBMUIyQzNENEU1RjYwNzg5",
  "recipient_id": "5215512345678",
  "timestamp": 1755253860,
  "fromMe": true,
  "provider": "cloud_api",
  "phone_id": "5c9a2f18-6d40-4e7b-9a13-8b4f0c2e7d51"
}
  • Only type: "all" webhooks receive these. new_messages does not.

  • The subtype is status, not the status_update a QR-linked phone sends.

  • status is Meta's own wording (sent, delivered, read, failed); mapped_status is the same transition in the vocabulary the rest of Wapisimo uses (SERVER_ACK, DELIVERY_ACK, READ, ERROR). A failed status carries Meta's errors array.

  • This updates the original outbound row rather than creating a new one, which is why there is no matching history sample. Correlate on message_id.

Natural Mode

Natural Mode makes a phone behave like a person rather than a script. It is on by default and lowers the chance the number gets flagged as automated.

It applies to QR-linked phones only. Official Cloud API sends go straight to Meta with no typing indicator, no pre-send delay, and no read-receipt shaping — there is nothing to flag as automated, because the number is officially registered for business messaging.

What it does. Real people do not reply the millisecond an event lands, so Natural Mode adds signals that look human:

  • A typing… indicator appears before the message arrives; voice notes show recording… instead.
  • The pre-send delay scales with message length, the way writing a paragraph takes longer than "ok".
  • Every interaction carries its own jitter, so two identical messages never behave identically.
  • Read receipts arrive after a variable delay, and some messages are left unread — most real conversations have a few sitting on two grey ticks.

The exact mix changes as we tune it. Leaving Natural Mode on means picking up future adjustments automatically.

Keep it on for anything conversational: support, sales replies, chatbots, outreach. The more human a number looks, the longer it stays healthy.

Turn it off when the recipient is actively waiting and latency matters: OTPs and login codes, payment confirmations, system alerts, transactional notifications.

Toggling. Open the phone's settings in the dashboard and flip Natural behavior in the Behavior card. It takes effect within seconds. There is no API for this today.

What changes. On, outbound messages take a few seconds longer and read receipts are delayed or skipped. Off, messages send immediately. Either way webhook delivery is never delayed — inbound events reach you in real time.

Rate Limits

RouteLimit
/qr10 requests per minute
/webhook60 requests per hour
/events (all three scopes)60 requests per minute
/sendnot request-limited; QR-linked phones drain at ~1 message/second

Other routes, including /v1/phones, are not rate limited today. Do not build against that: treat it as unspecified rather than guaranteed, and handle 429 everywhere.

Exceeding a limit returns 429. Limits are counted per instance id, except group event reads, which are counted against the account so that fanning out across many groups cannot multiply the budget.

For QR-linked phones the /send queue is what keeps a number under WhatsApp's own limits: it preserves ordering and retries on failure. Cloud API sends skip the queue and go straight to Meta, which enforces its own per-number throughput.

Status Codes

CodeMeaning
200Success
201Created
202Accepted — queued, not yet delivered
400Bad request: malformed parameter, missing field, or a send to a disconnected phone
401Unauthorized: missing, unknown, or wrong-scope key — also returned for a malformed instance id
404Not found, or a group with no connected members
405Method not allowed on this path
429Rate limited
500Server error

Errors carry a JSON body where the API produces one:

{
  "error": "phone_status only applies to group and account-wide reads"
}

Some rejections return a plain-text body instead of JSON, so parse defensively and rely on the status code rather than the body shape.