{
  "openapi": "3.1.0",
  "info": {
    "title": "Wapisimo API",
    "version": "1.0.0",
    "summary": "RESTful API for WhatsApp messaging integration",
    "description": "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.\n\nFull documentation: https://app.wapisimo.dev/docs/"
  },
  "servers": [
    {
      "url": "https://api.wapisimo.dev",
      "description": "Production gateway"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Account key, phone key, or group key depending on the route. Sent as `Authorization: Bearer <key>`. `X-API-Key` is internal to the platform and is not accepted from customers."
      }
    }
  },
  "paths": {
    "/v1/phones": {
      "post": {
        "operationId": "createPhone",
        "summary": "Create Phone",
        "description": "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.\n\n- 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.\n- 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.\n- Creating a phone consumes a subscription slot.",
        "tags": [
          "Account"
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "phone_number": {
                    "type": "string",
                    "description": "Digits only, country code included, no \"+\"."
                  },
                  "name": {
                    "type": "string",
                    "description": "Label shown in the dashboard."
                  }
                },
                "required": [
                  "phone_number"
                ]
              },
              "examples": {
                "create-phone": {
                  "summary": "Create Phone",
                  "value": {
                    "phone_number": "5215512345678",
                    "name": "Support line"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Create Phone",
            "content": {
              "application/json": {
                "example": {
                  "phone_number": "5215512345678",
                  "name": "Support line",
                  "id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                  "api_key": "a91c4e77-2b58-4f03-9d16-6c8e2a4b5f90"
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          }
        }
      },
      "get": {
        "operationId": "listPhones",
        "summary": "List Phones",
        "description": "List every phone in the account that has not been deleted.\n\n- `status` is `connected` or `disconnected`. Deleted phones are omitted from this list.\n- `provider` is `cloud_api` for official WhatsApp Cloud API connections, or `baileys` for QR-linked (WhatsApp Web) phones.\n- A newly created phone is `disconnected` until somebody scans its QR code.\n- Each phone's `api_key` is included, so a lost phone key can be recovered here with the account key.\n- The response can include fields not listed above. Anything undocumented may change without notice and should not be built on.",
        "tags": [
          "Account"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "List Phones",
            "content": {
              "application/json": {
                "example": [
                  {
                    "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"
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          }
        }
      }
    },
    "/v1/phones/{phone_id}": {
      "delete": {
        "operationId": "deletePhone",
        "summary": "Delete Phone",
        "description": "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.\n\n- Not reversible. Reconnecting that number means creating a new phone and pairing it again.",
        "tags": [
          "Account"
        ],
        "parameters": [
          {
            "name": "phone_id",
            "in": "path",
            "required": true,
            "description": "The phone to delete.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delete Phone",
            "content": {
              "application/json": {
                "example": {
                  "message": "Phone deleted"
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          }
        }
      }
    },
    "/v1/{phone_id}/send": {
      "post": {
        "operationId": "sendMessageOrSendMediaOrSendLocationOrSendTemplate",
        "summary": "Send Text Message / Send Media Message / Send Location Message / Send Template Message",
        "description": "**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.\n\n**Send Media Message** — Queue an image, video, audio file, or document. The file is fetched from your URL and forwarded to WhatsApp.\n\n**Send Location Message** — Queue a location pin.\n\n**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.\n\n- `202` means queued. A message can still fail afterwards — WhatsApp may reject it, or the phone may not be connected.\n- 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.\n- 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.\n- 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.\n- 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.\n- `mediaUrl` must be reachable without authentication — the fetch carries no credentials of yours.\n- 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.)\n- The template must already be APPROVED (list them with GET /v1/cloud/templates/{phone_id}).\n- `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.\n- `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.\n- A FOOTER never takes a parameter, so there is no `footer` field.\n- Meta rejects a partially filled template, so send every value the template asks for.\n- 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.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "phone_id",
            "in": "path",
            "required": true,
            "description": "Sending phone. A group id also works; see Send via Group.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "title": "Send Text Message",
                    "type": "object",
                    "properties": {
                      "to": {
                        "type": "string",
                        "description": "Recipient in international format."
                      },
                      "message": {
                        "type": "string",
                        "description": "Text body. Required unless `location` is given."
                      }
                    },
                    "required": [
                      "to",
                      "message"
                    ]
                  },
                  {
                    "title": "Send Media Message",
                    "type": "object",
                    "properties": {
                      "to": {
                        "type": "string",
                        "description": "Recipient in international format."
                      },
                      "mediaUrl": {
                        "type": "string",
                        "description": "Publicly reachable URL. Wapisimo fetches it server-side."
                      },
                      "mediaType": {
                        "type": "string",
                        "description": "One of `image`, `video`, `audio`, `document`."
                      },
                      "message": {
                        "type": "string",
                        "description": "Caption."
                      }
                    },
                    "required": [
                      "to",
                      "mediaUrl",
                      "mediaType"
                    ]
                  },
                  {
                    "title": "Send Location Message",
                    "type": "object",
                    "properties": {
                      "to": {
                        "type": "string",
                        "description": "Recipient in international format."
                      },
                      "location": {
                        "type": "object",
                        "description": "`latitude` and `longitude` as numbers; optional `name` and `address` labels."
                      }
                    },
                    "required": [
                      "to",
                      "location"
                    ]
                  },
                  {
                    "title": "Send Template Message",
                    "type": "object",
                    "properties": {
                      "to": {
                        "type": "string",
                        "description": "Recipient in international format."
                      },
                      "template": {
                        "type": "object",
                        "description": "`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."
                      }
                    },
                    "required": [
                      "to",
                      "template"
                    ]
                  }
                ]
              },
              "examples": {
                "send-message": {
                  "summary": "Send Text Message",
                  "value": {
                    "to": "+5215512345678",
                    "message": "Hello from Wapisimo!"
                  }
                },
                "send-media": {
                  "summary": "Send Media Message",
                  "value": {
                    "to": "+5215512345678",
                    "mediaUrl": "https://example.com/invoice.pdf",
                    "mediaType": "document",
                    "message": "Your invoice"
                  }
                },
                "send-location": {
                  "summary": "Send Location Message",
                  "value": {
                    "to": "+5215512345678",
                    "location": {
                      "latitude": 19.4326,
                      "longitude": -99.1332,
                      "name": "Zócalo",
                      "address": "Ciudad de México"
                    }
                  }
                },
                "send-template": {
                  "summary": "Send Template Message",
                  "value": {
                    "to": "+5215512345678",
                    "template": {
                      "name": "order_update",
                      "language": "es_MX",
                      "header": "1234",
                      "variables": [
                        "Ana",
                        "hoy"
                      ],
                      "buttons": [
                        {
                          "index": 0,
                          "type": "url",
                          "parameter": "1234"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Send Template Message",
            "content": {
              "application/json": {
                "example": {
                  "status": "queued",
                  "message": "Message has been queued for delivery",
                  "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34"
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          }
        }
      }
    },
    "/v1/cloud/templates/{phone_id}": {
      "get": {
        "operationId": "listTemplates",
        "summary": "List Templates",
        "description": "List the message templates on a cloud phone's WhatsApp Business Account, with their approval status. Official Cloud API phones only.\n\n- Only templates whose `status` is `APPROVED` can be sent.\n- Create and edit templates in WhatsApp Manager; new ones start `PENDING` until Meta reviews them.\n- 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.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "phone_id",
            "in": "path",
            "required": true,
            "description": "Cloud phone whose WABA templates to list.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List Templates",
            "content": {
              "application/json": {
                "example": {
                  "templates": [
                    {
                      "name": "order_update",
                      "status": "APPROVED",
                      "category": "UTILITY",
                      "language": "es_MX",
                      "components": [
                        {
                          "type": "BODY",
                          "text": "Tu pedido {{1}} va en camino, llega el {{2}}."
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          }
        }
      }
    },
    "/v1/cloud/check/{phone_id}": {
      "post": {
        "operationId": "checkCloudConnection",
        "summary": "Check Cloud Connection",
        "description": "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.\n\n- 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`.\n- 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\"`.\n- 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.\n- `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.\n- `404` means the phone id is not a Cloud API connection. QR-linked phones use Get QR Code instead.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "phone_id",
            "in": "path",
            "required": true,
            "description": "Cloud phone to check.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Check Cloud Connection",
            "content": {
              "application/json": {
                "example": {
                  "ok": true,
                  "status": "connected",
                  "display_phone_number": "+52 1 55 1234 5678",
                  "verified_name": "Acme Support",
                  "quality_rating": "GREEN",
                  "warnings": []
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          }
        }
      }
    },
    "/v1/verify": {
      "get": {
        "operationId": "verifyNumber",
        "summary": "Verify Number",
        "description": "Check whether a number is registered on WhatsApp.\n\n- `verify` sits where an instance id normally goes, so the path is `/v1/verify`, not `/v1/{phone_id}/verify`.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "phone",
            "in": "query",
            "required": true,
            "description": "Number to check, digits with country code.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Verify Number",
            "content": {
              "application/json": {
                "example": {
                  "hasWhatsApp": true,
                  "phone": "5215512345678@s.whatsapp.net",
                  "details": [
                    {
                      "jid": "5215512345678@s.whatsapp.net",
                      "exists": true
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          }
        }
      }
    },
    "/v1/{phone_id}/qr": {
      "get": {
        "operationId": "getQr",
        "summary": "Get QR Code",
        "description": "Fetch the pairing QR code for a phone. Scan it from WhatsApp on the handset (Settings → Linked devices) to move the phone to `connected`.\n\n- QR codes are short-lived. Poll for a fresh one if the user does not scan in time.\n- Rate limited to 10 requests per minute.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "phone_id",
            "in": "path",
            "required": true,
            "description": "Phone to pair.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Get QR Code",
            "content": {
              "application/json": {
                "example": {
                  "status": "disconnected",
                  "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          },
          "429": {
            "description": "Rate limit exceeded."
          }
        }
      }
    },
    "/v1/{phone_id}/groups": {
      "get": {
        "operationId": "listGroups",
        "summary": "List WhatsApp Groups",
        "description": "List the WhatsApp group chats this phone belongs to, with metadata and participants.\n\n- These are WhatsApp group chats, which are a different thing from Wapisimo phone groups. WhatsApp group ids end in `@g.us`.\n- Requires the phone to be connected.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "phone_id",
            "in": "path",
            "required": true,
            "description": "Connected phone to read from.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List WhatsApp Groups",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "120363000000000000@g.us",
                    "subject": "Team chat",
                    "owner": "5215512345678@s.whatsapp.net",
                    "creation": 1672531200,
                    "size": 25,
                    "participants": [
                      {
                        "id": "5215512345678@s.whatsapp.net",
                        "admin": "superadmin"
                      },
                      {
                        "id": "5215587654321@s.whatsapp.net",
                        "admin": null
                      }
                    ]
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          }
        }
      }
    },
    "/v1/{phone_id}/webhook": {
      "get": {
        "operationId": "listWebhooks",
        "summary": "List Webhooks",
        "description": "List the webhooks registered for a phone.\n\n- `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.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "phone_id",
            "in": "path",
            "required": true,
            "description": "Phone whose webhooks to list.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List Webhooks",
            "content": {
              "application/json": {
                "example": [
                  {
                    "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
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          },
          "429": {
            "description": "Rate limit exceeded."
          }
        }
      },
      "post": {
        "operationId": "addWebhook",
        "summary": "Add Webhook",
        "description": "Register a URL to receive this phone's events.\n\n- The created row is nested under `webhook`, not returned at the top level.\n- `type` is constrained to `new_messages`, `all`, or `proxy`; anything else is rejected with `400`. Omitting it gives `new_messages`.\n- `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.\n- 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.\n- Connection events reach every webhook on the phone regardless of `type`.\n- Rate limited to 60 requests per hour.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "phone_id",
            "in": "path",
            "required": true,
            "description": "Phone to attach the webhook to.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "HTTPS endpoint that receives POSTed events."
                  },
                  "type": {
                    "type": "string",
                    "description": "`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."
                  },
                  "name": {
                    "type": "string",
                    "description": "Label shown in the dashboard."
                  }
                },
                "required": [
                  "url"
                ]
              },
              "examples": {
                "add-webhook": {
                  "summary": "Add Webhook",
                  "value": {
                    "url": "https://example.com/hooks/whatsapp",
                    "type": "new_messages",
                    "name": "Order updates"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Add Webhook",
            "content": {
              "application/json": {
                "example": {
                  "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
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          },
          "429": {
            "description": "Rate limit exceeded."
          }
        }
      }
    },
    "/v1/{phone_id}/webhook/{webhook_id}": {
      "delete": {
        "operationId": "deleteWebhook",
        "summary": "Delete Webhook",
        "description": "Remove a registered webhook.\n\n- Idempotent in effect: deleting an id that does not exist on this phone still returns `success: true`.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "phone_id",
            "in": "path",
            "required": true,
            "description": "Phone the webhook belongs to.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook_id",
            "in": "path",
            "required": true,
            "description": "Webhook to remove.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delete Webhook",
            "content": {
              "application/json": {
                "example": {
                  "success": true
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          },
          "429": {
            "description": "Rate limit exceeded."
          }
        }
      }
    },
    "/v1/{phone_id}/resync": {
      "post": {
        "operationId": "resyncPhone",
        "summary": "Resync Phone",
        "description": "Tear down the WhatsApp session, clear stored pairing credentials, and start a fresh one so a new QR code can be issued.\n\n- Destructive: this discards pairing credentials, so somebody has to scan a new QR code before the phone can send again.\n- To recover a phone that merely dropped its connection, call Get QR Code first — it resumes the stored session without forcing a re-scan.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "phone_id",
            "in": "path",
            "required": true,
            "description": "Phone to resync.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resync Phone",
            "content": {
              "application/json": {
                "example": {
                  "message": "Phone resynced successfully"
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          }
        }
      }
    },
    "/v1/{instance_id}/events": {
      "get": {
        "operationId": "listEventsOrListGroupEvents",
        "summary": "List Events for a Phone / List Events for a Phone Group",
        "description": "**List Events for a Phone** — Read one phone's event history, newest first by default.\n\n**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.\n\nThis path accepts a phone id or a group id; the gateway resolves which it is.\n\n- `phone_status` is rejected here with `400` — it only means something on the group and account reads.\n- Event rows do not include the `details` object that webhook payloads carry.\n- All three scopes return the same row. Event Samples shows a complete row per subtype, including which fields are empty on each.\n- Disconnected members are included by default — a phone that dropped off last week still has history worth reading. Deleted phones never are.\n- Narrow the member set with `phone_status`.\n- 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.\n- Unlike a send, an events read never selects a single member and never advances the round-robin pointer.",
        "tags": [
          "Phone"
        ],
        "parameters": [
          {
            "name": "instance_id",
            "in": "path",
            "required": true,
            "description": "Phone id or phone group id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "conversation_id",
            "in": "query",
            "required": false,
            "description": "Exact WhatsApp conversation JID, e.g. `5215512345678@s.whatsapp.net`. Max 200 characters. Cloud API rows store a bare `wa_id` here instead, so match on digits alone for those.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "phone_number",
            "in": "query",
            "required": false,
            "description": "Filter by contact number, digits with country code. Does not match group conversations.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "`message` or `connection`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "subtype",
            "in": "query",
            "required": false,
            "description": "Event subtype such as `text` or `send_failed`. Letters, digits, `_` and `-`; max 50 characters.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from_me",
            "in": "query",
            "required": false,
            "description": "`true` for outbound, `false` for inbound.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Only events at or after this timestamp.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "Only events at or before this timestamp.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "phone_status",
            "in": "query",
            "required": false,
            "description": "Group and account reads only. Comma-separated `connected` and/or `disconnected`; defaults to both. Rejected with `400` on a phone read.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "`desc` (default) or `asc`, by timestamp then event_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 50, maximum 200. Out-of-range and unparseable values fall back to 50.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque token from the previous response's `next_cursor`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List Events for a Phone Group",
            "content": {
              "application/json": {
                "examples": {
                  "page": {
                    "summary": "A page of results",
                    "value": {
                      "data": [
                        {
                          "event_id": "b2c3d4e5-f6a7-4901-bcde-f12345678901",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0234567890ABCDEF1",
                          "type": "message",
                          "subtype": "text",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "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
                    }
                  },
                  "history-text-inbound": {
                    "summary": "Inbound Text Message",
                    "description": "Someone messaged the phone. The baseline row that most of a history read consists of. `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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB01234567890ABCDEF",
                          "type": "message",
                          "subtype": "text",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "from_me": false,
                          "content": "Hello!",
                          "media_url": null,
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:30:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-text-outbound": {
                    "summary": "Outbound Message",
                    "description": "A message the phone sent, whether through the API or from the handset itself. `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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "b2c3d4e5-f6a7-4901-bcde-f12345678901",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0234567890ABCDEF1",
                          "type": "message",
                          "subtype": "text",
                          "from_jid": "5215500001111:12@s.whatsapp.net",
                          "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
                    }
                  },
                  "history-media": {
                    "summary": "Media Message",
                    "description": "An image, video, audio note, document, or sticker. The only rows that carry a media URL. 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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "c3d4e5f6-a7b8-4012-cdef-123456789012",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0345678901ABCDEF2",
                          "type": "message",
                          "subtype": "image",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "from_me": false,
                          "content": "Optional caption",
                          "media_url": "https://files.wapisimo.dev/eyJtZWRpYUtleSI6IjxvcGFxdWUtdG9rZW4-In0",
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:32:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-group-chat": {
                    "summary": "Group Chat Message",
                    "description": "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. `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 `@`.",
                    "value": {
                      "data": [
                        {
                          "event_id": "d4e5f6a7-b8c9-4123-def0-234567890123",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "120363000000000000@g.us",
                          "message_id": "3EB0456789012ABCDEF3",
                          "type": "message",
                          "subtype": "text",
                          "from_jid": "5215587654321@s.whatsapp.net",
                          "from_me": false,
                          "content": "I'll take that one",
                          "media_url": null,
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:33:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-reaction": {
                    "summary": "Reaction",
                    "description": "Somebody reacted to a message. The history records that it happened, not what it was. 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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "e5f6a7b8-c9d0-4234-ef01-345678901234",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0567890123ABCDEF4",
                          "type": "message",
                          "subtype": "reaction",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "from_me": false,
                          "content": "",
                          "media_url": null,
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:34:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-location-contact": {
                    "summary": "Location and Contact Shares",
                    "description": "A location pin or a contact card. Both are recorded as having happened, with their payload left to the webhook. 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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "f6a7b8c9-d0e1-4345-f012-456789012345",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0678901234ABCDEF5",
                          "type": "message",
                          "subtype": "location",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "from_me": false,
                          "content": "",
                          "media_url": null,
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:35:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-send-failed": {
                    "summary": "Send Failed",
                    "description": "A send was refused because the phone was not connected. History only, never pushed to a webhook. 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.",
                    "value": {
                      "data": [
                        {
                          "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"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-send-rejected": {
                    "summary": "Send Rejected by WhatsApp",
                    "description": "The message left the socket and WhatsApp refused it on the acknowledgement. The failure behind \"connected but nothing arrives\". History only. 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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "b8c9d0e1-f2a3-4567-1234-678901234567",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0789012345ABCDEF6",
                          "type": "message",
                          "subtype": "send_rejected",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "from_me": true,
                          "content": "Message rejected by WhatsApp",
                          "media_url": null,
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:37:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-connection-lost": {
                    "summary": "Connection Lost",
                    "description": "The phone stopped being usable. The same event also reaches every webhook on the phone. 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.",
                    "value": {
                      "data": [
                        {
                          "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"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-restricted": {
                    "summary": "Sending Restricted",
                    "description": "WhatsApp time-locked the number out of starting conversations. The phone stays connected and keeps receiving, so nothing else in the history looks wrong. 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`.",
                    "value": {
                      "data": [
                        {
                          "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"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-cloud-message": {
                    "summary": "Cloud API Message",
                    "description": "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. `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.",
                    "value": {
                      "data": [
                        {
                          "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"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          },
          "429": {
            "description": "Rate limit exceeded."
          }
        }
      }
    },
    "/v1/events": {
      "get": {
        "operationId": "listAccountEvents",
        "summary": "List Events Across the Account",
        "description": "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.\n\n- Account key only. A per-phone key is rejected with `401` rather than being narrowed to its own phone.\n- An account with no phones returns an empty `data` array, not an error.",
        "tags": [
          "Account"
        ],
        "parameters": [
          {
            "name": "conversation_id",
            "in": "query",
            "required": false,
            "description": "Exact WhatsApp conversation JID, e.g. `5215512345678@s.whatsapp.net`. Max 200 characters. Cloud API rows store a bare `wa_id` here instead, so match on digits alone for those.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "phone_number",
            "in": "query",
            "required": false,
            "description": "Filter by contact number, digits with country code. Does not match group conversations.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "`message` or `connection`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "subtype",
            "in": "query",
            "required": false,
            "description": "Event subtype such as `text` or `send_failed`. Letters, digits, `_` and `-`; max 50 characters.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from_me",
            "in": "query",
            "required": false,
            "description": "`true` for outbound, `false` for inbound.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Only events at or after this timestamp.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "Only events at or before this timestamp.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "phone_status",
            "in": "query",
            "required": false,
            "description": "Group and account reads only. Comma-separated `connected` and/or `disconnected`; defaults to both. Rejected with `400` on a phone read.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "`desc` (default) or `asc`, by timestamp then event_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 50, maximum 200. Out-of-range and unparseable values fall back to 50.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque token from the previous response's `next_cursor`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List Events Across the Account",
            "content": {
              "application/json": {
                "examples": {
                  "page": {
                    "summary": "A page of results",
                    "value": {
                      "data": [
                        {
                          "event_id": "c3d4e5f6-a7b8-4012-cdef-123456789012",
                          "phone_id": "9e1d4c73-5a28-4b60-8f31-7c2e5a9d0b46",
                          "conversation_id": "5215587654321@s.whatsapp.net",
                          "message_id": "3EB0345678901ABCDEF2",
                          "type": "message",
                          "subtype": "image",
                          "from_jid": "5215587654321@s.whatsapp.net",
                          "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
                    }
                  },
                  "history-text-inbound": {
                    "summary": "Inbound Text Message",
                    "description": "Someone messaged the phone. The baseline row that most of a history read consists of. `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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB01234567890ABCDEF",
                          "type": "message",
                          "subtype": "text",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "from_me": false,
                          "content": "Hello!",
                          "media_url": null,
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:30:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-text-outbound": {
                    "summary": "Outbound Message",
                    "description": "A message the phone sent, whether through the API or from the handset itself. `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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "b2c3d4e5-f6a7-4901-bcde-f12345678901",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0234567890ABCDEF1",
                          "type": "message",
                          "subtype": "text",
                          "from_jid": "5215500001111:12@s.whatsapp.net",
                          "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
                    }
                  },
                  "history-media": {
                    "summary": "Media Message",
                    "description": "An image, video, audio note, document, or sticker. The only rows that carry a media URL. 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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "c3d4e5f6-a7b8-4012-cdef-123456789012",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0345678901ABCDEF2",
                          "type": "message",
                          "subtype": "image",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "from_me": false,
                          "content": "Optional caption",
                          "media_url": "https://files.wapisimo.dev/eyJtZWRpYUtleSI6IjxvcGFxdWUtdG9rZW4-In0",
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:32:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-group-chat": {
                    "summary": "Group Chat Message",
                    "description": "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. `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 `@`.",
                    "value": {
                      "data": [
                        {
                          "event_id": "d4e5f6a7-b8c9-4123-def0-234567890123",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "120363000000000000@g.us",
                          "message_id": "3EB0456789012ABCDEF3",
                          "type": "message",
                          "subtype": "text",
                          "from_jid": "5215587654321@s.whatsapp.net",
                          "from_me": false,
                          "content": "I'll take that one",
                          "media_url": null,
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:33:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-reaction": {
                    "summary": "Reaction",
                    "description": "Somebody reacted to a message. The history records that it happened, not what it was. 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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "e5f6a7b8-c9d0-4234-ef01-345678901234",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0567890123ABCDEF4",
                          "type": "message",
                          "subtype": "reaction",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "from_me": false,
                          "content": "",
                          "media_url": null,
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:34:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-location-contact": {
                    "summary": "Location and Contact Shares",
                    "description": "A location pin or a contact card. Both are recorded as having happened, with their payload left to the webhook. 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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "f6a7b8c9-d0e1-4345-f012-456789012345",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0678901234ABCDEF5",
                          "type": "message",
                          "subtype": "location",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "from_me": false,
                          "content": "",
                          "media_url": null,
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:35:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-send-failed": {
                    "summary": "Send Failed",
                    "description": "A send was refused because the phone was not connected. History only, never pushed to a webhook. 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.",
                    "value": {
                      "data": [
                        {
                          "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"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-send-rejected": {
                    "summary": "Send Rejected by WhatsApp",
                    "description": "The message left the socket and WhatsApp refused it on the acknowledgement. The failure behind \"connected but nothing arrives\". History only. 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.",
                    "value": {
                      "data": [
                        {
                          "event_id": "b8c9d0e1-f2a3-4567-1234-678901234567",
                          "phone_id": "3f7c1e02-9b4a-4d61-8e2f-1a5c9d0b7e34",
                          "conversation_id": "5215512345678@s.whatsapp.net",
                          "message_id": "3EB0789012345ABCDEF6",
                          "type": "message",
                          "subtype": "send_rejected",
                          "from_jid": "5215512345678@s.whatsapp.net",
                          "from_me": true,
                          "content": "Message rejected by WhatsApp",
                          "media_url": null,
                          "thumbnail": null,
                          "timestamp": "2026-07-30T10:37:00.000Z"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-connection-lost": {
                    "summary": "Connection Lost",
                    "description": "The phone stopped being usable. The same event also reaches every webhook on the phone. 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.",
                    "value": {
                      "data": [
                        {
                          "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"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-restricted": {
                    "summary": "Sending Restricted",
                    "description": "WhatsApp time-locked the number out of starting conversations. The phone stays connected and keeps receiving, so nothing else in the history looks wrong. 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`.",
                    "value": {
                      "data": [
                        {
                          "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"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  },
                  "history-cloud-message": {
                    "summary": "Cloud API Message",
                    "description": "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. `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.",
                    "value": {
                      "data": [
                        {
                          "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"
                        }
                      ],
                      "next_cursor": null,
                      "has_more": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown, or wrong-scope API key."
          },
          "429": {
            "description": "Rate limit exceeded."
          }
        }
      }
    }
  }
}
