{"openapi":"3.1.0","info":{"title":"SonIA Public API","description":"Server-to-server REST API for SonIA. Authenticate with `Authorization: Bearer sonia_sk_<env>_<prefix>_<secret>`. Keys are server-side credentials: a request carrying an `Origin` header is refused with `browser_credential_use`. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and a server-generated `X-Request-Id` — quote the latter in support requests.","version":"1.0.0"},"paths":{"/v1/meta/health":{"get":{"tags":["meta"],"summary":"Liveness of the public API","description":"Unauthenticated liveness probe for integrators.\n\nDeliberately says nothing about the deployment: no build version, no\ndatabase state, no tenant count. It answers \"is /v1 serving?\" and any\nfurther detail here would be free reconnaissance for an unauthenticated\ncaller.","operationId":"getHealth","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Gethealth"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/meta/errors":{"get":{"tags":["meta"],"summary":"The error-code catalog","description":"Every error code /v1 can return, with its HTTP status.\n\nPublished as an endpoint (not only prose) so a client can assert at build\ntime that it handles the codes this deployment actually emits.","operationId":"listErrorCodes","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Listerrorcodes"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/whoami":{"get":{"tags":["identity"],"summary":"Identify the calling key","description":"Echo the credential's own identity, scopes and budget.\n\nReturns the key's PUBLIC prefix, never the key. ``account`` is the tenant\nthe key resolved to — the caller's own account id, which they need in order\nto correlate with their SonIA console; it is read from the credential row\nand from nowhere else.","operationId":"getWhoami","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Getwhoami"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}},"security":[{"ApiKeyAuth":[]}]}},"/v1/messages":{"post":{"tags":["messages"],"summary":"Send a WhatsApp message","description":"Sends a message and returns the transcript row it created.\n\n**Two modes, two scopes.** Passing `conversation_id` REPLIES inside an existing conversation and needs `messages:send`. Passing `to` writes to a number of your choosing, and passing `template` sends an approved template — both need `messages:initiate`, because contacting somebody who has not written first is the spam-capable action.\n\n**The 24-hour rule.** A free-form message is only deliverable within 24 hours of the contact's last inbound message — including one addressed by `to`, so a number that has never written can be reached ONLY by a template. Outside that window this refuses with `session_window_closed` BEFORE calling WhatsApp — ask `GET /v1/conversations/{id}/window` first, and send an approved template when it says so.\n\n**One call, one message.** `body` is limited to 1600 characters, WhatsApp's own per-message limit; a longer one is `invalid_request` rather than silently split into several messages billed as one.\n\n**Free-form sends follow the account's human-intervention switch.** When it is off, a console agent cannot reply by hand and neither can a key: the refusal is `human_replies_disabled`. Templates are unaffected.\n\n**Retries are safe.** Send an `Idempotency-Key` header and a repeat of the same request returns the ORIGINAL result with `Idempotent-Replay: true` instead of sending twice. Without the header one is derived from the body, so an accidental double-submit is absorbed anyway. A request that failed AFTER WhatsApp was called can never be replayed: it answers `idempotency_conflict`, and a genuine second attempt needs a new key.","operationId":"sendMessage","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Your own key for this send, at most 255 characters. Optional: one is derived from the body when it is absent.","title":"Idempotency-Key"},"description":"Your own key for this send, at most 255 characters. Optional: one is derived from the body when it is absent."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageRequest"}}}},"responses":{"201":{"description":"The message was accepted by WhatsApp for delivery.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageResponse"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"409":{"description":"Error codes: `bulk_send_not_cancellable`, `idempotency_conflict`, `session_window_closed`, `template_not_usable`","content":{"application/json":{"example":{"error":{"code":"bulk_send_not_cancellable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"422":{"description":"Error codes: `invalid_recipient`, `invalid_recipients`, `missing_template_variables`","content":{"application/json":{"example":{"error":{"code":"invalid_recipient","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"502":{"description":"Error codes: `send_failed`","content":{"application/json":{"example":{"error":{"code":"send_failed","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/messages/{message_id}":{"get":{"tags":["messages"],"summary":"Fetch one message","description":"Returns a single transcript row by id — the id `POST /v1/messages` returned, or one from a conversation page. Use it to poll a send's delivery `status` as WhatsApp advances it from `queued` to `delivered` or `read`.\n\nA message belonging to another account answers `404 not_found`, exactly as an id that does not exist does: confirming that an id is real but not yours would leak the existence of another account's traffic.","operationId":"getMessage","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"message_id","in":"path","required":true,"schema":{"type":"string","title":"Message Id"}}],"responses":{"200":{"description":"The message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResource"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/conversations":{"get":{"tags":["conversations"],"summary":"List conversations","description":"Pages the account's conversations, NEWEST-CREATED first.\n\n**This is not the console inbox order.** The inbox sorts by last activity, which moves every time anyone writes — fine for a screen that redraws, useless as a paging key, because a conversation that receives a message mid-walk would be served twice or skipped entirely. This orders on `(created_at, id)`, which never moves, so a page boundary means the same thing before and after new traffic arrives. To find recent activity, poll with `updated_since` instead of re-reading page 1.\n\n`q` matches the contact's name or number and the last message body — the same terms the console's search matches. `updated_since` keeps only conversations that CHANGED at or after that instant, which makes an incremental sync one call instead of a full walk.","operationId":"listConversations","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `next_cursor` of the previous page. Omit for the first page.","title":"Cursor"},"description":"The `next_cursor` of the previous page. Omit for the first page."},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Rows per page. Defaults to 50; a larger value is CLAMPED to 200 rather than refused.","title":"Limit"},"description":"Rows per page. Defaults to 50; a larger value is CLAMPED to 200 rather than refused."},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search the contact's name/number and the last message body.","title":"Q"},"description":"Search the contact's name/number and the last message body."},{"name":"updated_since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Only conversations that changed at or after this instant. A naive timestamp is read as UTC.","title":"Updated Since"},"description":"Only conversations that changed at or after this instant. A naive timestamp is read as UTC."}],"responses":{"200":{"description":"A page of conversations plus the cursor that continues it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationPage"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/conversations/{conversation_id}":{"get":{"tags":["conversations"],"summary":"Fetch one conversation","description":"Returns a single conversation with its most recent message and whether its 24-hour session window is currently open.\n\nAn id belonging to another account answers `404 not_found` rather than a 403, which would confirm the conversation exists.","operationId":"getConversation","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"The conversation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResource"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/conversations/{conversation_id}/messages":{"get":{"tags":["conversations"],"summary":"List a conversation's messages","description":"Pages one conversation's transcript, OLDEST first, so it reads forwards and so resuming is trivial: the cursor you hold is the last message you saw, and the next page is what has been written since. Both inbound and outbound rows are returned, in the order they were written.","operationId":"listConversationMessages","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `next_cursor` of the previous page. Omit to start at the oldest.","title":"Cursor"},"description":"The `next_cursor` of the previous page. Omit to start at the oldest."},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Rows per page. Defaults to 50; a larger value is CLAMPED to 200 rather than refused.","title":"Limit"},"description":"Rows per page. Defaults to 50; a larger value is CLAMPED to 200 rather than refused."}],"responses":{"200":{"description":"A page of transcript rows plus the cursor that continues it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessagePage"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/conversations/{conversation_id}/window":{"get":{"tags":["conversations"],"summary":"Check the 24-hour session window","description":"Answers whether a free-form message is deliverable to this contact right now, and when that stops being true.\n\nWhatsApp only accepts a free-form message within 24 hours of the contact's last inbound one; outside that window, only an approved template will reach them. This endpoint exists so you learn that from a cheap GET rather than from a failed send: check it before composing, and when `requires_template` is `true`, pick one from `GET /v1/templates`.\n\nThe clock runs on the CONTACT's last message, never on yours — your own reply does not extend the window.","operationId":"getMessagingWindow","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"The state of the conversation's session window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessagingWindow"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/templates":{"get":{"tags":["templates"],"summary":"List usable templates","description":"Lists the templates this account can actually send: APPROVED by WhatsApp and carrying a provider content id. Drafts, pending submissions and rejected copies are omitted, because sending one is impossible and listing it would only produce a `template_not_usable` later.\n\nRequires `messages:initiate`: a template is how a conversation is OPENED, so the catalogue belongs to the same capability as the act.\n\nFill every key in `variables` when you send — a placeholder left empty is refused with `missing_template_variables` rather than delivered blank.","operationId":"listTemplates","responses":{"200":{"description":"The approved templates for this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateList"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}},"security":[{"ApiKeyAuth":[]}]}},"/v1/bulk-sends/preview":{"post":{"tags":["bulk sends"],"summary":"Validate a bulk send without sending it","description":"Runs the exact validation `POST /v1/bulk-sends` runs and returns every recipient's verdict — and sends NOTHING, creates no run, and costs no budget.\n\n**Use this first for any list you did not generate yourself.** It is the cheap way to find the numbers missing a country prefix, the rows whose variables do not match the template, and the overrides pointing at a template that is not approved — before any of it reaches a real phone.\n\nThe verdicts come from the same validator the run uses, so a list that previews clean is a list that will be accepted. `warn` rows are NOT sendable: a template cannot render with a placeholder missing, so treat a warn exactly as you treat an invalid.\n\n`daily_budget_remaining` is returned so you can see whether the run would fit in today's budget without discovering it from a 429.","operationId":"previewBulkSend","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSendRequest"}}},"required":true},"responses":{"200":{"description":"Every recipient's verdict, plus the aggregate counts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSendPreview"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"409":{"description":"Error codes: `bulk_send_not_cancellable`, `idempotency_conflict`, `session_window_closed`, `template_not_usable`","content":{"application/json":{"example":{"error":{"code":"bulk_send_not_cancellable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"422":{"description":"Error codes: `invalid_recipient`, `invalid_recipients`, `missing_template_variables`","content":{"application/json":{"example":{"error":{"code":"invalid_recipient","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"502":{"description":"Error codes: `send_failed`","content":{"application/json":{"example":{"error":{"code":"send_failed","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}},"security":[{"ApiKeyAuth":[]}]}},"/v1/bulk-sends":{"post":{"tags":["bulk sends"],"summary":"Start a bulk send","description":"Accepts up to 1000 recipients of one approved template and starts sending them in the background. Answers **202** with a run id: the messages have been accepted for delivery, not delivered — poll `GET /v1/bulk-sends/{id}` for progress and `GET /v1/bulk-sends/{id}/rows` for per-recipient outcomes.\n\n**A template is required, and that is WhatsApp's rule.** An approved template is the only message that can reach somebody outside their 24-hour session window, which is where a bulk audience lives.\n\n**Bad recipients refuse the whole run by default.** With `on_invalid: \"reject\"` a single unusable row answers `422 invalid_recipients` listing every offender, and NO run is created — so you cannot accidentally contact 60% of a list and never learn about the rest. Pass `\"skip\"` to accept the run without them.\n\n**The whole list is charged before the run is accepted.** If `recipients` exceeds what is left of this key's daily budget the request is refused with `rate_limited` and nothing is sent — never half-sent. `POST /v1/bulk-sends/preview` reports what is left.\n\n**Retries are safe.** Send an `Idempotency-Key` and a repeat of the same request returns the ORIGINAL run with `Idempotent-Replay: true` instead of starting a second one. Without the header one is derived from the body, so an accidental double-submit is absorbed anyway — which on this endpoint is the difference between one message and a thousand duplicates.","operationId":"createBulkSend","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Your own key for this run, at most 255 characters. Optional: one is derived from the body when it is absent.","title":"Idempotency-Key"},"description":"Your own key for this run, at most 255 characters. Optional: one is derived from the body when it is absent."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSendRequest"}}}},"responses":{"202":{"description":"The run was accepted and is being sent in the background.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSendAccepted"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"409":{"description":"Error codes: `bulk_send_not_cancellable`, `idempotency_conflict`, `session_window_closed`, `template_not_usable`","content":{"application/json":{"example":{"error":{"code":"bulk_send_not_cancellable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"422":{"description":"Error codes: `invalid_recipient`, `invalid_recipients`, `missing_template_variables`","content":{"application/json":{"example":{"error":{"code":"invalid_recipient","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"502":{"description":"Error codes: `send_failed`","content":{"application/json":{"example":{"error":{"code":"send_failed","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}},"get":{"tags":["bulk sends"],"summary":"List bulk sends","description":"Pages this account's bulk-send runs, NEWEST-CREATED first — including the ones started from the console, because it is one history and one account.\n\nOrdered on `(created_at, id)`, which never moves, so a page boundary means the same thing before and after new runs are created. A run's counters change constantly while it drains; ordering on them would shuffle rows underneath a walking client.","operationId":"listBulkSends","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `next_cursor` of the previous page. Omit for the first page.","title":"Cursor"},"description":"The `next_cursor` of the previous page. Omit for the first page."},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Runs per page. Defaults to 50; a larger value is CLAMPED to 200 rather than refused.","title":"Limit"},"description":"Runs per page. Defaults to 50; a larger value is CLAMPED to 200 rather than refused."}],"responses":{"200":{"description":"A page of runs plus the cursor that continues it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSendPage"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/bulk-sends/{run_id}":{"get":{"tags":["bulk sends"],"summary":"Fetch one bulk send","description":"Returns a run's current status and live counters. This is the endpoint to poll after a 202: `pending_rows` reaches 0 and `status` becomes `completed` (or `cancelled`, or `failed`) when it is over.\n\nA run belonging to another account answers `404 not_found`, exactly as an id that does not exist does.","operationId":"getBulkSend","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"The run.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSendRun"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/bulk-sends/{run_id}/rows":{"get":{"tags":["bulk sends"],"summary":"List a bulk send's recipients","description":"Pages what happened to each recipient, in the order you sent them.\n\nThis is the reconciliation endpoint: `send_status` tells you which numbers were reached (`sent`, with the provider id), which were refused (`failed`, with the reason), which never qualified (`skipped`) and which were stopped (`cancelled`). Walk it once the run's status is terminal and you have a complete, per-contact account of the campaign.","operationId":"listBulkSendRows","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `next_cursor` of the previous page. Omit to start at the first.","title":"Cursor"},"description":"The `next_cursor` of the previous page. Omit to start at the first."},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Rows per page. Defaults to 50; a larger value is CLAMPED to 200 rather than refused.","title":"Limit"},"description":"Rows per page. Defaults to 50; a larger value is CLAMPED to 200 rather than refused."}],"responses":{"200":{"description":"A page of per-recipient outcomes plus its cursor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSendRowPage"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}},"/v1/bulk-sends/{run_id}/cancel":{"post":{"tags":["bulk sends"],"summary":"Cancel a bulk send","description":"Stops a run that is still queued or running, and returns it in its `cancelled` state.\n\n**Already-sent rows stand.** A cancellation stops what has not happened yet; it cannot un-buzz a phone. Every row still waiting becomes `cancelled` with a reason, and the sender notices between recipients — so cancelling is prompt but not instantaneous, and a row already handed to WhatsApp still counts as sent.\n\n**Safe to retry.** Cancelling an already-cancelled run answers 200 with the same run rather than an error. A run that has already finished answers `409 bulk_send_not_cancellable`: there is nothing left to stop, and pretending otherwise would misreport what went out.","operationId":"cancelBulkSend","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"The run, now cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSendRun"}}}},"400":{"description":"Error codes: `invalid_request`","content":{"application/json":{"example":{"error":{"code":"invalid_request","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"401":{"description":"Error codes: `invalid_api_key`, `missing_api_key`","content":{"application/json":{"example":{"error":{"code":"invalid_api_key","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"403":{"description":"Error codes: `browser_credential_use`, `channel_not_configured`, `human_replies_disabled`, `insufficient_scope`, `tenant_suspended`","content":{"application/json":{"example":{"error":{"code":"browser_credential_use","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"404":{"description":"Error codes: `not_found`","content":{"application/json":{"example":{"error":{"code":"not_found","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"429":{"description":"Error codes: `rate_limited`","content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"500":{"description":"Error codes: `internal_error`","content":{"application/json":{"example":{"error":{"code":"internal_error","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"503":{"description":"Error codes: `api_unavailable`","content":{"application/json":{"example":{"error":{"code":"api_unavailable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"409":{"description":"Error codes: `bulk_send_not_cancellable`, `idempotency_conflict`, `session_window_closed`, `template_not_usable`","content":{"application/json":{"example":{"error":{"code":"bulk_send_not_cancellable","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"422":{"description":"Error codes: `invalid_recipient`, `invalid_recipients`, `missing_template_variables`","content":{"application/json":{"example":{"error":{"code":"invalid_recipient","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}},"502":{"description":"Error codes: `send_failed`","content":{"application/json":{"example":{"error":{"code":"send_failed","message":"human-readable explanation","request_id":"0f9c2a1e6b8d4f27a1c3e5d709b4a6f8"}}}}}}}}},"components":{"schemas":{"BulkRecipient":{"properties":{"to":{"type":"string","title":"To","description":"The recipient's phone number. Send E.164 (`+34600111222`): a prefix-less number is only resolvable when the account states a country, and is otherwise reported as an invalid recipient rather than guessed at — guessing is how one contact becomes two."},"variables":{"additionalProperties":{"type":"string"},"type":"object","title":"Variables","description":"This recipient's values for the template's placeholders. Positional keys (`\"1\"`, `\"2\"`) and named keys (`\"nombre\"`) both work, keyed exactly as `GET /v1/templates` lists them. Every placeholder the template declares needs a non-empty value, and a key the template does NOT declare rejects the row — the variables are checked against the template that will actually be sent, never a different one."},"template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template","description":"Send THIS recipient a different template (by name or id) from the run's default. Useful for a mixed list — a reminder for some, a confirmation for others — in one run. The override is validated against ITS OWN template: its variables must match that template's placeholders, and it must be approved and belong to this account."}},"type":"object","required":["to"],"title":"BulkRecipient","description":"One recipient of a bulk send, with that recipient's OWN variable values.","examples":[{"to":"+34600111222","variables":{"1":"María","2":"el jueves a las 10:00"}}]},"BulkRecipientVerdict":{"properties":{"index":{"type":"integer","title":"Index","description":"0-based position in the `recipients` array you posted."},"ordinal":{"type":"integer","title":"Ordinal","description":"1-based row number, as the run records it."},"to":{"type":"string","title":"To","description":"The number as canonicalised to E.164, or the raw value you sent when it could not be canonicalised (which is itself the reason it failed)."},"validation":{"type":"string","title":"Validation","description":"`valid`, `warn` or `invalid`."},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason","description":"Why, in the operator's language, or `null` for a valid row. Phone numbers are masked in this text."},"template_sid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Sid","description":"The provider content id this row would be sent with."}},"type":"object","required":["index","ordinal","to","validation"],"title":"BulkRecipientVerdict","description":"One recipient's validation verdict, before anything is sent.","examples":[{"index":2,"ordinal":3,"reason":"número inválido: falta el prefijo del país (p. ej. +34)","template_sid":"HX7c1f0a2b","to":"600111222","validation":"invalid"}]},"BulkSendAccepted":{"properties":{"run":{"$ref":"#/components/schemas/BulkSendRun","description":"The run — poll `GET /v1/bulk-sends/{id}`."},"summary":{"$ref":"#/components/schemas/BulkSendSummary","description":"Verdict counts for the list you sent."},"skipped":{"items":{"$ref":"#/components/schemas/BulkRecipientVerdict"},"type":"array","title":"Skipped","description":"The recipients that will NOT be sent, with their reasons. Always empty when `on_invalid` was `reject` (such a request is refused instead); with `skip` this is the list you should log, because it is the part of your audience this run never reaches."}},"type":"object","required":["run","summary"],"title":"BulkSendAccepted","description":"The run that was accepted, plus what happened to the recipients.","examples":[{"run":{"created_at":"2026-08-17T09:14:03.221Z","created_by":"api:CRM sync","failed_rows":0,"id":"wbs-4f2c1a9e","pending_rows":2,"sent_rows":0,"status":"queued","template_id":"tpl_01HZY0Q8M2","template_name":"cita_confirmada","total_rows":2,"updated_at":"2026-08-17T09:14:03.221Z","valid_rows":2},"skipped":[],"summary":{"invalid":0,"sendable":2,"total":2,"valid":2,"warn":0}}]},"BulkSendPage":{"properties":{"data":{"items":{"$ref":"#/components/schemas/BulkSendRun"},"type":"array","title":"Data","description":"The runs, newest-CREATED first."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page — pass it back as `?cursor=`. `null` means this was the last page, and it is the ONLY end-of-collection signal: a short page can still have more behind it."}},"type":"object","required":["data"],"title":"BulkSendPage","description":"A page of runs plus the cursor that continues it.","examples":[{"data":[{"created_at":"2026-08-17T09:14:03.221Z","created_by":"api:CRM sync","failed_rows":1,"id":"wbs-4f2c1a9e","pending_rows":0,"sent_rows":247,"status":"completed","template_id":"tpl_01HZY0Q8M2","template_name":"cita_confirmada","total_rows":250,"updated_at":"2026-08-17T09:21:02.118Z","valid_rows":248}],"next_cursor":"eyJ2IjoxfQ.Zm9vYmFy"}]},"BulkSendPreview":{"properties":{"template_id":{"type":"string","title":"Template Id","description":"The resolved default template's id."},"template_name":{"type":"string","title":"Template Name","description":"Its name, as approved by WhatsApp."},"summary":{"$ref":"#/components/schemas/BulkSendSummary","description":"The aggregate counts."},"recipients":{"items":{"$ref":"#/components/schemas/BulkRecipientVerdict"},"type":"array","title":"Recipients","description":"Every recipient's verdict, in the order you sent them."},"daily_budget_remaining":{"type":"integer","title":"Daily Budget Remaining","description":"How much of this key's daily send budget is left RIGHT NOW. Compare it with `summary.total`: a run larger than what remains is refused, and this is how you find that out before trying."}},"type":"object","required":["template_id","template_name","summary","recipients","daily_budget_remaining"],"title":"BulkSendPreview","description":"What a run would do, computed without sending anything.","examples":[{"daily_budget_remaining":998,"recipients":[{"index":0,"ordinal":1,"template_sid":"HX7c1f0a2b","to":"+34600111222","validation":"valid"},{"index":1,"ordinal":2,"reason":"número inválido: falta el prefijo del país (p. ej. +34)","template_sid":"HX7c1f0a2b","to":"600333444","validation":"invalid"}],"summary":{"invalid":1,"sendable":1,"total":2,"valid":1,"warn":0},"template_id":"tpl_01HZY0Q8M2","template_name":"cita_confirmada"}]},"BulkSendRequest":{"properties":{"template_id":{"type":"string","title":"Template Id","description":"The APPROVED template every recipient receives unless they override it. Accepts the template's name or its id — whichever you stored. A template is mandatory: it is the only thing WhatsApp lets you send to somebody who has not written to you first."},"recipients":{"items":{"$ref":"#/components/schemas/BulkRecipient"},"type":"array","title":"Recipients","description":"The recipients, at most 1000 per request. Order is preserved and becomes each row's `ordinal`, so a result page lines up with the list you sent."},"on_invalid":{"type":"string","enum":["reject","skip"],"title":"On Invalid","description":"What to do when a recipient fails validation.\n\n`reject` (the DEFAULT) refuses the WHOLE run with a per-row error list and creates nothing — so you can never discover afterwards that part of your list was quietly dropped.\n\n`skip` accepts the run without the bad rows; each is recorded with its reason and is readable at `GET /v1/bulk-sends/{id}/rows`. Choose it when a partially-deliverable list is genuinely acceptable.","default":"reject"}},"type":"object","required":["template_id","recipients"],"title":"BulkSendRequest","description":"A template, a list of recipients, and what to do with the bad ones.","examples":[{"on_invalid":"reject","recipients":[{"to":"+34600111222","variables":{"1":"María","2":"el jueves a las 10:00"}},{"to":"+34600333444","variables":{"1":"Jorge","2":"el viernes a las 12:30"}}],"template_id":"cita_confirmada"}]},"BulkSendRowPage":{"properties":{"data":{"items":{"$ref":"#/components/schemas/BulkSendRowResource"},"type":"array","title":"Data","description":"The rows, in RECIPIENT order (ascending `ordinal`)."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page; `null` on the last page."}},"type":"object","required":["data"],"title":"BulkSendRowPage","description":"A page of a run's per-recipient outcomes plus the cursor that continues it.","examples":[{"data":[{"created_at":"2026-08-17T09:14:03.221Z","id":"wbsr-91ab77c0","ordinal":1,"provider_sid":"SM7f1c0f0a2b","send_status":"sent","template_sid":"HX7c1f0a2b","to":"+34600111222","updated_at":"2026-08-17T09:14:07.905Z","validation":"valid","variables":{"1":"María"}}]}]},"BulkSendRowResource":{"properties":{"id":{"type":"string","title":"Id","description":"Stable id of this row."},"ordinal":{"type":"integer","title":"Ordinal","description":"1-based position in the recipient list, and the paging key."},"to":{"type":"string","title":"To","description":"The recipient, canonicalised where it could be."},"variables":{"additionalProperties":{"type":"string"},"type":"object","title":"Variables","description":"The values this row was sent with."},"template_sid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Sid","description":"The provider content id this row used."},"validation":{"type":"string","title":"Validation","description":"`valid`, `warn` or `invalid`, decided before sending."},"validation_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Validation Reason","description":"Why validation said so, or `null`."},"send_status":{"type":"string","title":"Send Status","description":"`pending` (not attempted yet), `sent` (WhatsApp accepted it), `failed` (WhatsApp refused it — `send_reason` says why), `skipped` (validation rejected it before the run started) or `cancelled` (the run was stopped before this row went out)."},"send_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Send Reason","description":"Why a row failed, was skipped or was cancelled."},"provider_sid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Sid","description":"The provider's own message id for a sent row, for support queries."},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At","description":"When the row was created (UTC)."},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At","description":"When its outcome was last written (UTC)."}},"type":"object","required":["id","ordinal","to","variables","validation","send_status"],"title":"BulkSendRowResource","description":"What happened to ONE recipient of a run.","examples":[{"created_at":"2026-08-17T09:14:03.221Z","id":"wbsr-91ab77c0","ordinal":3,"provider_sid":"SM7f1c0f0a2b","send_status":"sent","template_sid":"HX7c1f0a2b","to":"+34600111222","updated_at":"2026-08-17T09:14:07.905Z","validation":"valid","variables":{"1":"María","2":"el jueves a las 10:00"}}]},"BulkSendRun":{"properties":{"id":{"type":"string","title":"Id","description":"Stable id of the run. Poll it, or cancel it."},"status":{"type":"string","title":"Status","description":"`queued` (accepted, not started), `running` (draining), `completed` (every row resolved), `failed` (the run itself could not proceed — e.g. the account's sender credentials disappeared) or `cancelled` (stopped by hand; already-sent rows still stand)."},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id","description":"The run's default template, if it still exists."},"template_name":{"type":"string","title":"Template Name","description":"That template's name at the time of sending."},"total_rows":{"type":"integer","title":"Total Rows","description":"Recipients the run was created with."},"valid_rows":{"type":"integer","title":"Valid Rows","description":"Of those, the ones cleared for sending."},"sent_rows":{"type":"integer","title":"Sent Rows","description":"Rows WhatsApp has accepted so far."},"failed_rows":{"type":"integer","title":"Failed Rows","description":"Rows that were attempted and refused."},"pending_rows":{"type":"integer","title":"Pending Rows","description":"Sendable rows still waiting to go out. Always 0 once `status` is terminal — including a `cancelled` run, whose remaining rows were abandoned rather than queued, and which `GET /v1/bulk-sends/{id}/rows` reports individually as `cancelled`. It is the single number to poll if you only want to know 'is there anything still coming?'."},"created_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By","description":"Who started it: `api:<key name>` for a run created through this API."},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At","description":"When the run was accepted (UTC)."},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At","description":"When it last made progress (UTC)."}},"type":"object","required":["id","status","template_name","total_rows","valid_rows","sent_rows","failed_rows","pending_rows","created_at"],"title":"BulkSendRun","description":"A bulk send and how far it has got.","examples":[{"created_at":"2026-08-17T09:14:03.221Z","created_by":"api:CRM sync","failed_rows":1,"id":"wbs-4f2c1a9e","pending_rows":130,"sent_rows":117,"status":"running","template_id":"tpl_01HZY0Q8M2","template_name":"cita_confirmada","total_rows":250,"updated_at":"2026-08-17T09:15:44.870Z","valid_rows":248}]},"BulkSendSummary":{"properties":{"total":{"type":"integer","title":"Total","description":"How many recipients were examined."},"valid":{"type":"integer","title":"Valid","description":"Recipients that will be sent."},"warn":{"type":"integer","title":"Warn","description":"Recipients missing a required variable. **These are NOT sent** — a template cannot render without all of its placeholders — so treat a warn exactly as you treat an invalid."},"invalid":{"type":"integer","title":"Invalid","description":"Recipients that cannot be sent: bad number, or bad template."},"sendable":{"type":"integer","title":"Sendable","description":"The number that will actually leave — always equal to `valid`."}},"type":"object","required":["total","valid","warn","invalid","sendable"],"title":"BulkSendSummary","description":"Aggregate verdict counts for a list of recipients.","examples":[{"invalid":1,"sendable":2,"total":3,"valid":2,"warn":0}]},"ConversationPage":{"properties":{"data":{"items":{"$ref":"#/components/schemas/ConversationResource"},"type":"array","title":"Data","description":"The rows, newest-CREATED first."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page — pass it back as `?cursor=`. `null` means this was the last page, and it is the ONLY end-of-collection signal: a short page can still have more behind it."}},"type":"object","required":["data"],"title":"ConversationPage","description":"A page of conversations plus the cursor that continues it.","examples":[{"data":[{"assignment_status":"open","contact":"+34600111222","created_at":"2026-08-10T18:02:41.900Z","display_name":"María López","id":"cnv_01HZY0RRPQ","last_inbound_at":"2026-08-17T09:11:58.004Z","last_message_at":"2026-08-17T09:14:03.221Z","session_open":true,"status":"open","updated_at":"2026-08-17T09:14:03.221Z"}],"next_cursor":"eyJ2IjoxfQ.Zm9vYmFy"}]},"ConversationResource":{"properties":{"id":{"type":"string","title":"Id","description":"Stable id of the conversation."},"contact":{"type":"string","title":"Contact","description":"The contact's phone number in E.164 (`+34600111222`)."},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name","description":"The best name we hold for the contact: an operator's override if one was set, otherwise the WhatsApp profile name. `null` when neither exists."},"status":{"type":"string","title":"Status","description":"`open` or `closed`."},"assignment_status":{"type":"string","title":"Assignment Status","description":"Collaboration state: `open`, `claimed` or `resolved`."},"assigned_agent_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned Agent Name","description":"The human who has claimed the thread, if any."},"session_open":{"type":"boolean","title":"Session Open","description":"Whether a free-form message can be delivered right now. When `false` only an approved template will reach this contact."},"last_inbound_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Inbound At","description":"When the CONTACT last wrote — the clock the session window runs on."},"last_message_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Message At","description":"When anybody last wrote, in either direction."},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At","description":"When the conversation was created (the paging key)."},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At","description":"When the row last changed — what `updated_since` filters on."},"last_message":{"anyOf":[{"$ref":"#/components/schemas/LastMessage"},{"type":"null"}],"description":"The most recent message, or `null` for an empty thread."}},"type":"object","required":["id","contact","status","assignment_status","session_open"],"title":"ConversationResource","description":"One conversation — a thread with a single contact.","examples":[{"assignment_status":"open","contact":"+34600111222","created_at":"2026-08-10T18:02:41.900Z","display_name":"María López","id":"cnv_01HZY0RRPQ","last_inbound_at":"2026-08-17T09:11:58.004Z","last_message":{"at":"2026-08-17T09:14:03.221Z","author":"human:api:CRM sync","body":"Su cita queda confirmada para el jueves a las 10:00.","direction":"outbound"},"last_message_at":"2026-08-17T09:14:03.221Z","session_open":true,"status":"open","updated_at":"2026-08-17T09:14:03.221Z"}]},"LastMessage":{"properties":{"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body","description":"Its text (truncated by nothing)."},"author":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Author","description":"Its author, same vocabulary as above."},"direction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Direction","description":"`inbound` or `outbound`."},"at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"At","description":"When it was written (UTC)."}},"type":"object","title":"LastMessage","description":"The most recent message of a conversation, for triage without a second call.","examples":[{"at":"2026-08-17T09:11:58.004Z","author":"end_user","body":"¿Puedo pagar el IBI a plazos?","direction":"inbound"}]},"MessagePage":{"properties":{"data":{"items":{"$ref":"#/components/schemas/MessageResource"},"type":"array","title":"Data","description":"The rows, OLDEST first."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page; `null` on the last page."}},"type":"object","required":["data"],"title":"MessagePage","description":"A page of transcript rows plus the cursor that continues it.","examples":[{"data":[{"author":"end_user","body":"¿Puedo pagar el IBI a plazos?","conversation_id":"cnv_01HZY0RRPQ","created_at":"2026-08-17T09:11:58.004Z","direction":"inbound","id":"msg_01HZY0S3T4","num_media":0,"provider_sid":"SM2b9d0e11aa"}]}]},"MessageResource":{"properties":{"id":{"type":"string","title":"Id","description":"Stable id of this message. Pass it to `GET /v1/messages/{id}`."},"conversation_id":{"type":"string","title":"Conversation Id","description":"The conversation this message belongs to."},"direction":{"type":"string","title":"Direction","description":"`inbound` (from the contact) or `outbound` (from you)."},"author":{"type":"string","title":"Author","description":"Who wrote it: `end_user` for the contact, `ai` for the assistant, or `human:<label>` for a person. Messages sent through this API are attributed as `human:api:<key name>`, so the console inbox shows which integration sent them."},"body":{"type":"string","title":"Body","description":"The text as delivered. Empty for a media-only message."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Delivery status as last reported by WhatsApp: `queued`, `sent`, `delivered`, `read`, `failed` or `undelivered`. `null` for an inbound message, which has no delivery state of ours."},"provider_sid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Sid","description":"The provider's own message id, for cross-referencing with support."},"num_media":{"type":"integer","title":"Num Media","description":"How many attachments the message carries.","default":0},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the message was written (UTC)."},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At","description":"When the row last changed — a delivery status advancing moves this and not `created_at`. `null` when the deployment has not recorded one yet."}},"type":"object","required":["id","conversation_id","direction","author","body","created_at"],"title":"MessageResource","description":"One row of a WhatsApp transcript, inbound or outbound.","examples":[{"author":"human:api:CRM sync","body":"Su cita queda confirmada para el jueves a las 10:00.","conversation_id":"cnv_01HZY0RRPQ","created_at":"2026-08-17T09:14:03.221Z","direction":"outbound","id":"msg_01HZY0S3T4","num_media":0,"provider_sid":"SM7f1c0f0a2b","status":"queued","updated_at":"2026-08-17T09:14:03.221Z"}]},"MessagingWindow":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id","description":"The conversation this answer is about."},"session_open":{"type":"boolean","title":"Session Open","description":"`true` while a free-form message is deliverable to this contact."},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"When the window closes (24h after the contact's last message). `null` when the contact has never written, so there is no window to expire."},"requires_template":{"type":"boolean","title":"Requires Template","description":"The actionable half of the answer: when `true`, only an approved template will reach this contact — see `GET /v1/templates`."},"last_inbound_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Inbound At","description":"When the contact last wrote, or `null` if never."}},"type":"object","required":["conversation_id","session_open","requires_template"],"title":"MessagingWindow","description":"Whether WhatsApp's 24-hour session window is open for a conversation.","examples":[{"conversation_id":"cnv_01HZY0RRPQ","expires_at":"2026-08-18T09:11:58.004Z","last_inbound_at":"2026-08-17T09:11:58.004Z","requires_template":false,"session_open":true}]},"SendMessageRequest":{"properties":{"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Reply inside this conversation. Requires the `messages:send` scope. Mutually exclusive with `to`."},"to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To","description":"Address this number instead of a conversation id. Requires the `messages:initiate` scope. Send E.164 (`+34600111222`): a prefix-less number is only resolvable when the account states a country, and is otherwise refused with `invalid_recipient` rather than guessed at.\n\n**A free-form `body` to this number only works while the contact's 24-hour window is open** — that is WhatsApp's rule, not ours, and a number that has never written has no window at all, so the send is refused with `session_window_closed`. To reach somebody who has not written (or has gone quiet for a day), send a `template`: that is the only body shape that can OPEN a conversation."},"body":{"anyOf":[{"type":"string","maxLength":1600},{"type":"null"}],"title":"Body","description":"The message text, at most 1600 characters — WhatsApp's own per-message limit, so ONE call is ONE message on the contact's phone and one unit of your daily budget. A longer body is refused with `invalid_request`; split it yourself if you mean to send several. Required unless `template` is given, in which case it is ignored — WhatsApp renders an approved template from its own copy."},"template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template","description":"The name (or id) of an approved template. Sending one requires the `messages:initiate` scope and is the ONLY way to reach a contact whose 24-hour session window has closed."},"variables":{"additionalProperties":{"type":"string"},"type":"object","title":"Variables","description":"Values for the template's placeholders, keyed as `GET /v1/templates` lists them."}},"type":"object","title":"SendMessageRequest","description":"What to send, and to whom.\n\nExactly one of ``conversation_id`` (reply) or ``to`` (initiate) is required.\n``body`` is required unless ``template`` is given.","examples":[{"body":"Su cita queda confirmada para el jueves a las 10:00.","conversation_id":"cnv_01HZY0RRPQ"},{"template":"cita_confirmada","to":"+34600111222","variables":{"1":"María","2":"el jueves a las 10:00"}},{"body":"Hemos recibido su solicitud.","to":"+34600111222"}]},"SendMessageResponse":{"properties":{"mode":{"type":"string","title":"Mode","description":"How the send was interpreted: `reply` (into an existing conversation), `initiate` (free-form to a number) or `template`."},"conversation_id":{"type":"string","title":"Conversation Id","description":"The conversation the message landed in — created if it did not exist."},"to":{"type":"string","title":"To","description":"The recipient as canonicalised to E.164."},"message":{"$ref":"#/components/schemas/MessageResource","description":"The persisted transcript row."}},"type":"object","required":["mode","conversation_id","to","message"],"title":"SendMessageResponse","description":"The message that was handed to WhatsApp.","examples":[{"conversation_id":"cnv_01HZY0RRPQ","message":{"author":"human:api:CRM sync","body":"Su cita queda confirmada para el jueves a las 10:00.","conversation_id":"cnv_01HZY0RRPQ","created_at":"2026-08-17T09:14:03.221Z","direction":"outbound","id":"msg_01HZY0S3T4","num_media":0,"provider_sid":"SM7f1c0f0a2b","status":"queued","updated_at":"2026-08-17T09:14:03.221Z"},"mode":"reply","to":"+34600111222"}]},"TemplateList":{"properties":{"data":{"items":{"$ref":"#/components/schemas/TemplateResource"},"type":"array","title":"Data","description":"Approved templates, name-ordered."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Always `null`: a template catalogue is small and returned whole. The field exists so this collection has the same envelope as every other."}},"type":"object","required":["data"],"title":"TemplateList","description":"The usable templates for this account.","examples":[{"data":[{"body_preview":"Hola {{1}}, su cita queda confirmada para {{2}}.","category":"UTILITY","id":"tpl_01HZY0Q8M2","language":"es","name":"cita_confirmada","status":"approved","variables":["1","2"]}]}]},"TemplateResource":{"properties":{"id":{"type":"string","title":"Id","description":"Stable id; either this or `name` may be sent as `template`."},"name":{"type":"string","title":"Name","description":"The template's name, as approved by WhatsApp."},"language":{"type":"string","title":"Language","description":"Its language tag (`es`, `en`…)."},"category":{"type":"string","title":"Category","description":"WhatsApp's category, e.g. `UTILITY` or `MARKETING`."},"variables":{"items":{"type":"string"},"type":"array","title":"Variables","description":"The placeholder keys the template declares. EVERY one of them needs a non-empty value in `variables`, or the send is refused with `missing_template_variables`."},"body_preview":{"type":"string","title":"Body Preview","description":"The approved body with its placeholders, for building a preview."},"status":{"type":"string","title":"Status","description":"Always `approved` here — nothing else is listed."}},"type":"object","required":["id","name","language","category","variables","body_preview","status"],"title":"TemplateResource","description":"An approved WhatsApp template — one of the things that can open a conversation.","examples":[{"body_preview":"Hola {{1}}, su cita queda confirmada para {{2}}.","category":"UTILITY","id":"tpl_01HZY0Q8M2","language":"es","name":"cita_confirmada","status":"approved","variables":["1","2"]}]}},"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"sonia_sk_<env>_<prefix>_<secret>","description":"A key minted from the console. Scopes are derived from the features of the administrator who mints it, so a key can never exceed the person behind it — and disabling that person revokes the key."}}},"tags":[{"name":"meta","description":"Service metadata: liveness, the machine-readable error catalogue, and the identity of the calling key. None of these mutate anything, and `meta/health` and `meta/errors` need no credential."},{"name":"identity","description":"Who this credential is. Use `whoami` to confirm the account and the EFFECTIVE scope list before wiring anything else — a scope the key was not granted is refused at call time, not at mint time."},{"name":"messages","description":"Sending, and reading back what was sent. Replying inside a conversation the contact opened needs `messages:send`; writing to a number of your choosing — or sending a template — needs `messages:initiate`, because contacting somebody who has not written first is the spam-capable action. Every send is idempotent: retry with the same `Idempotency-Key` and you get the original result, never a second message on somebody's phone."},{"name":"conversations","description":"Reading threads and their transcripts, and asking whether WhatsApp's 24-hour session window is still open for one. Collections are paged by an opaque cursor over an immutable key, so a walk stays correct while new messages arrive underneath it — which an offset walk does not."},{"name":"bulk sends","description":"Sending ONE approved template to many people at once, each with their own variables. Always preview first: a bulk send reaches people who never wrote to you, so the cost of a bad list is paid on real phones. A single unusable recipient refuses the whole run by default — a silent partial send is the failure an integration only notices weeks later — and a run in flight can be cancelled, which is the difference between a mistake and an incident."},{"name":"templates","description":"The approved templates that can open a conversation or reach a contact whose session window has closed. Approval is WhatsApp's, not ours: only templates it has approved are listed, because nothing else can be sent."}],"security":[{"ApiKeyAuth":[]}],"servers":[{"url":"https://sonia.omniloy.com/v1","description":"This deployment"},{"url":"http://localhost:8000/v1","description":"Local development"}],"x-sonia-scopes":[{"scope":"conversations:read","feature":"whatsapp","description":"Read conversations, contacts and message history."},{"scope":"messages:send","feature":"whatsapp","description":"Reply inside an existing conversation."},{"scope":"messages:initiate","feature":"bulk_send","description":"Start a new conversation with a contact (outbound-initiated)."},{"scope":"conversations:control","feature":"whatsapp","description":"Claim, assign, escalate, close or hand off a conversation."},{"scope":"webhooks:manage","feature":"whatsapp","description":"Register, inspect and delete outbound event webhooks."},{"scope":"events:read","feature":"whatsapp","description":"Read the event stream (message status, escalations, handoffs)."},{"scope":"assistant:ask","feature":"answer_console","description":"Ask the assistant a question and receive a cited answer."}]}