UPPU API documentation

API reference

This page is generated from the OpenAPI document that UPPU serves at /v1/openapi.json: import that file into your tools to generate a client or to validate responses. Every endpoint lives under the base URL, https://messaging.uppu.net/v1 in production and https://messaging-staging.uppu.net/v1 on staging, and needs a key (Authorization: Bearer <key>) unless it says otherwise. Error codes are explained on the error codes page.

Every response follows the same conventions:

Servers

  • https://messaging.uppu.net/v1 Live (uppu_live_ keys)
  • https://messaging-staging.uppu.net/v1 Staging (uppu_test_ keys)

Channels

GET /v1/channels

List the channels this key can use

Authentication
API key (Authorization: Bearer)
Scope
Any key (no scope)

Needs no scope. A channel-bound key sees only its channel.

Parameters

  • limit query integer
    • Minimum: 1
    • Maximum: 100
    • Default: 25
  • cursor query string

    The next_cursor of the previous page. It carries the sort order, so order may be left out on later pages; an order that contradicts the cursor gets 422 validation_failed (field cursor). Repeat the time filters on every page.

  • order query string

    Sort order. With a cursor and no order, the order of the cursor applies.

    • Allowed values: asc, desc
    • Default: desc
  • created_after query string
    • Format: date-time
  • created_before query string
    • Format: date-time

Responses

GET /v1/channels/{id}

Get a channel

Authentication
API key (Authorization: Bearer)
Scope
Any key (no scope)

Needs no scope. A channel the key cannot see is 404.

Parameters

  • id path Id required

Responses

Messages

GET /v1/messages/{id}

Get a message of either direction

Authentication
API key (Authorization: Bearer)
Scope
messages:read

Needs the messages:read scope. A message outside the key's channels, or past retention, is 404. The API never masks text; a message older than the short retention of the special-category data protection may already be masked in storage (see the Message schema).

Parameters

  • id path Id required

Responses

GET /v1/messages

List messages of either direction (reconciliation after a webhook gap)

Authentication
API key (Authorization: Bearer)
Scope
messages:read

Needs the messages:read scope. Messages of the key's channels, newest first unless order=asc. Filter by channel_id, conversation_id, contact_id and direction; an id the key cannot see is 404. The cursor carries the order; repeat the filters and the time filters on every page. The API never masks text; a message older than the short retention of the special-category data protection may already be masked in storage (see the Message schema).

Parameters

  • channel_id query Id
  • conversation_id query Id
  • contact_id query Id
  • direction query string
    • Allowed values: in, out
  • limit query integer
    • Minimum: 1
    • Maximum: 100
    • Default: 25
  • cursor query string

    The next_cursor of the previous page. It carries the sort order, so order may be left out on later pages; an order that contradicts the cursor gets 422 validation_failed (field cursor). Repeat the time filters on every page.

  • order query string

    Sort order. With a cursor and no order, the order of the cursor applies.

    • Allowed values: asc, desc
    • Default: desc
  • created_after query string
    • Format: date-time
  • created_before query string
    • Format: date-time

Responses

POST /v1/messages

Send a message

Authentication
API key (Authorization: Bearer)
Scope
messages:send

Needs the messages:send scope; an inline consent also needs consents:write. At most 20 requests per second per key. The Idempotency-Key header is required and stays unique per channel for good: the same key with the same body returns 200 with the current state of the first message and Idempotent-Replayed: true, the same key with another body gets 409 idempotency_key_reused. Use a new UUID or ULID per logical send; a refused send keeps no key, so it can be retried with the same key once the condition changed.

Address the recipient by contact_id (recommended), by phone (E.164, WhatsApp only; a contact of the channel that owns the phone, otherwise a new unverified contact) or by an identity that already exists on the channel. UPPU picks Meta's address itself. Telegram has no reply window: a bot can write to a person who wrote to it first (their telegram identity exists) until they block it (422 recipient_unreachable); Telegram reports only sent (no delivered or read; see capabilities.delivery_statuses). Session content (text, interactive, reaction, location) needs the 24-hour window on WhatsApp, Messenger and Instagram; an approved template does not. The window is counted like Meta counts it: from Meta's timestamp of the person's last message (its sent_at, never later than its received_at), not from the moment the message reached UPPU. On Messenger and Instagram the person's reaction counts like a message (added within v1; removing a reaction does not). Meta can deliver old messages late; such a message arrives with the window already closed (service_window.open false in message.received) and a session message then gets 422 window_closed: on WhatsApp send a template. Template rules: MARKETING needs a recorded marketing consent, UTILITY a recorded utility consent or a message from the person on this number after the latest withdrawal, AUTHENTICATION is never sent. A policy refusal writes nothing but a ledger row; a validation error writes nothing. Media (image, video, audio, document) is sent by media_id (POST /media) or by an https link that UPPU downloads, stores and uploads to Meta before sending (on Telegram the file travels inside the send call itself). Added within v1: stickers (by media_id only) and contact cards on WhatsApp and Telegram (see StickerContent and ContactsContent); other channels get 422 content_not_supported_on_channel. Show the person that an answer is coming with POST /conversations/{id}/typing.

E-mail channels (added within v1): text and media (image, video, audio, document; the file travels as an attachment of the e-mail). Address the person by contact_id or by their email identity (the address they wrote from; they must have written to the channel first); a phone gets 422 recipient_invalid. There is no reply window. UPPU sends from the channel's sending address, DKIM-signed with the key of its verified domain, as a reply in an e-mail thread of the person (subject "Re: ...", In-Reply-To and References): by default the conversation's last received e-mail, or the received e-mail you name in context.message_id (a person can have several threads in one open conversation; another kind of message gets 422 reply_target_invalid with details.reason not_an_email). Without a sending address or a verified domain the send gets 409 channel_unavailable (details.reason no_sender or sender_unverified; email_off while UPPU's e-mail channel is switched off). E-mail reports only sent (UPPU's mail server took the message) and failed (it could not, or the recipient's server bounced it: error.code recipient_unreachable).

A send to a contact without an open conversation is recorded in a new, already resolved conversation; the reply of the person opens a conversation as usual. Statuses follow as message.status webhooks (accepted, sent, delivered, read, failed) with your client_reference. Do not resend on unknown.

Optional ttl (added within v1, accepted once UPPU switches it on, until then 422 validation_failed with code unknown_field): the message's time to live, whole seconds from 10 to 86400 (24 hours). If UPPU has not handed the message to the provider by created_at + ttl (it waited in the queue, for the rate limit, for a retry, or the channel could not send), UPPU stops trying: status expired, error.code ttl_expired, a message.status event with status expired; nothing reaches Meta or Telegram. If an earlier attempt reached the provider and it answered with an error (for example its rate limit or a temporary error), the ttl only ends UPPU's retries: status failed with that error (for example provider_rate_limited or provider_unavailable), as when the retries run out without a ttl. The check runs before every attempt and every minute, so expired usually follows the deadline within about a minute; when thousands of messages run out at once it can take a few minutes longer, but nothing is sent after the deadline. A message handed over before the deadline is sent and never recalled (Meta's own retry period for an undelivered message is separate and set on the template, not per message). Without ttl nothing changes. On web chat a message is delivered when it is written, so ttl has no effect there. ttl is part of the body: the same Idempotency-Key with another ttl gets 409 idempotency_key_reused.

Parameters

  • Idempotency-Key header string required

    Unique per channel for good (1 to 64 characters: letters, digits, dot, underscore, colon, hyphen). Use a UUID or ULID per logical send.

    • Pattern: ^[A-Za-z0-9._:-]{1,64}$

Request body

application/json SendMessageRequest

  • channel_id Id required
  • to Recipient required
  • context object

    Reply to a message of the same channel and contact that has a provider id (WhatsApp; Telegram, a message of the contact's chat; e-mail, added within v1: a received e-mail of the contact, whose thread the reply joins).

    • message_id Id required
  • content OutboundContent required
  • client_reference string

    Your opaque reference (no personal data); echoed in GET and in every message.status event.

    • Minimum length: 1
    • Maximum length: 64
  • ttl integer

    Added within v1, accepted once UPPU switches it on (until then 422 validation_failed, code unknown_field). The message's time to live in whole seconds. If UPPU has not handed the message to the provider by created_at + ttl, it is not sent: status expired, error.code ttl_expired (status failed with the provider's error when an earlier attempt got an error answer from the provider). Out of range or not a whole number: 422 validation_failed, field ttl, code between.

    • Minimum: 10
    • Maximum: 86400

Responses

  • 200 A replay of an earlier send with the same Idempotency-Key and body

    Headers: X-Request-Id, UPPU-Service-Stop-At, Idempotent-Replayed

    application/json Message

  • 202 Accepted; status queued (WhatsApp, Messenger, Instagram, Telegram) or sent (web chat)

    Headers: X-Request-Id, UPPU-Service-Stop-At

    application/json Message

  • 400, 401, 403, 404, 409, 413, 422, 429, 500, 503 Errors use the common error envelope; see the error codes page.

    application/json ErrorEnvelope

POST /v1/messages/{id}/read

Mark an inbound message as read

Authentication
API key (Authorization: Bearer)
Scope
messages:send

Needs the messages:send scope. Only a WhatsApp message the contact sent. Read receipts exist only on WhatsApp: a message on any other channel (web chat, Messenger, Instagram, Telegram) is 422 content_not_supported_on_channel with details.type read_receipt and details.channel_type. On a WhatsApp channel, any other target is 422 not_inbound with details.reason: outbound (sent by the business), reaction (WhatsApp has no read receipt for reactions), system_notice (a WhatsApp notice such as a number change), not_a_message (an event stored as an unsupported message, such as request_welcome), unsupported_message (a message Meta delivered with its own unsupported type, content.unsupported.provider_type unsupported; Meta does not accept a read receipt for it) or no_provider_id. These never reach Meta. not_inbound is final and not retryable: a repeat gets the same answer and nothing is queued. A client that marks every received message as read should treat it as nothing to mark and accept reason values it does not know; the values listed never change. The 24-hour window applies. Returns 202 the first time and 200 with the current state on a repeat. Send an empty body or {}.

Parameters

  • id path Id required

Responses

Contacts

POST /v1/contacts/lookup

Find a contact by phone or identity

Authentication
API key (Authorization: Bearer)
Scope
contacts:read

Needs the contacts:read scope. Personal data never goes into a URL: the phone or identity is in the body. Give the channel and exactly one of phone (E.164, WhatsApp channels) or identity. A contact that is not found on the channel is 404. This is a read: it also works for read-only companies.

Request body

application/json ContactLookupRequest

  • channel_id Id required
  • phone string
    • Pattern: ^\+[1-9][0-9]{4,19}$
  • identity object
    • type string required
      • Allowed values: bsuid, psid, igsid, web_visitor, telegram, email, external
    • value string required
      • Maximum length: 255

Responses

GET /v1/contacts/{id}

Get a contact

Authentication
API key (Authorization: Bearer)
Scope
contacts:read

Needs the contacts:read scope. The same object as POST /contacts/lookup: the identities on the key's channels (superseded ones with current false), the reply window per WhatsApp, Messenger and Instagram channel and the current consent per channel type and purpose. A contact without an identity or a conversation on the key's channels is 404: a channel-bound key sees only the contacts of its channel.

Parameters

  • id path Id required

Responses

GET /v1/contacts/{id}/consents

List the consent records of a contact

Authentication
API key (Authorization: Bearer)
Scope
contacts:read

Needs the contacts:read scope. The contact's consent ledger, newest record first unless order=asc: every grant and withdrawal, also the automatic ones (the person's STOP message, Meta's marketing opt-out) and the ones the company recorded in the UPPU panel or imported from a file. Records are never changed; the current state per channel type and purpose is the record with the latest collected_at (a withdrawal wins a tie), which the contact's consents summary shows. A record added later can be older than the current one: the newest record (and the latest contact.consent_changed event) is not always the current state. created_after and created_before filter on recorded_at. A contact the key cannot see is 404.

Parameters

  • id path Id required
  • limit query integer
    • Minimum: 1
    • Maximum: 100
    • Default: 25
  • cursor query string

    The next_cursor of the previous page. It carries the sort order, so order may be left out on later pages; an order that contradicts the cursor gets 422 validation_failed (field cursor). Repeat the time filters on every page.

  • order query string

    Sort order. With a cursor and no order, the order of the cursor applies.

    • Allowed values: asc, desc
    • Default: desc
  • created_after query string
    • Format: date-time
  • created_before query string
    • Format: date-time

Responses

Consents

POST /v1/consents

Record a consent

Authentication
API key (Authorization: Bearer)
Scope
consents:write

Needs the consents:write scope. Adds a record to the contact's consent ledger (records are never changed): a grant or a withdrawal of the marketing or utility purpose, on a WhatsApp channel (another channel type gets 422 content_not_supported_on_channel). Address the person by contact_id (the contact needs an identity on the channel), by phone (E.164; the channel's contact with that phone, otherwise a new unverified contact) or by an identity that already exists on the channel. source_text (the exact text the person agreed to) and source_ref (your reference to the evidence, no personal data) are required. collected_at is when the person gave or withdrew the consent: now if left out, never in the future. A grant without collected_at gets 422 validation_failed (collected_at) while the latest record is a withdrawal: send the time the new consent was collected. Template sends read the record with the latest collected_at (a withdrawal wins a tie). Every new record emits contact.consent_changed; the event reports the record that was written, not the current state (a grant older than a newer withdrawal is recorded and sent as granted while the withdrawal stays in effect): read the current state from the contact's consents summary (GET /contacts/{id}). An optional Idempotency-Key (company scope): the same key with the same consent (contact, purpose, state, source_text and source_ref) returns 200 with the first record and Idempotent-Replayed: true, another consent with the same key gets 409 idempotency_key_reused. Without a key every request adds a record.

Parameters

  • Idempotency-Key header string

    Optional; letters, digits, dot, underscore, colon or hyphen, at most 64.

    • Pattern: ^[A-Za-z0-9._:-]{1,64}$

Request body

application/json ConsentRequest

  • channel_id Id required
  • to Recipient required
  • purpose string required
    • Allowed values: marketing, utility
  • state string required
    • Allowed values: granted, withdrawn
  • source_text string required

    The exact text the person agreed to (or the withdrawal they asked for).

    • Maximum length: 2000
  • source_ref string required

    Your reference to the evidence (no personal data), for example web-form:checkout:7781.

    • Maximum length: 191
  • collected_at string

    When the person gave or withdrew the consent; now if left out, never in the future.

    • Format: date-time

Responses

Conversations

GET /v1/conversations

List conversations

Authentication
API key (Authorization: Bearer)
Scope
messages:read

Needs the messages:read scope. Conversations of the key's channels, newest first unless order=asc. Filter by contact_id, channel_id and status (open or resolved); an id the key cannot see is 404. created_after and created_before filter on opened_at. The cursor carries the order; repeat the filters and the time filters on every page. A conversation resolved or reopened between two pages keeps the cursor valid: the next page continues after it, so you can resolve the conversations of a page before asking for the next one.

Parameters

  • contact_id query Id
  • channel_id query Id
  • status query string
    • Allowed values: open, resolved
  • limit query integer
    • Minimum: 1
    • Maximum: 100
    • Default: 25
  • cursor query string

    The next_cursor of the previous page. It carries the sort order, so order may be left out on later pages; an order that contradicts the cursor gets 422 validation_failed (field cursor). Repeat the time filters on every page.

  • order query string

    Sort order. With a cursor and no order, the order of the cursor applies.

    • Allowed values: asc, desc
    • Default: desc
  • created_after query string
    • Format: date-time
  • created_before query string
    • Format: date-time

Responses

GET /v1/conversations/{id}

Get a conversation

Authentication
API key (Authorization: Bearer)
Scope
messages:read

Needs the messages:read scope. A conversation outside the key's channels, or past retention, is 404. The assignee is never shown, only whether the conversation is assigned. service_window is the contact's current 24-hour reply window on the conversation's channel (null on web chat, Telegram and e-mail).

Parameters

  • id path Id required

Responses

POST /v1/conversations/{id}/resolve

Resolve a conversation

Authentication
API key (Authorization: Bearer)
Scope
conversations:write

Needs the conversations:write scope. Send an empty body or {}. Resolves an open conversation the way the team inbox does (the same audit record, with your key as the actor); a conversation that is already resolved stays as it is. Both answer 200 with the current conversation, so a retry is safe. The person's next message opens a new conversation. Endpoints subscribed to conversation.updated get it with change resolved (added within v1). A read-only company gets 403 tenant_read_only.

Parameters

  • id path Id required

Responses

POST /v1/conversations/{id}/handover

Hand a conversation over to the team

Authentication
API key (Authorization: Bearer)
Scope
conversations:write

Needs the conversations:write scope; added within v1. On a channel your system (handling api) or UPPU Bot (handling bot) answers, gives the conversation to the team: held_by becomes team, handed_over_at is set, handover_reason is api, and the conversation joins the team inbox's normal views (Unassigned, waiting for a reply). The team gets UPPU's usual handover help: the channel's automatic assignment when it is on, and an e-mail when no one replies within 10 minutes. On a bot channel UPPU Bot stops at once and sends nothing more in the conversation. Nothing is sent to the person; send your own message with POST /messages if you want to. Send an empty body, {} or {reason, note}: reason is a short code of your own (at most 64 characters: letters, digits, dot, underscore, colon, hyphen; no personal data), note a text for the team (at most 1000 characters). Both appear only in the team inbox, next to the handover; the API and the webhooks never return them. A conversation that is already with the team stays as it is, and nothing is written again. Both answer 200 with the current conversation, so a retry is safe. Endpoints subscribed to conversation.updated get change handed_over once. An inbox channel gets 422 handling_not_supported (details.handling), and so does a company whose plan has no messaging panel (details.reason not_in_plan: there is no team to hand over to); a resolved conversation gets 409 not_held. A read-only company gets 403 tenant_read_only.

Parameters

  • id path Id required

Request body

application/json HandoverRequest

  • reason string

    A short code of your own for why you hand the conversation over, for example customer_request. No personal data.

    • Pattern: ^[A-Za-z0-9._:-]+$
    • Maximum length: 64
  • note string

    A note for the team (line breaks allowed, no other control characters).

    • Maximum length: 1000

Responses

POST /v1/conversations/{id}/release

Give a conversation back from the team

Authentication
API key (Authorization: Bearer)
Scope
conversations:write

Needs the conversations:write scope; added within v1. Send an empty body or {}. Gives a conversation the team holds back to the automation of its channel: on an api channel to your system (held_by api, the conversation leaves the team's views for the inbox's API view), on a bot channel to UPPU Bot (held_by bot, the way "Return to bot" in the inbox does: UPPU Bot answers only the person's next message, never the older ones). handed_over_at and handover_reason become null and the assignment is removed. A conversation the automation already holds stays as it is, and nothing is written again. Both answer 200 with the current conversation, so a retry is safe. Endpoints subscribed to conversation.updated get change released (api channel) or returned_to_bot (bot channel), after unassigned when someone was assigned. On a bot channel UPPU Bot must be able to take the conversation, otherwise 409 bot_not_ready with details.reason bot_unavailable (UPPU Bot is off on the channel or for the company), bot_busy (it is still finishing its turn from before the handover; retry in a few seconds) or reply_first (the person's last message has no reply yet, from the team or your system; reply first). An inbox channel gets 422 handling_not_supported, a resolved conversation 409 not_handed_over, a read-only company 403 tenant_read_only.

Parameters

  • id path Id required

Responses

POST /v1/conversations/{id}/typing

Show a typing indicator to the contact

Authentication
API key (Authorization: Bearer)
Scope
messages:send

Needs the messages:send scope; added within v1. Shows "typing…" to the contact of the conversation while your system prepares an answer. Send an empty body or {}; no Idempotency-Key. A typing indicator is not a message: nothing is stored, nothing is billed and no webhook event follows; it ends when your message arrives or after a short time set by the network (WhatsApp 25 seconds, Telegram 5 seconds), and cannot be switched off early. Call it again for a longer answer. GET /channels tells you where it works (capabilities.typing_indicator): WhatsApp, Messenger and Telegram; any other channel (web chat, Instagram) gets 422 content_not_supported_on_channel with details.type typing_indicator and details.channel_type.

On WhatsApp the indicator travels with Meta's mark-as-read call: it marks the newest message the contact sent in this conversation (and the earlier ones) as read, the same target rules as POST /messages/{id}/read; message_id in the answer names that message. Without such a message from the last 30 days the answer is 422 not_inbound with details.reason no_inbound_message. The rules of a send apply: the 24-hour window on WhatsApp and Messenger (422 window_closed), a paused or unavailable channel (409), a shadow channel (403 shadow_mode), a blocked contact (422 contact_blocked) and, on Telegram, a person who blocked the bot (422 recipient_unreachable). At most one indicator per conversation every few seconds (429 rate_limited with details.scope conversation and Retry-After) and a per-key limit of its own. The answer is 202 with state queued; the outcome is not reported. A read-only company gets 403 tenant_read_only.

Parameters

  • id path Id required

Responses

Templates

GET /v1/templates

List the WhatsApp templates of the key's channels

Authentication
API key (Authorization: Bearer)
Scope
templates:read

Needs the templates:read scope. Templates belong to the WhatsApp Business account of a channel; channel_ids lists the key's channels that can use a template. Deleted templates are left out unless status asks for them. sendable tells whether POST /messages accepts the template today; send_example is a ready content object built from the template's own example values.

Parameters

  • channel_id query Id
  • status query string

    Meta's status, for example APPROVED.

  • name query string
  • language query string

    For example tr or en_US.

  • limit query integer
    • Minimum: 1
    • Maximum: 100
    • Default: 25
  • cursor query string

    The next_cursor of the previous page. It carries the sort order, so order may be left out on later pages; an order that contradicts the cursor gets 422 validation_failed (field cursor). Repeat the time filters on every page.

  • order query string

    Sort order. With a cursor and no order, the order of the cursor applies.

    • Allowed values: asc, desc
    • Default: desc
  • created_after query string
    • Format: date-time
  • created_before query string
    • Format: date-time

Responses

POST /v1/templates

Create a WhatsApp template

Authentication
API key (Authorization: Bearer)
Scope
templates:write

Needs the templates:write scope (added within v1). No preset includes it and existing keys do not have it: the owner adds it to a key on the developer page on purpose. Creates the template in the WhatsApp Business account of the channel with the rules of the panel's template page: name of lowercase letters, digits and underscores; language tr or en; category UTILITY or MARKETING; an optional text header with at most one variable; the body with numbered ({{1}}, {{2}} ...) or named ({{first_name}}) variables and an example for each; an optional footer; buttons: up to 3 quick replies, 2 URL buttons (a variable only as {{1}} at the end of the address, with an example of that end) and 1 phone number, the quick replies next to each other. A header with an image, a video or a document (added within v1) takes a sample file: the media_id of an upload of the same channel made with POST /media, {"type": "image", "image": {"media_id": "..."}}; a JPEG or PNG image up to 5 MB, an MP4 video up to 16 MB, a PDF document up to 25 MB (otherwise 422 validation_failed with code mime_not_allowed, or 413 media_too_large); an upload of another channel, an expired one or one past its reuse period gets 422 media_unavailable (details.reason). UPPU uploads the sample to Meta with Meta's Resumable Upload before it creates the template; Meta's refusal of the sample is 422 validation_failed with field header.<type>.media_id, code sample_refused and Meta's code in provider_error, and no template is created. The upload and the create run within the time of the request (about 26 seconds): when the upload leaves no time for the create, the answer is 503 provider_unavailable with details.reason unreachable and nothing is created (retry, or use a smaller sample). A retry after unconfirmed with the same Idempotency-Key and template gets the template Meta created even when the sample's upload is no longer usable. A header with a location cannot be created here (422 validation_failed, field header.type, code not_supported). A field that breaks a rule gets 422 validation_failed with the field's path (header.text, body.examples.1, buttons.0.url) and code invalid. The answer is 201 with the template, usually status PENDING: Meta reviews it, mostly within 24 hours, and template.updated reports the result. The same name and language again, or a name whose every language was deleted within 30 days, gets 422 validation_failed with field name (code exists or name_blocked); Meta allows 100 new templates per hour for a WhatsApp account (429 rate_limited, details.reason templates_per_hour, with Retry-After). Meta's refusal of the template is 422 validation_failed with field template, code meta_refused and Meta's code in provider_error. The call to Meta runs inside the request: one template create or delete at a time per company (429 rate_limited with details.reason concurrent_writes otherwise) and at most 10 per minute per company. When Meta's own rate limit is reached (for example templates created outside UPPU) the answer is 429 rate_limited with details.reason provider_rate_limited, Retry-After and Meta's code in provider_error. When Meta cannot be reached the answer is 503 provider_unavailable with details.reason unreachable (nothing was created); when Meta does not answer in time or answers with a temporary error, unconfirmed (the template may have been created). A retry with the same Idempotency-Key and template is safe: UPPU looks at Meta again and answers 200 with the template and Idempotent-Replayed: true when the earlier request created it, creates it when it did not, and answers unconfirmed again when Meta still cannot be read. Without a key, read GET /templates?name=&language= after a minute before you retry. A channel that is not WhatsApp gets 422 content_not_supported_on_channel (details.type template); a key bound to one channel that names another channel 403 channel_not_allowed; a channel without a usable WhatsApp connection 409 channel_unavailable (details.reason; credentials also when Meta refuses the account's access token, with Meta's code in provider_error); a number in shadow mode 403 shadow_mode. An optional Idempotency-Key (company scope): the same key with the same template answers 200 with the template as it is now and Idempotent-Replayed: true; another template with the same key gets 409 idempotency_key_reused. A read-only company gets 403 tenant_read_only.

Parameters

  • Idempotency-Key header string

    Optional; letters, digits, dot, underscore, colon or hyphen, at most 64. Kept only by an accepted create; after 503 provider_unavailable with details.reason unconfirmed a retry with the same key and template looks at Meta again (for 24 hours).

    • Pattern: ^[A-Za-z0-9._:-]{1,64}$

Request body

application/json TemplateCreateRequest

  • channel_id string required

    A WhatsApp channel of the key; the template belongs to the channel's WhatsApp Business account.

    • Pattern: ^[0-9a-hjkmnp-tv-z]{26}$
  • name string required

    1 to 512 lowercase letters, digits and underscores, for example order_update.

    • Pattern: ^[a-z0-9_]{1,512}$
  • language string required
    • Allowed values: tr, en
  • category string required

    Upper or lower case. Authentication templates cannot be created.

    • Allowed values: UTILITY, MARKETING, utility, marketing
  • body TemplateCreateBody required
  • footer object

    One line of at most 60 characters, without variables.

    • text string required
      • Maximum length: 60
  • At most 3 quick replies, 2 URL buttons and 1 phone number button; the quick replies next to each other, before or after the other buttons. Button texts are one line of at most 25 characters without variables and differ from each other.

Responses

  • 200 A replay of an earlier request with the same Idempotency-Key and template, also when the earlier request got 503 unconfirmed and created the template

    Headers: X-Request-Id, UPPU-Service-Stop-At, Idempotent-Replayed

    application/json Template

  • 201 The new template, sent to Meta for review

    Headers: X-Request-Id, UPPU-Service-Stop-At

    application/json Template

  • 400, 401, 403, 404, 409, 413, 422, 429, 500, 503 Errors use the common error envelope; see the error codes page.

    application/json ErrorEnvelope

GET /v1/templates/{id}

Get a template

Authentication
API key (Authorization: Bearer)
Scope
templates:read

Needs the templates:read scope. A template outside the key's channels is 404.

Parameters

  • id path Id required

Responses

DELETE /v1/templates/{id}

Delete a WhatsApp template in one language

Authentication
API key (Authorization: Bearer)
Scope
templates:write

Needs the templates:write scope (added within v1; in no preset). Deletes this template, in this language only: UPPU sends Meta the template's id together with its name, so the other languages of the name stay. Delete every language one by one to remove a name; once every language of a name is deleted, Meta lets you use the name again only after 30 days (a create with it gets 422 validation_failed, field name, code name_blocked). The template stays readable with status DELETED. Answers 200 with {object, id, name, language, deleted}; a template that is already deleted answers the same without calling Meta. Send an empty body or {}. Meta's refusal is 422 validation_failed with field template, code meta_refused and Meta's code in provider_error; Meta's rate limit (429 provider_rate_limited), 503 provider_unavailable (unconfirmed: the template may have been deleted), 409 channel_unavailable, 403 shadow_mode and the limits are those of creating. A template outside the key's channels is 404. A read-only company gets 403 tenant_read_only.

Parameters

  • id path Id required

Responses

Reports

GET /v1/reports/summary

Get the report of a period

Authentication
API key (Authorization: Bearer)
Scope
reports:read

Needs the reports:read scope (added within v1; in no preset, existing keys do not have it). The figures of the panel's Reports page, counted every night: conversations the contacts opened and those that reached the team, the ones left unanswered on their day, the team's replies, first response and resolution times, resolutions and reopenings, the open conversations at the end of the period, SLA targets met and missed, topics and tags; for the whole company also per channel, agent and group. Counts only: no contact, conversation or message. The period is days=7, 30 or 90 finished days (30 by default) or from and to (calendar days in the company's time zone, at most 90 days); it ends on the last day the nightly count has finished (period.to and period.counted_through; period.pending is true while yesterday is still being counted), so a to in the future or not counted yet is moved back. Any other value of the period gets 422 validation_failed (for example a period longer than 90 days, field from, code max). One of channel_id, member_id or group_id narrows the scope; an id the key cannot see is 404. A key bound to one channel reads only its channel (member_id and group_id get 403 channel_not_allowed). A channel UPPU stopped for the API gets 403 api_disabled, and its counts are one row with hidden true among the per-channel rows; the totals and the agent and group rows count every channel of the company. Durations are given per bucket (1m, 5m, 15m, 30m, 1h, 4h, 8h, 24h, 72h and over for first responses; 15m, 1h, 4h, 8h, 24h, 72h, 7d and over for resolutions: at most that long); median and p90 name the bucket in which half and nine tenths of the samples fall. The report's tier (tier, features) is that of the panel: with basic, agents, groups, SLA, topics and tags are null. Reports are part of the messaging panel: a company whose plan does not include it gets 403 api_disabled with details.resource reports and details.reason not_in_plan. At most 2 requests per second per key, together with GET /reports/daily.

Parameters

  • days query string

    The last 7, 30 or 90 finished days (30 when neither days nor from and to is given). Not together with from and to.

    • Allowed values: 7, 30, 90
  • from query string

    The first day (YYYY-MM-DD, the company's time zone); needs to. The period is at most 90 days.

    • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$
  • to query string

    The last day (YYYY-MM-DD); needs from. A day after the last counted day is moved back to it.

    • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$
  • channel_id query Id

    Only this channel. At most one of channel_id, member_id and group_id.

  • member_id query Id

    Only this agent, a current member of the company (the id in breakdowns.members); not with a key bound to one channel.

  • group_id query Id

    Only this group (the id in breakdowns.groups); not with a key bound to one channel.

Responses

GET /v1/reports/daily

List the days of a report

Authentication
API key (Authorization: Bearer)
Scope
reports:read

Needs the reports:read scope. The figures of each day of the period and scope (the period, the scope and the rules of GET /reports/summary), newest day first unless order=asc; a day without figures has zeros, and counted says whether the nightly count has finished that day. open and open_unassigned are the open conversations at the end of the day. Pass next_cursor as cursor for the next page and repeat the period and the scope on every page.

Parameters

  • days query string

    The last 7, 30 or 90 finished days (30 when neither days nor from and to is given). Not together with from and to.

    • Allowed values: 7, 30, 90
  • from query string

    The first day (YYYY-MM-DD, the company's time zone); needs to. The period is at most 90 days.

    • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$
  • to query string

    The last day (YYYY-MM-DD); needs from. A day after the last counted day is moved back to it.

    • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$
  • channel_id query Id

    Only this channel. At most one of channel_id, member_id and group_id.

  • member_id query Id

    Only this agent, a current member of the company (the id in breakdowns.members); not with a key bound to one channel.

  • group_id query Id

    Only this group (the id in breakdowns.groups); not with a key bound to one channel.

  • limit query integer
    • Minimum: 1
    • Maximum: 100
    • Default: 25
  • cursor query string

    The next_cursor of the previous page. It carries the sort order, so order may be left out on later pages; an order that contradicts the cursor gets 422 validation_failed (field cursor). Repeat the time filters on every page.

  • order query string

    Sort order. With a cursor and no order, the order of the cursor applies.

    • Allowed values: asc, desc
    • Default: desc

Responses

Events

GET /v1/events

Pull the events of the last 7 days (webhook fallback)

Authentication
API key (Authorization: Bearer)
Scope
events:read

Needs the events:read scope; at most 2 requests per second per key. Returns the same event ids and the same bodies that the webhooks deliver, oldest first, for 7 days after an event was created. Pass the next_cursor of the previous page as after. An event is listed about 10 seconds after it was recorded, once every transaction that may still write an earlier id has finished, so a cursor never skips an event. A channel-bound key sees only the events of its channel; events without a channel (template.updated and, added within v1, account.service_stop_scheduled and account.service_resumed) need an all-channels key. Test events are never listed.

Parameters

  • after query Id

    The id of the last event already processed: the next_cursor of the previous page, or the last webhook-id you handled (of any type, also one outside types). Prefer a cursor from this endpoint; events recorded in the same seconds as a webhook-id may sort before it.

  • types query string

    Comma-separated event types, for example message.received,message.status.

  • limit query integer
    • Minimum: 1
    • Maximum: 100
    • Default: 25

Responses

Media

POST /v1/media

Upload a media file to send later

Authentication
API key (Authorization: Bearer)
Scope
media:write

Needs the media:write scope; at most 2 requests per second and 1 GB per day per key. A multipart/form-data body with two fields: channel_id (a WhatsApp, Messenger, Instagram, Telegram or, added within v1, web chat channel the key can use; an Instagram channel while UPPU's Instagram outbound media is off gets 422 content_not_supported_on_channel; the channel's capabilities.content_types show whether media can be sent) and file; added within v1, an optional third field kind (see Stickers below). Any other field gets 422 validation_failed with code unknown_field. At most 25 MB; the type is read from the content and must be one the channel's type can send, otherwise 415 unsupported_media_type. The lists below are UPPU's: for WhatsApp and Instagram they stay within the types and sizes in Meta's documentation, which gives Messenger's sizes but no list of Messenger types. An MP4 container that reads as video is audio when the file part is declared audio/mp4, and video otherwise.

  • WhatsApp: image/jpeg, image/png up to 5 MB; video/mp4, video/3gpp, audio/aac, audio/amr, audio/mpeg, audio/mp4, audio/ogg up to 16 MB; PDF, plain text and Office documents up to 25 MB. UPPU checks the type and the size only. WhatsApp also requires 8-bit RGB or RGBA images, H.264 video with AAC audio (at most one audio stream) and OGG audio in Opus, mono; it can refuse a file that breaks these rules when the message is sent (message.status failed, for example with media_rejected).
  • Messenger: image/jpeg, image/png, image/gif up to 8 MB; video/mp4; audio/aac, audio/mpeg, audio/mp4, audio/wav; PDF, plain text and Office documents; all up to 25 MB.
  • Instagram (when outbound media is on): image/jpeg, image/png up to 8 MB; video/mp4, video/quicktime, video/webm; audio/aac, audio/mp4, audio/wav; up to 25 MB. No documents.
  • Telegram (added within v1): image/jpeg, image/png, image/webp up to 10 MB (Telegram's photo limit); video/mp4; audio/mpeg, audio/mp4, audio/aac, audio/ogg (sent as a voice note); PDF, plain text and Office documents; all up to 25 MB (UPPU's cap, below Telegram's 50 MB).
  • Web chat (added within v1): image/jpeg, image/png, image/webp, image/gif up to 10 MB; video/mp4, video/webm, video/quicktime; audio/mpeg, audio/mp4, audio/aac, audio/ogg, audio/wav; PDF, plain text, Office and OpenDocument documents; all up to 25 MB. UPPU removes an image's metadata (EXIF with the location, XMP, comments) without re-encoding it and keeps its orientation; size and sha256 are those of the stored file. The visitor gets the file in the chat widget through a short-lived link.
  • Stickers (added within v1): send the field kind with the value sticker to upload a sticker for content.sticker.media_id on WhatsApp and Telegram; another channel gets 422 content_not_supported_on_channel with details.type sticker, another value of kind 422 validation_failed (code in). The file must be a WebP of exactly 512×512 pixels, static up to 100 KB or animated up to 500 KB (WhatsApp's sizes; Telegram: static only). The upload's kind is sticker and the rules are checked here, in this order: another type gets 415 unsupported_media_type; then 422 validation_failed for the file with code sticker_unreadable when UPPU cannot read the WebP header, code sticker_animated_not_allowed for an animated file on a channel that sends static stickers only (whatever its size), 413 media_too_large for a file over the limit of its own kind, static or animated as read from the header (details.kind sticker, max_bytes of that limit, animated; a static file over 500 KB gets the static 100 KB), and code sticker_dimensions for other dimensions. Without kind an upload is checked as before: on WhatsApp a WebP gets 415 unsupported_media_type; on Telegram a WebP is a photo (kind image) that can also be sent as a sticker, with the sticker rules checked in the same order when it is sent.

A company stores at most 20 GB (413 media_too_large, details.reason tenant_quota).

The upload can be sent for 7 days (reusable_until) as content.{image|video|audio|document|sticker}.media_id; the file is kept 90 days after the last send that used it. An optional Idempotency-Key (company scope): the same key with the same file returns 200 with the first upload and Idempotent-Replayed: true, another request with the same key gets 409 idempotency_key_reused.

Parameters

  • Idempotency-Key header string

    Optional; letters, digits, dot, underscore, colon or hyphen, at most 64.

    • Pattern: ^[A-Za-z0-9._:-]{1,64}$

Request body

multipart/form-data object

  • channel_id Id required
  • file string required
    • Format: binary
  • kind string

    Optional, added within v1: sticker uploads the file as a sticker (WhatsApp and Telegram). It is part of the request for the Idempotency-Key.

    • Allowed values: sticker

Responses

GET /v1/media/{id}

Get a media file's metadata

Authentication
API key (Authorization: Bearer)
Scope
media:read

Needs the media:read scope. Inbound files (the media.id of a message.received image, video, audio, document or sticker), your uploads and the files UPPU fetched from your links. A file of a channel the key cannot see is 404. state is pending while UPPU downloads the file, ready once it is stored, failed with failure_reason, expired after retention (inbound 90 days; an upload 90 days after its last send). Messenger and Instagram: every media attachment of a message is its own file (content.{type}.media and content.{type}.attachments), whatever attachment comes before it, as is the preview of a shared post or a reel sent without a media attachment (content.unsupported.media). Instagram stories (a story reply's story, a story mention) are never downloaded: Meta does not allow storing story content. Meta's attachment links are short-lived: UPPU downloads them at once and retries temporary errors within the link's 24 hours. If a temporary error lasts so long that the next retry would fall outside those 24 hours, the file ends as failed with download_failed (about 16 hours of continuous outage with today's schedule). An expired link (Meta answers 403, 404 or 410, or the download could not start within 24 hours) ends as failed with provider_expired. Telegram (added within v1): every file of an inbound message (photo, video, video note, animation, voice note, audio, document, sticker) is downloaded by UPPU with the bot's token; neither Telegram's file id nor its file address is ever exposed. A file over 20 MB (Telegram's bot download limit) ends as failed with too_large; a file Telegram does not know any more ends with provider_refused. Temporary errors (Telegram unreachable, 5xx, 429, a rejected bot token until it is renewed) are retried in ten steps from 30 seconds to 24 hours (about 52 hours with today's schedule), then once a day; the last attempt is made when about an hour or less of the 7 days after receipt remains, even when earlier attempts were delayed (usually the shortened step exactly an hour before; only a file whose first attempt falls within that last hour is retried within it, on the common schedule while the next step fits), so a token renewed before the last attempt still brings the file, at most a day later. If the last attempt fails too, the file ends as failed with download_failed or no_token; a file UPPU could not try at all within those 7 days ends with download_failed. Web chat (added within v1): a file the visitor sends from the chat widget is stored as it arrives (ready at once; failed with quota_exceeded when the company's storage is full, the message is still written). The types and sizes are those of POST /media for web chat; an image's metadata is removed in the same way.

Parameters

  • id path Id required

Responses

GET /v1/media/{id}/content

Download a media file

Authentication
API key (Authorization: Bearer)
Scope
media:read

Needs the media:read scope; at most 10 requests per second and 2 GB per hour per key. Returns the bytes with Content-Type, Content-Length, ETag ("<sha256>"), Content-Disposition: attachment, X-Content-Type-Options: nosniff and Cache-Control: private, no-store; Range requests work (one range per request). The hourly byte budget is charged with the bytes a single range asks for; a request without Range, with several ranges or with If-Range is charged the whole file. While the file is still downloading the answer is 409 media_not_ready with Retry-After: 2 at once; the request is not held. A failed file is 422 media_unavailable (details.reason), a file past retention 410 media_expired. To hand a file over without your key, create a short-lived signed link (POST /media/{id}/link; added within v1).

Parameters

  • id path Id required

Responses

Create a signed download link

Authentication
API key (Authorization: Bearer)
Scope
media:read

Needs the media:read scope (added within v1). Returns a short-lived signed address that downloads the file without your API key, for example to hand a file to a browser or to another system of yours. Send an empty body or {}. The link works until expires_at (a few minutes; 5 today) and for at most max_uses requests, Range requests included. Only the expiry revokes a link: it cannot be cancelled. It is signed for this file of this company only and opens no other file, but anyone who has it can download the file until it expires: share it like the file itself. The file must be ready: 409 media_not_ready while it downloads, 422 media_unavailable when it failed, 410 media_expired after retention. Issuing a link counts as one request against the per-second limit of GET /media/{id}/content and needs room for one download of the whole file in the key's hourly byte budget (otherwise 429 rate_limited with details.reason bytes); it charges no bytes. Every download through the link then counts against the hourly byte budget of this key, as GET /media/{id}/content does. A read-only company can issue links too (it is a read).

Parameters

  • id path Id required

Responses

Download a file through a signed link

Authentication
No key needed

No API key (added within v1): the query string of the url that POST /media/{id}/link returned authorizes the request; use that url as it is. Returns the bytes with the headers of GET /media/{id}/content (Content-Disposition: attachment, X-Content-Type-Options: nosniff; one Range per request). A link that is not valid, or a link for another file, company or key, answers 404 not_found with details {"resource": "media_link", "reason": "invalid"}; an expired link the same with reason expired. A link used more than max_uses times answers 429 rate_limited with details.reason link_uses until it expires. Every download counts against the hourly byte budget of the key that issued the link (the budget of GET /media/{id}/content; a Range request with the bytes it asks for); when it is used up the answer is 429 rate_limited with details.reason bytes and Retry-After, and the refused request does not use up the link. A file that is no longer available answers 410 media_expired or 422 media_unavailable; 403 api_disabled when the company's plan no longer includes the API or UPPU stopped the channel. Browsers can open the link; no CORS headers are sent.

Parameters

  • id path Id required
  • team query Id required

    The company the link was signed for.

  • key_id query Id required

    The id of the API key that issued the link (not the key itself); its byte budget pays the downloads.

  • expires query string required

    When the link expires, in Unix seconds.

    • Pattern: ^[0-9]{1,12}$
  • signature query string required
    • Pattern: ^[A-Za-z0-9_-]{43}$

Responses

Webhook endpoints

GET /v1/webhook-endpoints

List webhook endpoints

Authentication
API key (Authorization: Bearer)
Scope
webhooks:manage

Needs the webhooks:manage scope (added within v1). No preset includes it and existing keys do not have it: the owner adds it to a key on the developer page on purpose. The company's webhook endpoints, the same ones the panel's Webhooks page shows, newest first unless order=asc. A key bound to one channel sees only the endpoints whose channel filter is that channel. The full address and the signing secret are never listed: url is masked (scheme, host and /...) and secret is null.

Parameters

  • limit query integer
    • Minimum: 1
    • Maximum: 100
    • Default: 25
  • cursor query string

    The next_cursor of the previous page. It carries the sort order, so order may be left out on later pages; an order that contradicts the cursor gets 422 validation_failed (field cursor). Repeat the time filters on every page.

  • order query string

    Sort order. With a cursor and no order, the order of the cursor applies.

    • Allowed values: asc, desc
    • Default: desc
  • created_after query string
    • Format: date-time
  • created_before query string
    • Format: date-time

Responses

POST /v1/webhook-endpoints

Create a webhook endpoint

Authentication
API key (Authorization: Bearer)
Scope
webhooks:manage

Needs the webhooks:manage scope. The rules of the panel's Webhooks page: https on port 443 only and a public host whose every address is public; a refused address gets 422 validation_failed with field url and the reason as its code (for example not_https, port_not_allowed, credentials_in_url, dns_unresolved, private, loopback or datacentre). events: at least one type a webhook endpoint can subscribe to; left out, the types that are on by default (message.received, message.status, contact.identity_changed, contact.erased, channel.status_changed, contact.reachability_changed). channel_id: null for every channel; left out, every channel, or the key's own channel for a key bound to one channel (which gets 403 channel_not_allowed for null or another channel). ordering best_effort (default) or strict; max_in_flight 1 to 32 (default 8); weekly_test (default false). A company has at most 5 endpoints: one more gets 409 plan_limit_reached with details resource webhook_endpoint, max and used. The new endpoint is pending_verification and gets no events until it passes a test event and the wrong-signature probe and is activated (POST /webhook-endpoints/{id}/test, /probe and /activate). The answer carries the signing secret in secret, only this once: store it on your server. An optional Idempotency-Key (company scope): the same key with the same body answers 200 with the endpoint, Idempotent-Replayed: true and secret null (if you lost the first answer, rotate the secret); another body with the same key gets 409 idempotency_key_reused. A read-only company gets 403 tenant_read_only. The address is looked up in DNS during the request, one at a time per company together with test events and probes: a create that would have to wait gets 429 rate_limited with details.reason concurrent_checks and Retry-After, and nothing is created.

Parameters

  • Idempotency-Key header string

    Optional; letters, digits, dot, underscore, colon or hyphen, at most 64.

    • Pattern: ^[A-Za-z0-9._:-]{1,64}$

Request body

application/json WebhookEndpointCreateRequest

  • url string required

    Your https address on port 443, at most 2048 characters.

    • Maximum length: 2048
  • events string[]

    At least one type; left out, the types that are on by default.

    • Allowed values: message.received, message.status, message.created, contact.identity_changed, contact.consent_changed, contact.erased, template.updated, channel.status_changed, channel.health_changed, conversation.updated, contact.reachability_changed, contact.updated, account.service_stop_scheduled, account.service_resumed
  • channel_id Id | null

    Only the events of this channel; null for every channel.

  • ordering string
    • Allowed values: best_effort, strict
    • Default: best_effort
  • max_in_flight integer
    • Minimum: 1
    • Maximum: 32
    • Default: 8
  • weekly_test boolean

    Send a test event every week (a failure is only a warning).

    • Default: false

Responses

GET /v1/webhook-endpoints/{id}

Get a webhook endpoint

Authentication
API key (Authorization: Bearer)
Scope
webhooks:manage

Needs the webhooks:manage scope. An endpoint of another company, or outside the channel of a key bound to one channel, is 404. verification says whether a test event and the wrong-signature probe passed within the last hour (the conditions of activation); weekly_test is the optional weekly test event.

Parameters

  • id path Id required

Responses

PATCH /v1/webhook-endpoints/{id}

Change a webhook endpoint

Authentication
API key (Authorization: Bearer)
Scope
webhooks:manage

Needs the webhooks:manage scope. Changes only the fields you send, with the rules of creating; an empty body changes nothing. A new url sends the endpoint back to pending_verification (its events wait) until it passes the test event and the wrong-signature probe again and is activated; a disabled endpoint stays disabled. A key bound to one channel cannot remove the channel filter or move it to another channel (403 channel_not_allowed). A read-only company gets 403 tenant_read_only. A body with url is looked up in DNS as on create, one at a time per company together with test events and probes (429 rate_limited with details.reason concurrent_checks otherwise, and nothing changes); a body without url is not.

Parameters

  • id path Id required

Request body

application/json WebhookEndpointUpdateRequest

  • url string

    A new address sends the endpoint back to verification.

    • Maximum length: 2048
  • events string[]
    • Allowed values: message.received, message.status, message.created, contact.identity_changed, contact.consent_changed, contact.erased, template.updated, channel.status_changed, channel.health_changed, conversation.updated, contact.reachability_changed, contact.updated, account.service_stop_scheduled, account.service_resumed
  • channel_id Id | null
  • ordering string
    • Allowed values: best_effort, strict
  • max_in_flight integer
    • Minimum: 1
    • Maximum: 32
  • weekly_test boolean

Responses

DELETE /v1/webhook-endpoints/{id}

Delete a webhook endpoint

Authentication
API key (Authorization: Bearer)
Scope
webhooks:manage

Needs the webhooks:manage scope. Deletes the endpoint and its deliveries; the events stay available for the other endpoints and for GET /events. Answers 200 with {object, id, deleted}. A read-only company gets 403 tenant_read_only (the panel can still delete it).

Parameters

  • id path Id required

Responses

POST /v1/webhook-endpoints/{id}/secret

Rotate the signing secret

Authentication
API key (Authorization: Bearer)
Scope
webhooks:manage

Needs the webhooks:manage scope. Send an empty body or {}. Creates a new signing secret and returns it in secret, only in this answer: no endpoint shows a secret again. The secret it replaces keeps signing for 24 hours (webhook-signature then carries two v1 values; secret_rotating_until says until when): switch your receiver to the new secret within that time. A rotation keeps only the secret it replaces, so rotate again only after your receiver uses the newest secret.

Parameters

  • id path Id required

Responses

POST /v1/webhook-endpoints/{id}/test

Send a test event to a webhook endpoint

Authentication
API key (Authorization: Bearer)
Scope
webhooks:manage

Needs the webhooks:manage scope. Sends a sample event of type (webhook.test if left out) to the endpoint now, with the endpoint's signature and the published egress IP, test true in the body, and answers with the result: passed (your server answered 2xx), status, latency_ms, the first characters of the answer and the error, if any. The request waits for your server (at most 10 seconds). Tests and probes together: at most 10 per minute for all keys of the company, one at a time per company and only a few at once for the whole service. A check that would have to wait is not queued: it answers 429 rate_limited with details.reason concurrent_checks and Retry-After, nothing is sent and it is not counted; send it again then. A passing test also clears the warning of a failed weekly test. A test that passed within the last hour is a condition of activation. The event is not stored and not listed by GET /events.

Parameters

  • id path Id required

Request body

application/json WebhookEndpointTestRequest

  • type string

    The event type of the sample (webhook.test if left out).

    • Allowed values: message.received, message.status, message.created, contact.identity_changed, contact.consent_changed, contact.erased, template.updated, channel.status_changed, channel.health_changed, conversation.updated, contact.reachability_changed, contact.updated, account.service_stop_scheduled, account.service_resumed, webhook.test

Responses

POST /v1/webhook-endpoints/{id}/probe

Send the wrong-signature probe

Authentication
API key (Authorization: Bearer)
Scope
webhooks:manage

Needs the webhooks:manage scope. Send an empty body or {}. Sends webhook.test signed with a wrong secret. It passes when your server refuses it (any HTTP answer other than 2xx): that shows your receiver checks the signature. A 2xx answer or a connection error does not pass. A new endpoint, and an endpoint whose address changed, needs a passing probe within the last hour to be activated. Counted with the tests: at most 10 per minute for the company, one at a time (429 rate_limited with details.reason concurrent_checks otherwise).

Parameters

  • id path Id required

Responses

POST /v1/webhook-endpoints/{id}/activate

Activate a webhook endpoint

Authentication
API key (Authorization: Bearer)
Scope
webhooks:manage

Needs the webhooks:manage scope. Send an empty body or {}. Starts the deliveries: a pending_verification endpoint needs a passing test event and a passing wrong-signature probe within the last hour; a suspended or disabled endpoint a passing test event (and the probe too when its address changed). Otherwise 422 validation_failed with field endpoint and code not_verified. Held events are then delivered in order. An endpoint that already receives events stays as it is (200).

Parameters

  • id path Id required

Responses

POST /v1/webhook-endpoints/{id}/disable

Disable a webhook endpoint

Authentication
API key (Authorization: Bearer)
Scope
webhooks:manage

Needs the webhooks:manage scope. Send an empty body or {}. Stops the deliveries: pending, waiting and held deliveries become dead (the panel can send them again for 30 days). Activate it again after a passing test event. A disabled endpoint stays disabled (200).

Parameters

  • id path Id required

Responses

Staging simulator

POST /v1/sandbox/inbound

Staging simulator: a message from the contact

Authentication
API key (Authorization: Bearer)

Staging only (https://messaging-staging.uppu.net/v1 with a uppu_test_ key); the live API has no /sandbox paths (404). Needs the messages:send scope and a simulated channel, created on the developer page of the staging panel; any other channel gets 422 validation_failed (channel_id not_simulated). Nothing reaches Meta.

The content has the shape of a received message's content (message.received): text, image, video, audio, document, sticker (media_id of a file uploaded to the same channel with POST /media, otherwise a sample file), location, contacts, interactive_reply (a button or list reply; source template for a template's quick-reply button), reaction (message_id of a message on this channel; an empty emoji removes it) and unsupported (Meta's own unsupported type: marking it as read gets 422 not_inbound with details.reason unsupported_message, as on a real channel). from needs phone (E.164), bsuid or both. context.message_id makes it a reply. The event goes through the normal inbound pipeline: contact, conversation, 24-hour window, media download and the message.received webhook event are the same as for a real message.

Request body

application/json SandboxInboundRequest

  • channel_id Id required
  • from object required

    The sender; phone (E.164), bsuid or both, and an optional profile name.

    • phone string
      • Pattern: ^\+[1-9][0-9]{4,19}$
    • bsuid string
      • Maximum length: 140
    • name string
      • Maximum length: 128
  • content object required

    {"type": "<type>", "<type>": {...}} in the shape of a received message's content. text: {body}; image, video, document: {media_id?, caption?} (document also filename?); audio: {media_id?, voice?}; sticker: {media_id?, animated?}; location: {latitude, longitude, name?, address?, url?}; contacts: {contacts: [{name: {formatted_name, ...}, phones?, emails?, org?, addresses?, urls?, birthday?}]}; interactive_reply: {source?: interactive|template, kind?: button|list, id, title, description?}; reaction: {message_id, emoji}; unsupported: {} or nothing. Added within v1, while WhatsApp Flows are switched on: flow_reply: {flow_token?, response}, a completed form; with context.message_id set to a flow message and no flow_token, the token of that message is used.

    • type string required
      • Allowed values: text, image, video, audio, document, sticker, location, contacts, interactive_reply, reaction, unsupported, flow_reply
  • context object

    The message on this channel that the contact replies to.

    • message_id Id required

Responses

POST /v1/sandbox/statuses

Staging simulator: a delivery status

Authentication
API key (Authorization: Bearer)

Staging only. A delivery status for an outbound message on a simulated channel: sent, delivered, read or failed (provider_code is Meta's error code, default 131026). Statuses only move forward; a status that does not advance is ignored (reason status_stale). The message.status webhook event follows as for a real status. Without this call a simulated send walks through accepted, sent, delivered and read by itself; the last two digits of the recipient's phone choose another outcome (see the developer page).

Request body

application/json SandboxStatusRequest

  • message_id Id required
  • status string required
    • Allowed values: sent, delivered, read, failed
  • provider_code integer

    Meta's error code, only with failed (default 131026).

    • Minimum: 1

Responses

POST /v1/sandbox/identity-rotation

Staging simulator: the contact's WhatsApp user id changes

Authentication
API key (Authorization: Bearer)

Staging only. The contact's current BSUID on a simulated channel changes to new_bsuid; the contact.identity_changed webhook event follows with reason user_id_update (a live change has reason user_changed_user_id or, when the person changed their number, user_changed_number). A contact without a BSUID on a simulated channel gets 422 validation_failed (contact_id no_bsuid): simulate an inbound message with from.bsuid first.

Request body

application/json SandboxIdentityRotationRequest

  • contact_id Id required
  • new_bsuid string required
    • Maximum length: 140

Responses

POST /v1/sandbox/contacts/{id}/close-window

Staging simulator: close the 24-hour window

Authentication
API key (Authorization: Bearer)

Staging only. Moves the contact's last inbound message on simulated channels more than 24 hours back: a session message then gets 422 window_closed and only a template can be sent. No webhook event. Send an empty body or {}.

Parameters

  • id path Id required

Responses

POST /v1/sandbox/channels/{id}/health

Staging simulator: change the health of a simulated channel

Authentication
API key (Authorization: Bearer)

Staging only (added within v1). Writes the checklist of a simulated channel as a fixed scenario, so the channel object (health) changes and endpoints subscribed to channel.health_changed get the same event a real channel sends: green (every item green), amber (account_limited: Meta limits the account) or red (not_registered: the number is not connected). messaging_limit and quality are the event's messaging_limit and quality; they alone do not change the health, so they send no event. No real channel is touched and no e-mail goes out. The same status again gives outcome ignored with reason health_unchanged. A real channel gets 422 validation_failed (field channel_id, code not_simulated).

Parameters

  • id path Id required

Request body

application/json SandboxChannelHealthRequest

  • status string required
    • Allowed values: green, amber, red
  • messaging_limit string

    The messaging limit in the event (default TIER_1K).

    • Allowed values: TIER_250, TIER_1K, TIER_10K, TIER_100K, TIER_UNLIMITED
  • quality string

    The quality rating in the event (default GREEN).

    • Allowed values: GREEN, YELLOW, RED, UNKNOWN

Responses

POST /v1/sandbox/templates/{id}/status

Staging simulator: change the status of a simulated template

Authentication
API key (Authorization: Bearer)

Staging only (added within v1). Sends Meta's template status notice for a template of a simulated channel through the normal pipeline: the template's status changes (GET /templates shows it) and endpoints subscribed to template.updated get the same event a real template sends. status: APPROVED, PENDING, REJECTED (with an optional reason, NONE if left out), PAUSED or DISABLED. A template that is not APPROVED cannot be sent (422 template_not_approved); APPROVED makes it sendable again. A template of a real channel gets 422 validation_failed (field template_id, code not_simulated).

Parameters

  • id path Id required

Request body

application/json SandboxTemplateStatusRequest

  • status string required
    • Allowed values: APPROVED, PENDING, REJECTED, PAUSED, DISABLED
  • reason string

    Only with REJECTED; Meta's reason value, NONE if left out.

    • Allowed values: ABUSIVE_CONTENT, INCORRECT_CATEGORY, INVALID_FORMAT, NONE, PROMOTIONAL, SCAM, TAG_CONTENT_MISMATCH

Responses

GET /v1/sandbox/action-target

Staging simulator: a stand-in company API, read

Authentication
No key needed

Staging only, without a key. Not part of the API you call: UPPU Bot's actions call it on staging in place of a company's own API, to rehearse a read. Answers fixed sample data and ignores the query, apart from the options below. At most 120 requests a minute per client address (429 with {"error": "rate_limited"}). A delay is honoured only for UPPU's own address, one request at a time; other requests answer without it. The live API has no /sandbox paths.

Parameters

  • delay_ms query integer

    Wait this long before answering (the write is recorded first). Honoured only for a request from UPPU's own address (the published egress IP or UPPU's data-centre range, where UPPU Bot's actions come from), one request at a time and at most 10 a minute per address; any other request answers at once.

    • Minimum: 0
    • Maximum: 9000
  • status query integer

    Answer with this HTTP status instead of the default, to rehearse an action error path.

    • Minimum: 200
    • Maximum: 599

Responses

  • 200 Sample data; with status=300 or more, {"error": "forced_status"} and that status

    application/json object

    • slots string[]
    • checked_at string
      • Format: date-time
  • 429 {"error": "rate_limited"}

POST /v1/sandbox/action-target

Staging simulator: a stand-in company API, write

Authentication
No key needed

Staging only, without a key. UPPU Bot's write actions call it on staging to rehearse a write that happens at most once. The Idempotency-Key header is required (400 with {"error": "idempotency_key_required"} otherwise). The write is recorded before the delay, so a client that gives up or dies while waiting still leaves it recorded. The same key gets the same reference; receipts counts how often that key arrived. The request body is not stored.

Parameters

  • Idempotency-Key header string required
    • Pattern: ^[A-Za-z0-9:#_.-]{1,96}$
    • Maximum length: 96
  • delay_ms query integer

    Wait this long before answering (the write is recorded first). Honoured only for a request from UPPU's own address (the published egress IP or UPPU's data-centre range, where UPPU Bot's actions come from), one request at a time and at most 10 a minute per address; any other request answers at once.

    • Minimum: 0
    • Maximum: 9000
  • status query integer

    Answer with this HTTP status instead of the default, to rehearse an action error path.

    • Minimum: 200
    • Maximum: 599

Responses

  • 201 Recorded; with status=300 or more, the same fields plus {"error": "forced_status"} and that status

    application/json object

    • reference string
      • Pattern: ^R-[0-9A-F]{8}$
    • receipts integer
      • Minimum: 1
  • 400 {"error": "idempotency_key_required"}
  • 429 {"error": "rate_limited"}

GET /v1/sandbox/action-target/receipts

Staging simulator: what the stand-in company API received

Authentication
No key needed

Staging only, without a key. The last 100 idempotency keys of writes, newest first, with how often each arrived: a write that UPPU Bot sent once shows receipts 1. Kept for 24 hours; no personal data (a key is a bot turn's id and a step id).

Responses

  • 200 The receipts

    application/json object

    • data object[]

      Items

      • key string
      • receipts integer
      • first_at string | null
        • Format: date-time
      • last_at string | null
        • Format: date-time
  • 429 {"error": "rate_limited"}

OpenAPI document

GET /v1/openapi.json

This document

Authentication
No key needed

Responses

  • 200 The OpenAPI document of v1

    application/json object

Webhooks

Event types, payloads and delivery rules are on the webhooks page.

POST your webhook endpoint

An event UPPU sends to your webhook endpoint

One event per request, compact UTF-8 JSON, at most 256 KB, from the published egress IP. Verify the signature over the raw body: base64(HMAC-SHA256(base64decode(secret without "whsec_"), webhook-id + "." + webhook-timestamp + "." + body)), compared in constant time; refuse a timestamp more than 300 seconds away; drop repeats by webhook-id (keep ids for at least 35 days). During a secret rotation webhook-signature carries two space-separated v1 values. Answer 2xx within 10 seconds and process later. Any other answer is retried 12 times over about 76 hours; 410 suspends the endpoint. Delivery is at least once and FIFO per contact; message.status carries previous_status for reordering. message.status can carry status expired (added within v1; previous_status queued): the ttl of the send ran out before UPPU handed it to the provider, nothing was sent, error.code is ttl_expired. When an earlier attempt got an error answer from the provider, the ttl ends the send as failed with that error instead. Rank expired with accepted and unknown, below sent: in the rare case that the provider took the message after all (UPPU could not reach it and counted the attempt as not delivered), a later sent, failed, delivered or read still moves it forward. message.received carries service_window {open, closes_at}, counted from Meta's timestamp of the message: a message Meta delivered late (received_at far after sent_at) can arrive with open false; on WhatsApp answer it with a template. On Messenger and Instagram the person's reaction opens the window as a message does (added within v1). On web chat, Telegram and e-mail service_window is null (no reply window). contact.reachability_changed (added within v1, Telegram) carries {contact_id, channel_id, identity: {type, value}, reachable, reason, occurred_at}; reason is blocked_by_contact, send_forbidden (both reachable false), unblocked_by_contact or contact_wrote (both reachable true). channel.status_changed carries {channel_id, status, previous_status, mode, previous_mode, reason}; when Meta tells UPPU that a WhatsApp Business account left UPPU, status is disconnected and reason is partner_removed, account_deleted or account_offboarded (the last two added within v1). Accept unknown reasons. conversation.updated (produced within v1; subscribe to it explicitly, new endpoints leave it off) carries ConversationUpdate: the conversation as GET /conversations/{id} returns it, change (handed_over, returned_to_bot, released, resolved, reopened, assigned, unassigned), reason, actor {type, api_key_prefix} and occurred_at. It is written only while an endpoint of the company subscribes to it, so GET /events has it only then too. Accept unknown changes, reasons and actor types. contact.updated (added within v1; subscribe to it explicitly, new endpoints leave it off) carries ContactUpdate: the contact as GET /contacts/{id} returns it to a key for all channels, changes (the names of the fields that changed: display_name, identities, merged_into, notes, attributes), reason and occurred_at; the envelope's channel is null. One change sends one event, a merge and its undo one for each of the two contacts; a change of the team's notes alone sends none. The contact carries its identities on all channels, so an endpoint with a channel filter gets it only when all of the contact's identities and conversations are on that channel. It is written only while an endpoint of the company subscribes to it. UPPU switches it on per environment; until then it can be subscribed to and nothing is sent. The opening of a contact is not a change (message.received carries it). Accept unknown changes and reasons. account.service_stop_scheduled and account.service_resumed (added within v1; on by default for new endpoints, which can leave them out of their event list) go to every subscribed endpoint of the company whatever its channel filter; the envelope's channel is null, so GET /events lists them only for an all-channels key. account.service_stop_scheduled carries {stop_at, reason, occurred_at} (reason billing_overdue or subscription_ended): the service stops at stop_at unless the payment is recorded first. If the warning reached nobody and goes again, a second account.service_stop_scheduled names a later stop_at: the instant only moves later, and UPPU-Service-Stop-At always carries the current one. account.service_resumed carries {stopped_at, resumed_at, occurred_at}. A stopped company produces no other event; its due deliveries end dead with last_error service_stopped and are not sent by themselves after the resume (resend them). An endpoint can opt in to a weekly test event (weekly_test; added within v1): UPPU then sends webhook.test, test true, once a week. A failed weekly test is only a warning on the endpoint (weekly_test.failed_at); the endpoint is never suspended for it.

Parameters

  • webhook-id header Id required

    The event id; the same across retries, resends and endpoints.

  • webhook-timestamp header string required

    Unix seconds, new on every attempt.

  • webhook-signature header string required

    One or two values "v1,<base64>" separated by a space.

  • Uppu-Event-Type header string required
  • Uppu-Delivery-Id header Id required

    The delivery to this endpoint (a resend keeps it).

  • Uppu-Attempt header string required

Request body

application/json Event

  • id Id required
  • object "event" required
  • type string required
    • Allowed values: message.received, message.status, message.created, contact.identity_changed, contact.consent_changed, contact.erased, template.updated, channel.status_changed, channel.health_changed, conversation.updated, contact.reachability_changed, contact.updated, account.service_stop_scheduled, account.service_resumed, webhook.test
  • api_version "v1" required
  • created_at Timestamp required
  • team_id Id required
  • channel object | null required
    • id Id required
    • type string required
  • test boolean required

    True only for test events (the webhook page's and the API's test event and probe, and the optional weekly test event).

  • data object required

Responses

  • 2XX Received; UPPU does not send this event to the endpoint again.

Schemas

Id

string

  • Pattern: ^[0-9a-hjkmnp-tv-z]{26}$

Timestamp

string

  • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$

NullableTimestamp

string | null

  • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$

ChannelList

object

  • object "list" required
  • data Channel[] required
  • next_cursor string | null required

Channel

object

  • object "channel" required
  • id Id required
  • type string required
    • Allowed values: whatsapp, instagram, messenger, telegram, web, email
  • name string required
  • display_phone string | null required

    E.164 with a plus sign; null for web channels.

  • handle string | null required

    How people find the channel; on Telegram the bot's @username, on e-mail the sending address (null until it is set), null on other channel types (added within v1).

  • contact_url string | null required

    A link that opens a chat with the channel; on Telegram https://t.me/<bot username>, to which ?start=<parameter> may be appended (1-64 of A-Z a-z 0-9 _ -; it arrives as context.start_parameter). On e-mail mailto:<sending address> (null until it is set). Null on other channel types (added within v1).

  • mode string required
    • Allowed values: live, shadow
  • status string required
    • Allowed values: pending, active, paused, disconnected
  • handling string required

    Who answers on the channel. inbox is the team inbox, api your own system, bot UPPU Bot. This is the choice the company owner stored: it stays the same when the company's plan changes. While UPPU Bot is not in the plan, a bot channel runs as a team inbox channel. In a company whose plan has no messaging panel nobody answers in UPPU: inbox and bot channels get no assignment and resolve every idle open conversation after 24 hours.

    • Allowed values: inbox, api, bot
  • bot object | null required

    The UPPU Bot that answers on a channel whose handling is bot: its display name, which is also the author.label of its messages. Null on other channels (added within v1).

    • name string required
  • sending string required
    • Allowed values: active, paused, unavailable
  • sending_reason string | null required

    Why sending is unavailable; the same codes as details.reason of channel_unavailable. team_disabled is retired within v1 and no longer produced: a company's plan never stops the sends of a connected channel (a channel type removed from the plan keeps working); the value stays listed for older clients. A company's Telegram gate likewise stops new connections only, never the sends of a connected bot. E-mail channels (added within v1): email_off (UPPU's e-mail channel is switched off), no_sender (no sending address is set) and sender_unverified (the domain of the sending address is not verified in the panel: its DKIM record was not found).

    • Allowed values: shadow_mode, meta_off, telegram_off, team_disabled, not_linked, not_active, no_token, not_supported, email_off, no_sender, sender_unverified, null
  • health ChannelHealth | null required
  • messaging_limit string | null required

    Meta's messaging limit tier (WhatsApp only).

  • quality string | null required

    Meta's quality rating (WhatsApp only).

  • capabilities ChannelCapabilities required
  • created_at Timestamp required
  • email object

    E-mail only, present only on e-mail channels (added within v1). inbound_address is the UPPU address the company forwards its support address to (null while UPPU's e-mail channel is switched off); from_address the sending address of the replies; sender_verified whether its domain is verified (its DKIM record found), without which nothing is sent.

    • inbound_address string | null required
    • from_address string | null required
    • sender_verified boolean required
  • foreign_apps ChannelForeignApps | null

    WhatsApp only, present only on WhatsApp channels (added within v1): the Meta apps other than UPPU's that are subscribed to the channel's WhatsApp Business Account, read with the channel checklist (every 15 minutes) from Meta's subscribed_apps list. Customers do not call Meta directly: an app listed here still receives this account's webhooks from Meta and can act on the account outside UPPU. Remove it from the account in Meta Business Settings and revoke its tokens on a date agreed with UPPU: for a number moved to UPPU this is the revocation step, 7 stable days after the move, and until then red is expected. UPPU only reads the list and never changes it. Not part of health. Null until the list has been read completely.

ChannelForeignApps

object

  • status string required

    red: at least one other app was subscribed at the last read; green: only UPPU's app, read within the last hour; unknown: no other app at the last read, but that read is older than an hour or UPPU's own app was not subscribed either (the channel's health then reports app_not_subscribed); not_applicable: UPPU does not serve the number in live mode (shadow mode, disconnected), so the account may still use other apps.

    • Allowed values: green, red, unknown, not_applicable
  • apps object[] required

    The other apps at the last read, by Meta's app id; name is the app's name on Meta.

    Items

    • id string required
    • name string | null required
  • checked_at Timestamp required

    The last successful read.

  • changed_at NullableTimestamp required

    When the set of other apps last changed. Not the status: the status also follows the age of the read, UPPU's own subscription and the channel's mode.

ChannelHealth

object

  • status string required
    • Allowed values: green, amber, red, unknown
  • reasons string[] required

    Reason codes of the checklist items that are not green. Telegram channels (added within v1) have items 1, 7, 8 and 9 only, with their own codes (for example webhook_elsewhere, webhook_missing, webhook_errors, webhook_backlog, token_rejected, sending_paused, telegram_unreachable); unknown codes are to be accepted. On WhatsApp, item 5 (payment method) is red with funding_missing (Meta shows no payment method), billing_fields_missing (the account's currency or time zone is empty), payment_error_131042 (Meta refused a message for a payment reason in the last 7 days) or payment_error_141006 (Meta reported a payment account problem on a message in the last 7 days, or reports it in the account's health status); on item 4, verification_unreadable (Meta does not show the field) is a grey code added within v1.

  • checked_at NullableTimestamp required

ChannelCapabilities

object

  • content_types string[] required
  • interactive_types string[] required

    The interactive types the channel sends (added within v1); empty when interactive is not a content type. cta_url (WhatsApp, Messenger) and flow (WhatsApp, once WhatsApp Flows are switched on for UPPU) were added within v1.

  • limits map<integer> required

    Per-field limits. Keys ending in _bytes count UTF-8 bytes (Instagram text_body_bytes), keys ending in _utf16 count UTF-16 code units, as JavaScript's String.length does (Telegram text_body_utf16 and caption_utf16); the other keys count characters, or items for counts such as buttons and quick_replies. The values are UPPU's and may change. Where the provider's documentation states a limit, UPPU's is the same or lower, except that Instagram's documentation also asks for a text shorter than 1,000 characters. Where it states none (for example Messenger's text_body, WhatsApp's template parameters and location fields, file names, Instagram's option_id and most Telegram keys), the value is UPPU's choice and the provider may still refuse a value within it. On WhatsApp interactive_body limits the body of a buttons message and list_body that of a list; button_id limits a reply button's id and option_id a list row's. list_body and button_id were added within v1; until then interactive_body and option_id covered both kinds, so a client that validates with them still sends valid messages. Contact cards (added within v1): contact_cards per message, contact_phones and contact_emails per card, contact_field for every text field of a card (UPPU's values; Telegram takes one card with one phone). Template buttons (added within v1, WhatsApp): template_button_url limits the whole address of a URL button once its variable is filled in, template_button_payload a quick reply's payload (UPPU's values). Link buttons and flows (added within v1): cta_display_text limits a cta_url button's text (the providers' 20), cta_url its address (UPPU's value); flow_cta a flow button's text (WhatsApp advises 30 without emoji; UPPU enforces it), flow_token, flow_screen and flow_data_bytes the flow's token, first screen and the JSON of its first screen's data (UPPU's values).

  • window_hours integer | null required
  • phone_addressing boolean required
  • reaction_emojis string[] | null required

    The only emojis a reaction may carry (Instagram the heart; Telegram its fixed ReactionTypeEmoji list, matched without the variation selector U+FE0F); null when any single emoji is accepted or the channel sends no reactions (added within v1).

  • delivery_statuses string[] required

    The statuses the channel reports after queued and accepted (added within v1): WhatsApp sent, delivered and read; Messenger delivered and read; Instagram read; Telegram, web chat and e-mail sent. Do not wait for a status the channel never reports.

    • Allowed values: sent, delivered, read
  • typing_indicator boolean required

    Whether POST /conversations/{id}/typing works on the channel (added within v1): WhatsApp, Messenger and Telegram.

Message

object

A message of either direction. The API and the webhooks never mask text: the company's own system gets the text as written. When the platform has switched on the company's special-category data protection, a recognized Turkish national ID number in a message older than the short retention (90 days by default, at least 30) is masked in storage for good (***46). The mask covers preview, the text fields of content (template variables included) and the names of the message's files (media.filename and attachments[].media.filename here, filename in GET /media/{id}); ids and context stay. One exception: a file uploaded with POST /media (or in the inbox) and sent in an outbound message keeps its own row, which is not tied to the message, because the upload can be reused in later sends. The short retention does not mask that row: media.filename here and filename in GET /media/{id} keep the uploaded name until the row is deleted on its own schedule (the file 90 days after its last use, the row 30 days later). The content's own filename is masked. A closed company's messages are masked the same way until its data is deleted.

  • object "message" required
  • id Id required
  • channel_id Id required
  • channel_type string required
  • conversation_id Id required
  • contact_id Id required
  • direction string required
    • Allowed values: in, out
  • author MessageAuthor required
  • content MessageContent required
  • context MessageContext | null required
  • template MessageTemplate | null required
  • preview string required
  • status string required

    Outbound: queued, accepted, sent, delivered, read, failed, unknown, or (added within v1) expired: the ttl ran out before UPPU handed the message to the provider, nothing was sent, error.code ttl_expired. Inbound: received. Accept values you do not know.

    • Allowed values: queued, accepted, sent, delivered, read, failed, unknown, expired, received
  • status_changed_at NullableTimestamp required
  • error DeliveryError | null required
  • client_reference string | null required
    • Maximum length: 64
  • provider_message_id string | null required
  • sent_at NullableTimestamp required

    Inbound: the provider's timestamp of the message (Meta's; on Telegram its date, in seconds; the receive time when the provider sent none). Outbound: when the provider accepted it (Meta accepted it, Telegram stored it; web chat: when it was written).

  • received_at NullableTimestamp required

    Inbound: when the message reached UPPU; null outbound. On channels with a reply window (WhatsApp, Messenger, Instagram) the window counts from the earlier of sent_at and received_at: a received_at far after sent_at marks a message Meta delivered late, whose window may already be closed. Telegram and web chat have no reply window.

  • created_at Timestamp required

MessageAuthor

object

  • type string required

    contact, agent (the team), system (UPPU), api (your API key), bot (UPPU Bot; label is the bot's display name) or, added within v1, campaign (a campaign of the company's panel; label is the campaign's name).

    • Allowed values: contact, agent, system, api, bot, campaign
  • label string | null required
  • api_key_prefix string | null required

MessageContent

object

{"type": T, T: {...}}. Types: text, image, video, audio, document, sticker, location, contacts, interactive, interactive_reply, template, reaction, unsupported, system and, added within v1, flow_reply. Media of both directions carries media: {id, mime_type, size, sha256, filename, state, failure_reason}: id is UPPU's media id (GET /media/{id}/content), size and sha256 stay null until the file is stored (state pending -> ready). A media object whose id is null belongs to an old inbound message stored before UPPU kept its files (WhatsApp before F3-14, Messenger and Instagram before their media support): it stays pending, never becomes ready and cannot be downloaded, so do not poll for it. Messenger and Instagram (added within v1): a message's type is the type of its first media attachment, whatever attachment comes before it (a link preview, a location, a template, a shared post, a reel or a story mention followed by a photo is an image message). A message with several media attachments carries attachments: [{type, media}] next to media, every media attachment (image, video, audio, document, sticker) in Meta's order (the first is the same file as media; the key is absent when there is at most one media attachment). Other attachment types (location, link previews, templates, shared posts, reels, story mentions) are not listed and have no file; the preview text counts every attachment. A message without a media attachment takes the type of its first attachment: a shared post or reel (unsupported) carries the preview's file as unsupported.media when Meta gave one; an Instagram story mention has no file (Meta does not allow storing story content). On Messenger and Instagram a caption reaches the person as a separate text message after the file. Meta's attachment links are never exposed. Telegram (added within v1): a message carries at most one file (every album item is its own message; an animated sticker's file is its thumbnail), which UPPU downloads; Telegram's file ids are never exposed. Web chat (added within v1): a visitor's message carries at most one file (image, video, audio or document, with an optional caption) with the same media object; it is stored as it arrives. UPPU Bot on web chat (added within v1): a question with options is interactive (quick_replies whose ids are the bot's option ids) and the visitor's tap is interactive_reply (kind quick_reply, the option's id and title; context.message_id is the question). POST /messages still sends no interactive content to web chat. Stickers and contact cards you send (added within v1) read back with the shapes of received ones: sticker with the upload's media object and animated, contacts with the cards (phones carry bsuid null). A template or buttons message sent with a file header (added within v1) carries header: {type, T: {media, filename?}} in template or interactive, with the upload's media object; the template's variables and button values are not echoed. Link buttons and WhatsApp Flows (added within v1): a sent interactive of type cta_url or flow reads back as sent (a flow with its flow_token, generated when you gave none, and flow_action navigate). A person's completed WhatsApp Flow is the inbound type flow_reply: {flow_token, flow_id, response, response_omitted}. flow_token is the token of the flow message (null when Meta sent none); flow_id is the flow's id when the person answered a flow message sent through UPPU (else null); response is the form's data as an object, as the flow defines it, without flow_token. Text values have invisible control characters removed. A file field (a photo or document picker) keeps its file_name, mime_type and sha256; UPPU does not download those files and never exposes the provider's media id. A response larger than UPPU keeps (32 KiB of JSON, 8 levels, 500 keys and items) is not stored: response is null and response_omitted says why (too_large or too_deep). A person's completed flow arrives as unsupported (provider_type nfm_reply) while WhatsApp Flows are switched off for UPPU. E-mail (added within v1): a received e-mail is text: body is its plain text (an HTML e-mail is turned into text; the quoted earlier messages of a reply are cut, the full e-mail is not kept in the message), email is {subject, from: {address, name}, reply_to, authenticated, in_reply_to, references, trimmed, truncated, html, attachment_count, attachments_omitted} (authenticated: UPPU's mail server found a valid DKIM signature of the domain of the person's address, the reply_to when given, else from; false means the address may be forged; trimmed: a quote was cut; truncated: the text was longer than the message keeps; html: the text came from HTML; attachments_omitted: attachments over UPPU's limit that were not kept) and attachments: [{type, media}] lists its files in order (absent when it has none). provider_message_id is the e-mail's Message-ID without angle brackets (UPPU's own Message-ID on an e-mail you sent).

  • type string required

MessageContext

object

  • message_id string | null required
  • provider_message_id string | null required
  • forwarded boolean required
  • frequently_forwarded boolean required
  • referral object | null required
  • story object

    Present only on a reply to an Instagram story: the story's id. UPPU does not keep the story's file or its address (Meta does not allow storing story content; a story expires after 24 hours).

    • id string | null required
  • start_parameter string

    Telegram only, present only when the person opened the bot with a deep link (https://t.me/<bot>?start=<parameter>, delivered as "/start <parameter>"): the parameter, 1-64 of A-Z a-z 0-9 _ -. The message itself stays a text "/start <parameter>".

  • media_group_id string

    Telegram only, present only on an item of an album: the album's id. Every item of an album is its own message with its own media; the caption sits on one of them.

MessageTemplate

object

  • id string | null required
  • name string | null required
  • language string | null required
  • category string | null required

DeliveryError

object

  • code string required
    • Allowed values: window_closed, payment_required, marketing_opt_out, not_registered, recipient_unreachable, provider_rate_limited, provider_unavailable, credentials_invalid, permission_denied, account_restricted, invalid_parameter, channel_unavailable, channel_paused, no_recipient, template_unavailable, media_fetch_failed, media_upload_failed, media_rejected, delivery_unverified, ttl_expired, service_stopped, provider_error
  • message string required
  • retryable boolean required
  • provider_error ProviderError | null required

ProviderError

object

  • provider string required

    meta, or telegram (added within v1; code is Telegram's error_code, title its description), or email (added within v1; code is the SMTP reply code of UPPU's mail server, title its masked reply).

    • Allowed values: meta, telegram, email
  • code integer required
  • subcode integer | null required
  • title string | null required
  • details string | null required

ErrorEnvelope

object

Error

object

  • type string required
    • Allowed values: authentication, permission, invalid_request, conflict, policy, rate_limit, server
  • code string required
    • Allowed values: invalid_json, invalid_request, idempotency_key_required, idempotency_key_invalid, missing_api_key, invalid_api_key, wrong_environment, insufficient_scope, channel_not_allowed, ip_not_allowed, browser_request_refused, api_disabled, tenant_read_only, service_stopped, shadow_mode, not_found, method_not_allowed, idempotency_key_reused, channel_paused, channel_unavailable, tenant_not_ready, plan_limit_reached, media_not_ready, not_held, not_handed_over, bot_not_ready, media_expired, payload_too_large, media_too_large, unsupported_media_type, validation_failed, window_closed, consent_required, consent_withdrawn, contact_blocked, recipient_unreachable, template_not_found, template_not_approved, template_parameters_invalid, template_category_not_allowed, recipient_invalid, recipient_not_found, reply_target_invalid, content_not_supported_on_channel, not_inbound, media_unavailable, handling_not_supported, rate_limited, channel_backlog, internal_error, service_unavailable, provider_unavailable
  • status integer required
  • retryable boolean required
  • message string required

    Follows Accept-Language (tr by default, en).

  • request_id string required
    • Maximum length: 64
  • doc_url string | null required
  • details object required

    For example resource (not_found; api_disabled: team, api_key or channel), reason (api_disabled with resource team: not_in_plan, the company's plan does not include the API), required (insufficient_scope), fields (validation_failed: [{field, code, message}]). Added within v1: handling (handling_not_supported: the channel's handling) and reason (handling_not_supported: not_in_plan, the company's plan has no messaging panel; not_held and not_handed_over: conversation_resolved; bot_not_ready: bot_unavailable, bot_busy or reply_first; not_inbound: no_inbound_message for a typing indicator on WhatsApp), scope (rate_limited: conversation, the per-conversation limit of typing indicators, with limit and period_seconds). Added within v1 too: resource, max and used (plan_limit_reached: webhook_endpoint, the limit and the endpoints the company has) and reason (not_found with resource media_link: invalid or expired). Added within v1 with template management and reports: resource reports with reason not_in_plan (api_disabled: the plan has no messaging panel, reports are part of it), resource member and group (not_found), reason templates_per_hour, concurrent_writes and provider_rate_limited (rate_limited; provider_rate_limited: Meta's own rate limit, with Meta's code in provider_error), reason credentials also when Meta refused the access token (channel_unavailable) and reason unreachable or unconfirmed (provider_unavailable: Meta could not be reached, or did not confirm the change because it did not answer in time or had a temporary error, so the change may have been made).

  • provider_error ProviderError | null required

MessageList

object

  • object "list" required
  • data Message[] required
  • next_cursor string | null required

SendMessageRequest

object

  • channel_id Id required
  • to Recipient required
  • context object

    Reply to a message of the same channel and contact that has a provider id (WhatsApp; Telegram, a message of the contact's chat; e-mail, added within v1: a received e-mail of the contact, whose thread the reply joins).

    • message_id Id required
  • content OutboundContent required
  • client_reference string

    Your opaque reference (no personal data); echoed in GET and in every message.status event.

    • Minimum length: 1
    • Maximum length: 64
  • ttl integer

    Added within v1, accepted once UPPU switches it on (until then 422 validation_failed, code unknown_field). The message's time to live in whole seconds. If UPPU has not handed the message to the provider by created_at + ttl, it is not sent: status expired, error.code ttl_expired (status failed with the provider's error when an earlier attempt got an error answer from the provider). Out of range or not a whole number: 422 validation_failed, field ttl, code between.

    • Minimum: 10
    • Maximum: 86400

Recipient

One of

Exactly one of contact_id, phone (E.164 with a plus sign, WhatsApp only) or an identity that already exists on the channel (telegram: the person's Telegram user id as a decimal string; the person must have written to the bot first; email, added within v1: the address the person wrote from, case-insensitive).

One of

  1. object
    • contact_id Id required
  2. object
    • phone string required
      • Pattern: ^\+[1-9][0-9]{4,19}$
  3. object
    • identity object required
      • type string required
        • Allowed values: bsuid, psid, igsid, web_visitor, telegram, email
      • value string required
        • Maximum length: 255

OutboundContent

One of

{"type": T, T: {...}}. Web chat channels take text and, added within v1, media by media_id only (see MediaContent). Limits come from the channel's capabilities (GET /channels; ChannelCapabilities.limits says how they relate to the providers' own limits). Media (image, video, audio, document) takes exactly one of media_id (POST /media) or an https link that UPPU downloads before sending. Added within v1: sticker (StickerContent) and contacts (ContactsContent) on WhatsApp and Telegram; other channels get 422 content_not_supported_on_channel. Telegram takes text (1-4096 UTF-16 units, plain text), interactive buttons and quick_replies (no header, footer or list), reaction (an emoji of capabilities.reaction_emojis; an empty string removes it), location (name and address both or neither: a venue) and, added within v1, media (image, video, audio, document; see MediaContent), a static sticker and one contact card with one phone. Added within v1: interactive cta_url (a link button) on WhatsApp and Messenger and interactive flow (a WhatsApp Flow) on WhatsApp; see InteractiveContent.

TextContent

object

  • type "text" required
  • text object required
    • body string required
      • Minimum length: 1
    • preview_url boolean

InteractiveContent

object

  • type "interactive" required
  • interactive object required
    • type string required

      buttons and list on WhatsApp; buttons and quick_replies on Messenger and Telegram; quick_replies on Instagram (see the channel's capabilities.interactive_types). quick_replies has the shape of buttons. On Telegram buttons are an inline keyboard (a tap arrives as interactive_reply, kind button) and quick_replies a one-time reply keyboard whose titles must be unique (a tap arrives as interactive_reply, kind quick_reply). Added within v1: cta_url, one button that opens an https address (WhatsApp and Messenger; see cta_url), and flow, a WhatsApp Flow published in the WhatsApp account (WhatsApp only, when the channel lists it in capabilities.interactive_types; see flow). A type the channel does not list gets 422 content_not_supported_on_channel (details.field content.interactive.type).

      • Allowed values: buttons, list, quick_replies, cta_url, flow
    • header One of

      A text header, or (added within v1) on a WhatsApp buttons or cta_url message a file header: an image, a video or a document given by the media_id of an upload of the same channel (POST /media; a link is not accepted), with the MIME types and sizes of the media type. A list or flow message takes only a text header. Messenger, Instagram and Telegram take no header: 422 content_not_supported_on_channel (details.field content.interactive.header).

      One of

      1. object
        • type "text" required
        • text string required
      2. MediaHeader
    • body object required
      • text string required
    • footer object
      • text string required
    • Buttons and quick replies messages only.

    • button string

      List message only; the label of the list button.

    • sections object[]

      List message only.

      Items

    • cta_url object

      cta_url message only (added within v1): one button that opens an https address in the person's browser. display_text is the button's text (capabilities.limits.cta_display_text: 20 on WhatsApp and Messenger). url is an https address made of the characters of a web address (letters, digits, -._~:/?#[]@!$&'()*+,;= and percent-encoding: encode a space as %20 and Turkish letters too), at most capabilities.limits.cta_url characters (the providers give no limit; this is UPPU's). UPPU sends the address as written: no shortener, no tracking. The body is required (WhatsApp interactive_body, Messenger button_text); WhatsApp also takes a header (text or file) and a footer, Messenger neither. Instagram, Telegram and web chat do not send it (422 content_not_supported_on_channel). A tap opens the address and sends nothing back.

      • display_text string required
      • url string required
    • flow object

      flow message only (added within v1; WhatsApp, when capabilities.interactive_types lists flow): opens a WhatsApp Flow that the company created and published in its WhatsApp account (Flow Builder in WhatsApp Manager); UPPU does not create or publish flows. Only navigate flows: data_exchange needs the company's own endpoint and encryption, which UPPU does not provide (422 validation_failed, content.interactive.flow.flow_action not_supported). flow_id is the flow's numeric id; flow_cta the button's text (at most capabilities.limits.flow_cta characters, without emoji). flow_token is yours (visible ASCII without spaces, at most capabilities.limits.flow_token characters); when you leave it out UPPU generates one, and the message object shows it. The person's completed form comes back on message.received as content type flow_reply with the same flow_token. flow_action_payload is optional: screen, the id of the entry screen to open (letters, digits and underscores, at most capabilities.limits.flow_screen; the flow's first screen when left out), and data, a non-empty object for that screen (needs screen; its JSON at most capabilities.limits.flow_data_bytes bytes and at most 5 levels deep). The body is required; a header takes only text. The message object echoes the flow with flow_action navigate.

      • flow_id string required
        • Pattern: ^[0-9]{1,32}$
      • flow_cta string required
      • flow_token string
      • flow_action string
        • Allowed values: navigate
      • flow_action_payload object
        • screen string
          • Pattern: ^[A-Za-z0-9_]+$
        • data object

InteractiveOption

object

  • id string required
  • title string required
  • description string

    List rows only.

ReactionContent

object

  • type "reaction" required
  • reaction object required
    • message_id Id required
    • emoji string required

      An empty string removes the reaction (UNVERIFIED).

LocationContent

object

  • type "location" required
  • location object required
    • latitude number required
      • Minimum: -90
      • Maximum: 90
    • longitude number required
      • Minimum: -180
      • Maximum: 180
    • name string
    • address string

MediaContent

object

An image, video, audio or document: {"type": "document", "document": {"media_id": "…", "filename": "…", "caption": "…"}}. Give exactly one of media_id (an upload of the same channel, within reusable_until) or link (https on port 443, public addresses only, no redirects, at most the type's size, 15 seconds; a failure ends as failed with media_fetch_failed). Audio has no caption; only a document has a filename. The type's MIME types and sizes are those of POST /media for the channel's type. WhatsApp, Messenger, Instagram and Telegram channels send media (see capabilities.content_types; Instagram only while UPPU's Instagram outbound media is on, otherwise 422 content_not_supported_on_channel); Instagram has no document (422 content_not_supported_on_channel). Web chat (added within v1) takes media_id only: a link is 422 content_not_supported_on_channel (details.field content.{type}.link); the message is sent at once and the visitor opens the file in the chat widget through a short-lived link. On Messenger and Instagram the caption is sent as a separate text message right after the file (it has the channel's text limit: Messenger 2000 characters, Instagram 1000 bytes), and Meta shows a document under the name it was uploaded with (the upload's file name, or filename with a link); one message, one status. If the file was accepted but the caption could not be sent, the message stays accepted. On Telegram (added within v1) the file is uploaded inside the send call and the caption is Telegram's own (at most 1024 UTF-16 units, 422 validation_failed beyond); an audio/ogg file goes out as a voice note. Later sends of the same upload reuse Telegram's file id (a document sent under another filename is uploaded again). A file Telegram refuses ends as failed with media_rejected.

MediaSource

object

  • media_id Id
  • link string
    • Pattern: ^https://
    • Maximum length: 2048
  • caption string
  • filename string
    • Maximum length: 255

StickerContent

object

Added within v1, on WhatsApp and Telegram: {"type": "sticker", "sticker": {"media_id": "…"}}. media_id is an upload of the same channel within reusable_until, made with POST /media and kind sticker (on Telegram also a WebP uploaded without kind): a WebP of exactly 512×512 pixels, static up to 100 KB or animated up to 500 KB; Telegram sends static stickers only. A link is 422 validation_failed (code not_supported); a sticker has no caption. A file that breaks the rules gets 413 media_too_large or 422 validation_failed for content.sticker.media_id (sticker_dimensions, sticker_unreadable, sticker_animated_not_allowed, mime_not_allowed), checked in the order POST /media gives: the 413's details carry kind sticker, the max_bytes of the file's own kind (static or animated, read from the header) and animated. The message reads back as content sticker with the media object and animated.

  • type "sticker" required
  • sticker object required
    • media_id Id required

ContactsContent

object

Added within v1, on WhatsApp and Telegram: {"type": "contacts", "contacts": {"contacts": [card, …]}}, the shape a received contact card has. At least one card; at most capabilities.limits.contact_cards cards (Telegram: one). Every card needs name.formatted_name and at least one phone number or e-mail address (Telegram: exactly one phone; e-mail addresses and org are 422 content_not_supported_on_channel). A phone number has 3 to 20 digits and may carry a plus sign, spaces, hyphens, dots and brackets; an e-mail address must be valid. Each text field has at most capabilities.limits.contact_field characters. On WhatsApp a name with formatted_name only is sent with first_name set to it. A contact card is personal data of the person it describes: it is kept, exported and erased with the message.

  • type "contacts" required
  • contacts object required

ContactCard

object

  • name object required
    • formatted_name string required

      The name as it is shown.

      • Minimum length: 1
    • first_name string
    • last_name string
    • middle_name string
    • prefix string
    • suffix string
  • phones object[]

    Items

    • phone string required
    • type string

      For example CELL, MAIN, IPHONE, HOME or WORK.

  • emails object[]

    Items

    • email string required
    • type string

      For example HOME or WORK.

  • org object
    • company string
    • department string
    • title string

Media

object

  • object "media" required
  • id Id required
  • channel_id string | null required
  • direction string required
    • Allowed values: in, out
  • kind string required
    • Allowed values: image, video, audio, document, sticker
  • mime_type string | null required
  • size integer | null required

    Bytes; null until the file is stored.

  • sha256 string | null required

    The hex SHA-256 of the stored file; null until it is stored.

  • filename string | null required
  • state string required
    • Allowed values: pending, ready, failed, expired
  • failure_reason string | null required

    Set when state is failed: too_large, mime_not_allowed, quota_exceeded, empty, no_provider_id, no_token, provider_refused, provider_expired, download_failed, link_refused, link_failed, upload_failed, channel_unavailable.

  • reusable_until NullableTimestamp required

    Uploads only; the last moment the file can be sent with media_id.

  • created_at Timestamp required

MessageMedia

object

The media object inside a message (content.{type}.media, content.{type}.attachments[].media, content.unsupported.media): UPPU's media id (GET /media/{id}/content) and the file's state. id is null only on old inbound messages stored before UPPU kept their files (WhatsApp before F3-14, Messenger and Instagram before their media support); such an object stays pending, never becomes ready and cannot be downloaded, so do not poll for it.

  • id string | null required
  • mime_type string | null required
  • size integer | null required
  • sha256 string | null required
  • filename string | null required
  • state string required
    • Allowed values: pending, ready, failed, expired
  • failure_reason string | null required

TemplateContent

object

  • type "template" required
  • template object required

    Give id, or name and language (a name and language with several rows resolves to the single APPROVED one). POSITIONAL templates take a list ({{1}} first), NAMED templates an object of name and value. Values are text only. A text header takes its variables; a template whose header is an image, a video or a document (added within v1) takes the file as {"type": "image", "image": {"media_id": "…"}}, the upload of the same channel (POST /media, no link; an image JPEG or PNG up to 5 MB, a video MP4 or 3GPP up to 16 MB, a document PDF only, up to 25 MB), and a document may carry the filename the person sees. Buttons (added within v1): give a value only for a button that takes one, by its position in the template (index, from 0): a URL button whose address ends in a variable needs its parameter (the end of the address: characters of a web address, others percent-encoded; with the template's address at most capabilities.limits.template_button_url characters), a quick reply may carry a payload (at most capabilities.limits.template_button_payload characters) that comes back in the person's tap. Buttons without a variable (a fixed address, a phone number, a quick reply without payload) take no value: they come with the approved template. GET /templates lists what a template needs (parameters.header_media, parameters.buttons). A value that does not fit the template is 422 template_parameters_invalid with the field paths (content.template.header, content.template.buttons.0.parameter); a file that cannot be used is 422 media_unavailable, 422 validation_failed (mime_not_allowed) or 413 media_too_large for content.template.header.<type>.media_id.

MediaHeader

One of

A file header (added within v1): the media_id of an upload of the same channel made with POST /media; only a document takes a filename.

One of

  1. object
  2. object
  3. object
    • type "document" required
    • document object required
      • media_id Id required
      • filename string
        • Maximum length: 255

HeaderFile

object

  • media_id Id required

TemplateButtonValue

One of

The value of a template button (added within v1), by the button's position in the template (index, 0 to 9).

One of

  1. object
    • index integer required
      • Minimum: 0
      • Maximum: 9
    • type "url" required
    • parameter string required

      The end of the address that replaces the URL's variable.

      • Minimum length: 1
  2. object
    • index integer required
      • Minimum: 0
      • Maximum: 9
    • type "quick_reply" required
    • payload string required

      Comes back as the id of the person's interactive_reply (kind button, source template).

      • Minimum length: 1

TemplateValues

Any of

Any of

  1. string[]
  2. map<string>

InlineConsent

object

Templates only; needs consents:write. Recorded before the policy check, exactly as a consent of its own. The same granted consent sent again (same fields, any Idempotency-Key) is recorded once and keeps its first time, so a later withdrawal of the person (for example STOP) still wins. A grant without collected_at is not recorded while the latest record is a withdrawal: send the time the new consent was collected.

  • purpose string required
    • Allowed values: marketing, utility
  • state string required
    • Allowed values: granted, withdrawn
  • source_text string required

    The exact text the person agreed to.

    • Maximum length: 2000
  • source_ref string required

    Your reference to the evidence (no personal data), for example web-form:checkout:7781.

    • Maximum length: 191
  • collected_at string

    When the person gave or withdrew the consent; now if left out. A grant overrides an earlier withdrawal only with a collected_at after that withdrawal.

    • Format: date-time

ReadReceipt

object

  • object "read_receipt" required
  • message_id Id required
  • state string required
    • Allowed values: queued, accepted, failed, unknown

TypingIndicator

object

Added within v1. The answer of POST /conversations/{id}/typing; nothing is stored.

  • object "typing_indicator" required
  • conversation_id Id required
  • message_id string | null required

    WhatsApp only, the contact's message the indicator marks as read; null on other channels.

  • state string required

    Always queued; the outcome is not reported.

    • Allowed values: queued

ContactLookupRequest

object

The channel and exactly one of phone (E.164, WhatsApp channels) or identity. Added within v1: identity type external finds the web chat contact your website signed in with that user id.

  • channel_id Id required
  • phone string
    • Pattern: ^\+[1-9][0-9]{4,19}$
  • identity object
    • type string required
      • Allowed values: bsuid, psid, igsid, web_visitor, telegram, email, external
    • value string required
      • Maximum length: 255

Contact

object

  • object "contact" required
  • id Id required
  • display_name string | null required

    The name shown in the inbox: the name an agent wrote, the WhatsApp profile name, on Messenger and Instagram the name UPPU reads from Meta's profile endpoint after the contact's first message (Instagram falls back to @username), or on Telegram the name every update carries (first and last name, otherwise @username; present from the first message). On web chat the name your website signs for a signed-in user (added within v1) fills an empty name. An agent-edited name is never overwritten. Null until a name is known.

  • blocked boolean required
  • merged_into string | null required

    Id of the contact this contact was merged into on verified evidence, null when it was not merged. A merged contact has empty identities, service_windows and consents, is visible while that contact is, and is never a recipient (recipient_not_found). A merge can be undone, so read it again.

  • identities object[] required

    Identities on the key's channels; superseded identities have current false. Added within v1: on web chat type external is the user id your website signs for a signed-in user, and email and phone are the values it signs with it; on e-mail channels type email is the address the person wrote from (their Reply-To when they gave one), in lower case. Accept unknown types.

    Items

    • channel_id Id required
    • type string required
    • value string required
    • current boolean required
  • service_windows object[] required

    The 24-hour reply window per WhatsApp, Messenger or Instagram channel, counted from Meta's timestamp of the person's last message (never later than when UPPU received it); on Messenger and Instagram also of their last reaction (added within v1).

    Items

  • consents object[] required

    The current consent per channel type and purpose.

    Items

    • channel_type string required
    • purpose string required
      • Allowed values: marketing, utility
    • state string required
      • Allowed values: granted, withdrawn
    • recorded_at NullableTimestamp required
  • created_at NullableTimestamp required
  • notes string | null required

    The team's notes about the person, written in the inbox contact panel (plain text, at most 5000 characters; null without notes). Not the internal notes of a conversation. Added within v1.

  • attributes map<string | number> required

    The company's contact fields that have a value for this person, by field name (the name the owner or an admin gave the field in the panel). A text field is a string, a number field a number, a date field a "YYYY-MM-DD" string and a list field one of its options. An empty object when no field has a value. Read only in v1: the team fills them in the inbox contact panel. Added within v1.

ConsentRequest

object

A consent record to add (POST /consents): the fields of an inline consent plus the channel and the person.

  • channel_id Id required
  • to Recipient required
  • purpose string required
    • Allowed values: marketing, utility
  • state string required
    • Allowed values: granted, withdrawn
  • source_text string required

    The exact text the person agreed to (or the withdrawal they asked for).

    • Maximum length: 2000
  • source_ref string required

    Your reference to the evidence (no personal data), for example web-form:checkout:7781.

    • Maximum length: 191
  • collected_at string

    When the person gave or withdrew the consent; now if left out, never in the future.

    • Format: date-time

ConsentList

object

  • object "list" required
  • data Consent[] required
  • next_cursor string | null required

object

A record of the consent ledger; records are only added, never changed.

  • object "consent" required
  • id Id required
  • contact_id Id required
  • channel_type string required
  • purpose string required
    • Allowed values: marketing, utility
  • state string required
    • Allowed values: granted, withdrawn
  • source string required

    api (recorded with an API key: POST /consents or the inline consent of a template send), inbound_keyword (the person wrote STOP, DUR or İPTAL; both purposes), meta_opt_out (Meta's marketing opt-out, error 131050), meta_user_preferences (the person stopped the business's marketing messages in WhatsApp; marketing withdrawals only, turning them back on is not a consent and is not recorded), agent_recorded (an owner or admin of the company recorded it in the UPPU panel) or import (a row of a CSV file the company imported in the UPPU panel). v1 may add values; accept unknown values.

  • source_text string | null required

    The exact text the person agreed to; null for automatic withdrawals and for withdrawals recorded in the panel or imported without a note.

  • source_ref string | null required

    Your reference to the evidence; null for automatic withdrawals and for imported rows without a reference. For agent_recorded it is how the person told the company (conversation, phone_call, written, in_person or other), followed by ":" and the reference the panel user gave, if any.

  • collected_at Timestamp required

    When the person gave or withdrew the consent.

  • recorded_at Timestamp required

    When UPPU recorded it.

  • api_key_prefix string | null required

    The prefix of the API key that recorded it; null otherwise.

ConversationList

object

  • object "list" required
  • data Conversation[] required
  • next_cursor string | null required

Conversation

object

A conversation of a contact on a channel. A contact has at most one open conversation per channel; a message after the conversation was resolved opens a new one. held_by and handover_reason were added within v1 with the handover endpoints; ignore unknown fields.

  • object "conversation" required
  • id Id required
  • channel_id Id required
  • contact_id Id required
  • status string required
    • Allowed values: open, resolved
  • handling string required

    The channel's stored handling (inbox the team inbox, api your own system, bot UPPU Bot), also while a module of the company's plan is off (see the channel's handling); accept unknown values.

    • Allowed values: inbox, api, bot
  • assigned boolean required

    Whether a team member is assigned; the assignee is never shown.

  • handed_over_at NullableTimestamp required

    On api and bot channels: null while the automation (your system or UPPU Bot) holds the conversation, otherwise when it was handed over to the team. On bot channels UPPU Bot hands over by itself (a handover step of its flow, a request for a person, an error), a team member's reply, claim or assignment in the inbox takes the conversation over, and returning it to the bot in the inbox sets it back to null. Conversations that are open when a channel moves to UPPU Bot or away from it are handed over to the team. inbox channels ignore it: the team holds their conversations. In a company whose plan has no messaging panel, bot channels ignore it too (see the channel's handling). Your system hands over and releases with POST /conversations/{id}/handover and /release (added within v1).

  • held_by string required

    Who holds the conversation (added within v1): api (your system, on an api channel), bot (UPPU Bot, on a bot channel) or team (handed over, and every conversation of an inbox channel). It follows the channel's stored handling and handed_over_at, like handling; on a resolved conversation it names who held it last (a conversation reopened by the person's message stays with them). When an owner changes the channel's handling in the panel, the channel's open conversations change hands without a conversation.updated event: moving an api channel to UPPU Bot or to the team inbox gives your system's open conversations to the team, and moving an inbox channel to api gives you its open conversations that were not handed over. Only removing UPPU Bot from a channel sends handed_over (bot_detached) for each conversation the bot held. The channel (GET /channels) and the conversation.handling of message.received show the new handling; re-read the channel's open conversations (GET /conversations?channel_id=…&status=open) when it changes. Accept unknown values.

    • Allowed values: api, bot, team
  • handover_reason string | null required

    Why the conversation was last handed over to the team (added within v1); null while the automation holds it, and when the handover has no recorded reason (open conversations stamped when their channel moved to UPPU Bot, or when UPPU Bot came back to the company's plan; the reason of an earlier handover that was released or returned is never carried over). api is your own handover (POST /conversations/{id}/handover); the others are UPPU Bot's, for example flow (a handover step of its flow), keyword (the person asked for a person), agent_takeover (a team member replied, claimed or assigned it), bot_detached (the bot was removed from the channel), external_reply (someone replied outside UPPU), staff_initiated (the person answered a template of the team) and schedule (a new conversation came outside the bot's schedule on the channel). Accept unknown values.

    • Allowed values: flow, keyword, low_confidence, max_attempts, llm_unavailable, llm_busy, action_error, action_unknown, identity_unverified, window_closed, stale, policy_refused, unsupported_content, budget_exceeded, loop_detected, turn_failed, step_budget, pipeline_stalled, send_failed, bot_unavailable, bot_detached, agent_takeover, external_reply, staff_initiated, api, schedule, null
  • opened_at Timestamp required

    When the conversation was opened; a reopened conversation keeps it.

  • resolved_at NullableTimestamp required

    When the conversation was resolved; null while it is open.

  • last_inbound_at NullableTimestamp required

    The provider's timestamp of the person's last message in this conversation (Meta's; on Telegram the message's date; the receive time when the provider sent none, for example a Telegram button tap or a web chat message), never later than when UPPU received it; null if the person never wrote in it, for example a conversation opened by your template. On Messenger and Instagram the person's reaction counts as a message here, since it opens the reply window (added within v1).

  • last_message_at Timestamp required

    The last activity (inbound, outbound or system message; UPPU's automatic privacy notice on WhatsApp, Messenger and Instagram leaves it unchanged, the time stays that of the contact's message); api and bot channels resolve the idle conversations the automation holds from it. In a company whose plan has no messaging panel, inbox and bot channels resolve every idle open conversation from it, after the same 24 hours (see the channel's handling).

  • service_window object | null required

    The contact's current 24-hour reply window on the conversation's channel, as in message.received (counted from the person's latest message on the channel, in any conversation, or on Messenger and Instagram their latest reaction); null on web chat, Telegram and e-mail, which have no reply window.

HandoverRequest

object

The optional body of POST /conversations/{id}/handover (added within v1). Both fields appear only in the team inbox, next to the handover; the API and the webhooks never return them.

  • reason string

    A short code of your own for why you hand the conversation over, for example customer_request. No personal data.

    • Pattern: ^[A-Za-z0-9._:-]+$
    • Maximum length: 64
  • note string

    A note for the team (line breaks allowed, no other control characters).

    • Maximum length: 1000

ConversationUpdate

object

The data of a conversation.updated event (added within v1). Ordered per contact, together with the contact's message events. Never the assignee's identity, the handover note or your reason and note.

  • conversation Conversation required
  • change string required

    What changed: handed_over (to the team: UPPU Bot's handover, a team member's take-over, the bot removed from the channel, or your handover), returned_to_bot (back to UPPU Bot from the inbox or with POST /conversations/{id}/release), released (back to your system on an api channel), resolved, reopened, assigned or unassigned. A conversation that opens already assigned (the inbox's automatic assignment) sends assigned before the message.received of its first message. When a team member is removed, leaves, loses the reply permission or deletes their account, or the company closes, the member's open conversations send unassigned (member_removed, member_left, role_changed, account_deleted or tenant_closed) and their resolved ones send nothing. A change of the channel's handling sends nothing either (see the conversation's held_by). Accept unknown values.

    • Allowed values: handed_over, returned_to_bot, released, resolved, reopened, assigned, unassigned
  • reason string | null required

    For handed_over the conversation's handover_reason. For assigned, unassigned, resolved and reopened what caused it, for example claim, manual and reply (a team member in the inbox), auto (automatic assignment), idle (resolved after 24 hours without activity), api (your key), visitor_message (the person wrote within the reopen period), bot_handoff (the channel's automatic assignment after a handover, UPPU Bot's or yours), bot_idle, bot_end, returned_to_bot or member_removed. Null for returned_to_bot and released, and when nothing is recorded. Accept unknown values.

  • actor object required

    Who made the change: member (a team member in the inbox), api (an API key; api_key_prefix names it, so your system can recognise its own changes), bot (UPPU Bot) or system (UPPU on its own, for example an automatic assignment). Accept unknown types.

    • type string required
      • Allowed values: member, api, bot, system
    • api_key_prefix string | null required
  • occurred_at Timestamp required

ContactUpdate

object

The data of a contact.updated event (added within v1). Ordered per contact, together with the contact's message events; the envelope's channel is null. Never an earlier value.

  • contact Contact required
  • changes string[] required

    The fields of the contact that changed, in this order: display_name, identities (an identity added, replaced or superseded, or moved by a merge or its undo), merged_into, notes (only together with another change: the notes alone send no event) and attributes. The service windows and the consent summary follow the identities and conversations and are not listed. Never empty. Accept unknown values.

    • Allowed values: display_name, identities, merged_into, notes, attributes
  • reason string required

    What changed the contact: member_edited (a team member in the panel), provider_name (the name Messenger, Instagram or Telegram gives), pre_chat (the web chat pre-chat form filled an empty name), identity_added (the channel named another identity of the person), identity_rotated (WhatsApp changed the user id; contact.identity_changed follows the same change), signed_handoff (your website's signed sign-in on the web chat; also when replacing or turning off its secret signs out the browsers of signed-in users, whose web_visitor identities become current false), merged or merge_reverted (one event for each of the two contacts). Accept unknown values.

    • Allowed values: member_edited, provider_name, pre_chat, identity_added, identity_rotated, signed_handoff, merged, merge_reverted
  • occurred_at Timestamp required

TemplateList

object

  • object "list" required
  • data Template[] required
  • next_cursor string | null required

Template

object

  • object "template" required
  • id Id required
  • name string required
  • language string required
  • category string required

    Meta's category (UTILITY, MARKETING, AUTHENTICATION).

  • status string required

    Meta's status (APPROVED, PENDING, REJECTED, PAUSED, DISABLED ...).

  • quality string | null required
  • parameter_format string required
    • Allowed values: NAMED, POSITIONAL
  • channel_ids Id[] required
  • components TemplateComponent[] required
  • parameters TemplateParameterSchema required
  • sendable boolean required
  • unsendable_reason string | null required
    • Allowed values: not_approved, paused, category_not_allowed, unsupported_component, null
  • send_example TemplateContent | null required

    The content object of POST /messages with the template's own examples; null when the template cannot be sent. A variable without an example reads <name> and a URL button's parameter without one <1>; a file header carries a placeholder media_id made of zeros only: upload the file with POST /media and give its id.

  • status_changed_at NullableTimestamp required
  • created_at NullableTimestamp required

TemplateComponent

object

  • type string required
  • format string
  • text string
  • buttons object[]

    Items

    • type string required
    • text string | null required
    • url string
    • phone_number string

TemplateParameterSchema

object

  • format string required
    • Allowed values: named, positional
  • header TemplateVariable[] required
  • body TemplateVariable[] required
  • header_media string | null required

    Added within v1: image, video or document when the template's header is a file (send it in content.template.header with the media_id of an upload), null otherwise.

    • Allowed values: image, video, document, null
  • buttons TemplateButtonField[] required

    Added within v1: the buttons that take a value, by their position in the template. A URL button whose address has a variable needs its parameter (required true; example is the end of the address in the template's example); a quick reply may carry a payload. Buttons without a variable are not listed.

TemplateButtonField

object

  • index integer required
    • Minimum: 0
  • type string required
    • Allowed values: url, quick_reply
  • text string | null required
  • required boolean required
  • example string | null required

TemplateVariable

object

  • name string required

    The variable name, or its number for POSITIONAL templates.

  • example string | null required

TemplateCreateRequest

object

A WhatsApp template to create (POST /templates, added within v1): the rules of the panel's template page. Variables are either numbered ({{1}}, {{2}} ... from 1 without gaps) or named ({{first_name}}: lowercase letters, digits and underscores), never both; each variable once, every variable with a one-line example of at most 200 characters, and the body neither starts nor ends with a variable.

  • channel_id string required

    A WhatsApp channel of the key; the template belongs to the channel's WhatsApp Business account.

    • Pattern: ^[0-9a-hjkmnp-tv-z]{26}$
  • name string required

    1 to 512 lowercase letters, digits and underscores, for example order_update.

    • Pattern: ^[a-z0-9_]{1,512}$
  • language string required
    • Allowed values: tr, en
  • category string required

    Upper or lower case. Authentication templates cannot be created.

    • Allowed values: UTILITY, MARKETING, utility, marketing
  • body TemplateCreateBody required
  • footer object

    One line of at most 60 characters, without variables.

    • text string required
      • Maximum length: 60
  • At most 3 quick replies, 2 URL buttons and 1 phone number button; the quick replies next to each other, before or after the other buttons. Button texts are one line of at most 25 characters without variables and differ from each other.

TemplateCreateHeader

One of

A text header: one line of at most 60 characters with at most one variable ({{1}} in a numbered template) and its example. Or (added within v1) a file header with a sample file: the media_id of an upload of the same channel made with POST /media (a JPEG or PNG image up to 5 MB, an MP4 video up to 16 MB, a PDF document up to 25 MB). UPPU uploads the sample to Meta for the review (Meta's Resumable Upload); every message sent with the template still carries its own file. A header with a location cannot be created through UPPU (422 validation_failed, code not_supported).

One of

  1. object
    • type "text" required
    • text string required
      • Maximum length: 60
    • example string

      The example of the header's variable.

      • Maximum length: 200
  2. TemplateCreateMediaHeader

TemplateCreateMediaHeader

One of

A file header of a new template (added within v1): the sample file is the media_id of an upload of the same channel made with POST /media.

One of

  1. object
  2. object
  3. object
    • type "document" required
    • document HeaderFile required

TemplateCreateBody

object

  • text string required

    Plain text of at most 1024 characters; line breaks allowed.

    • Maximum length: 1024
  • examples Any of

    An example for every variable: an object by variable name (or number), or for a numbered template a list in the order of the numbers.

    Any of

    1. map<string>
    2. string[]
      • Maximum length: 200

TemplateCreateButton

object

quick_reply {type, text}; url {type, text, url, example}: an https or http address of at most 2000 characters whose only variable is {{1}} at its end, with example the end of an example address (the characters of a web address only); phone_number {type, text, phone_number}: E.164 with a plus sign.

  • type string required
    • Allowed values: quick_reply, url, phone_number
  • text string required
    • Maximum length: 25
  • url string
    • Maximum length: 2000
  • example string
    • Maximum length: 2000
  • phone_number string
    • Pattern: ^\+[1-9][0-9]{4,14}$

DeletedTemplate

object

  • object "template" required
  • id Id required
  • name string required
  • language string required
  • deleted true required

ReportSummary

object

The report of a period (GET /reports/summary, added within v1): the nightly counters of the panel's Reports page. Counts only. A section the report's tier does not include is null.

  • object "report_summary" required
  • tier string required

    The report's tier (basic or full; every company has full until the plan sets it).

    • Allowed values: basic, full
  • features ReportFeatures required
  • period ReportPeriod required
  • scope ReportScope required
  • totals ReportTotals required
  • first_response ReportDurations required
  • resolution ReportDurations required
  • sla ReportSla | null required

    SLA targets met and missed (counted from the targets' moments); null without the sla feature.

  • topics ReportTopic[] | null required

    The topics of new conversations, most first; null without the topics feature.

  • tags ReportTag[] | null required

    The other tags of new conversations, most first (a deleted tag has no name); null without the topics feature.

  • untagged integer | null required

    New conversations without a tag; null without the topics feature.

  • breakdowns ReportBreakdowns | null required

    Per channel, agent and group, only for the whole company (scope all); null otherwise. The totals and the agent and group rows count every channel of the company, also the channels UPPU stopped for the API.

  • updated_at NullableTimestamp required

    When the counters were last written.

ReportFeatures

object

The sections of the report's tier.

  • volume boolean required
  • times boolean required
  • channels boolean required
  • members boolean required
  • groups boolean required
  • sla boolean required
  • topics boolean required

ReportPeriod

object

  • from string required
    • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$
  • to string required

    The last day; never after the last counted day.

    • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$
  • days integer required
    • Minimum: 1
    • Maximum: 90
  • preset integer | null required

    7, 30 or 90 for the days parameter (and its default); null for from and to.

  • timezone string required

    The company's time zone; days are calendar days there.

  • counted_through string | null required

    The last day the nightly count finished; null before the first count.

    • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$
  • pending boolean required

    True while yesterday is not counted yet (the period ends earlier).

ReportScope

object

  • type string required
    • Allowed values: all, channel, member, group
  • id Id | null required

    The channel's, agent's or group's id; null for the whole company.

  • name string | null required

ReportTotals

object

The counts of the period. opened: conversations the contacts opened; team_opened: those that reached the team (on a channel your system or UPPU Bot answers, at the handover); unanswered: of these, without the team's first response on that day; replies: the team's messages; resolved and reopened: every resolve and reopen; open_at_end and open_unassigned_at_end: the team's open conversations at the end of the period's last day; tagged: new conversations with a tag (null without the topics feature).

  • opened integer required
    • Minimum: 0
  • team_opened integer required
    • Minimum: 0
  • unanswered integer required
    • Minimum: 0
  • unanswered_share integer | null required

    unanswered of team_opened in percent; null when team_opened is 0.

  • replies integer required
    • Minimum: 0
  • resolved integer required
    • Minimum: 0
  • reopened integer required
    • Minimum: 0
  • open_at_end integer required
    • Minimum: 0
  • open_unassigned_at_end integer required
    • Minimum: 0
  • tagged integer | null required

ReportDurations

object

First response times (from reaching the team to the team's first response) or resolution times (from the start of the cycle to the resolve, without the time the conversation waited). buckets counts the samples per bucket: at most 1m, 5m, 15m, 30m, 1h, 4h, 8h, 24h, 72h, or over for first responses; 15m, 1h, 4h, 8h, 24h, 72h, 7d, or over for resolutions. median and p90 are the buckets in which half and nine tenths of the samples fall (null without samples); average_seconds is seconds divided by count.

  • count integer required
    • Minimum: 0
  • seconds integer required
    • Minimum: 0
  • average_seconds integer | null required
  • median string | null required
  • p90 string | null required
  • buckets map<integer> required

ReportSla

object

  • targets_set boolean required

    Whether the company has SLA targets today.

  • first_response ReportSlaCount required
  • resolution ReportSlaCount required

ReportSlaCounts

object

ReportSlaCount

object

  • met integer required
    • Minimum: 0
  • missed integer required
    • Minimum: 0
  • share integer | null required

    met of met and missed in percent; null when both are 0.

ReportTopic

object

  • id Id required
  • name string required
  • count integer required
    • Minimum: 0

ReportTag

object

  • id Id required
  • name string | null required
  • deleted boolean required
  • count integer required
    • Minimum: 0

ReportBreakdowns

object

  • channels ReportRow[] required

    The channels the key can see by id, name and type; deleted channels are one row with deleted true and no name, and the channels UPPU stopped for the API are one last row with hidden true and no id, name or type.

  • members ReportRow[] | null required

    The agents (members who take conversations, and current members with counts) by id and name; the counts of people who are no longer members are one row with former true and no name. null without the members feature.

  • groups ReportRow[] | null required

    The groups; deleted groups are one row with deleted true and no name. null without the groups feature.

ReportRow

object

  • id Id | null required

    The channel's, agent's or group's id; null for the row of deleted channels or groups, of former members and of channels the key cannot see.

  • name string | null required
  • type string | null required

    The channel's type (channels only; null otherwise).

  • deleted boolean required
  • former boolean required
  • hidden boolean required

    True only for the one channel row that holds the counts of the channels the key cannot see (channels UPPU stopped for the API); its id, name and type are null. Always false for agents and groups.

  • totals ReportTotals required
  • first_response ReportDurations required
  • resolution ReportDurations required
  • sla ReportSlaCounts | null required

ReportDayList

object

  • object "list" required
  • data ReportDay[] required
  • next_cursor string | null required

ReportDay

object

The counts of one day (GET /reports/daily). answered and unanswered split team_opened by the team's first response on the day; open and open_unassigned are the team's open conversations at the end of the day.

  • object "report_day" required
  • day string required
    • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$
  • counted boolean required

    Whether the nightly count finished the day (zeros of a day not counted are unknown).

  • opened integer required
    • Minimum: 0
  • team_opened integer required
    • Minimum: 0
  • answered integer required
    • Minimum: 0
  • unanswered integer required
    • Minimum: 0
  • replies integer required
    • Minimum: 0
  • resolved integer required
    • Minimum: 0
  • reopened integer required
    • Minimum: 0
  • open integer required
    • Minimum: 0
  • open_unassigned integer required
    • Minimum: 0
  • tagged integer | null required
  • first_response ReportDurations required
  • resolution ReportDurations required
  • sla ReportSlaCounts | null required

EventList

object

  • object "list" required
  • data Event[] required
  • next_cursor string | null required

    The id of the last event of this page when more events follow; pass it as after.

Event

object

A webhook event; data reuses the REST resources (message, channel ...). Ignore unknown fields and types.

  • id Id required
  • object "event" required
  • type string required
    • Allowed values: message.received, message.status, message.created, contact.identity_changed, contact.consent_changed, contact.erased, template.updated, channel.status_changed, channel.health_changed, conversation.updated, contact.reachability_changed, contact.updated, account.service_stop_scheduled, account.service_resumed, webhook.test
  • api_version "v1" required
  • created_at Timestamp required
  • team_id Id required
  • channel object | null required
    • id Id required
    • type string required
  • test boolean required

    True only for test events (the webhook page's and the API's test event and probe, and the optional weekly test event).

  • data object required

SandboxInboundRequest

object

Staging simulator (F3-23): a message from the contact on a simulated channel.

  • channel_id Id required
  • from object required

    The sender; phone (E.164), bsuid or both, and an optional profile name.

    • phone string
      • Pattern: ^\+[1-9][0-9]{4,19}$
    • bsuid string
      • Maximum length: 140
    • name string
      • Maximum length: 128
  • content object required

    {"type": "<type>", "<type>": {...}} in the shape of a received message's content. text: {body}; image, video, document: {media_id?, caption?} (document also filename?); audio: {media_id?, voice?}; sticker: {media_id?, animated?}; location: {latitude, longitude, name?, address?, url?}; contacts: {contacts: [{name: {formatted_name, ...}, phones?, emails?, org?, addresses?, urls?, birthday?}]}; interactive_reply: {source?: interactive|template, kind?: button|list, id, title, description?}; reaction: {message_id, emoji}; unsupported: {} or nothing. Added within v1, while WhatsApp Flows are switched on: flow_reply: {flow_token?, response}, a completed form; with context.message_id set to a flow message and no flow_token, the token of that message is used.

    • type string required
      • Allowed values: text, image, video, audio, document, sticker, location, contacts, interactive_reply, reaction, unsupported, flow_reply
  • context object

    The message on this channel that the contact replies to.

    • message_id Id required

SandboxStatusRequest

object

Staging simulator (F3-23): a delivery status for an outbound message on a simulated channel.

  • message_id Id required
  • status string required
    • Allowed values: sent, delivered, read, failed
  • provider_code integer

    Meta's error code, only with failed (default 131026).

    • Minimum: 1

SandboxIdentityRotationRequest

object

Staging simulator (F3-23): the contact's new WhatsApp business-scoped user id.

  • contact_id Id required
  • new_bsuid string required
    • Maximum length: 140

SandboxEvent

object

The result of a simulator call (F3-23). outcome: processed; ignored (the normal pipeline skipped the event on purpose, reason is its code, for example status_stale or rotation_unmatched); failed (the event could not be processed); queued (not processed yet).

  • object "sandbox_event" required
  • type string required
    • Allowed values: inbound, status, identity_rotation, close_window, channel_health, template_status
  • outcome string required
    • Allowed values: processed, ignored, failed, queued
  • reason string | null required
  • provider_message_id string | null required
  • contact_id Id | null required
  • message Message | null required

SandboxChannelHealthRequest

object

A health scenario for a simulated channel.

  • status string required
    • Allowed values: green, amber, red
  • messaging_limit string

    The messaging limit in the event (default TIER_1K).

    • Allowed values: TIER_250, TIER_1K, TIER_10K, TIER_100K, TIER_UNLIMITED
  • quality string

    The quality rating in the event (default GREEN).

    • Allowed values: GREEN, YELLOW, RED, UNKNOWN

SandboxTemplateStatusRequest

object

A new status of a simulated channel's template (Meta's event value).

  • status string required
    • Allowed values: APPROVED, PENDING, REJECTED, PAUSED, DISABLED
  • reason string

    Only with REJECTED; Meta's reason value, NONE if left out.

    • Allowed values: ABUSIVE_CONTENT, INCORRECT_CATEGORY, INVALID_FORMAT, NONE, PROMOTIONAL, SCAM, TAG_CONTENT_MISMATCH

object

A short-lived signed download link of a media file. url works without the API key until expires_at, for at most max_uses requests; only the expiry revokes it.

  • object "media_link" required
  • media_id Id required
  • url string required

    The full address on this API host, with team, key_id, expires and signature in the query string.

  • expires_at Timestamp required
  • max_uses integer required

WebhookEndpointList

object

  • object "list" required
  • data WebhookEndpoint[] required
  • next_cursor string | null required

WebhookEndpoint

object

A webhook endpoint of the company, as the panel's Webhooks page shows it. url is masked (scheme, host, the port when it is not 443, and /...): the full address is never shown. secret is the signing secret, only in the answer that creates the endpoint and in the answer of a rotation; null everywhere else. Accept unknown event types, states and reasons.

  • object "webhook_endpoint" required
  • id Id required
  • url string required
  • events string[] required

    The subscribed event types (webhook.test always goes and is not listed).

  • channel_id Id | null required

    The channel filter; null for every channel.

  • ordering string required
    • Allowed values: best_effort, strict
  • max_in_flight integer required
  • api_version string required
  • state string required

    pending_verification: new or with a new address, gets no events until it is verified and activated. active. failing: 20 failures in a row, one attempt a minute. suspended: failing for 72 hours or answered 410, events are held for at most 7 days. disabled: turned off.

    • Allowed values: pending_verification, active, failing, suspended, disabled
  • consecutive_failures integer required
  • failing_since NullableTimestamp required
  • suspended_at NullableTimestamp required
  • suspended_reason string | null required

    gone (the endpoint answered 410) or failing (72 hours).

  • disabled_at NullableTimestamp required
  • last_success_at NullableTimestamp required
  • last_failure_at NullableTimestamp required
  • verification object required

    Whether a test event and the wrong-signature probe passed within the last hour, and their last HTTP status.

    • test_passed boolean required
    • probe_passed boolean required
    • probe_required boolean required

      Activation needs the probe too (a new endpoint or a new address).

    • last_test_status integer | null required
    • last_probe_status integer | null required
  • secret_rotating_until string | null required

    Until when the secret replaced by the last rotation still signs; null when none does.

    • Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$
  • weekly_test object required

    The optional weekly test event: UPPU sends webhook.test once a week to an active or failing endpoint that opted in. A failure only sets failed_at (a warning); the endpoint is never suspended for it. A passing weekly test or test event, turning the weekly test off and a new url clear failed_at. last_status is null when no HTTP answer came (for example a refused address or a timeout).

  • secret string | null required

    whsec_ and 44 base64 characters; only in the answer that creates the endpoint and in the answer of a rotation.

    • Pattern: ^whsec_[A-Za-z0-9+/]{43}=$
  • created_at Timestamp required

WebhookEndpointCreateRequest

object

  • url string required

    Your https address on port 443, at most 2048 characters.

    • Maximum length: 2048
  • events string[]

    At least one type; left out, the types that are on by default.

    • Allowed values: message.received, message.status, message.created, contact.identity_changed, contact.consent_changed, contact.erased, template.updated, channel.status_changed, channel.health_changed, conversation.updated, contact.reachability_changed, contact.updated, account.service_stop_scheduled, account.service_resumed
  • channel_id Id | null

    Only the events of this channel; null for every channel.

  • ordering string
    • Allowed values: best_effort, strict
    • Default: best_effort
  • max_in_flight integer
    • Minimum: 1
    • Maximum: 32
    • Default: 8
  • weekly_test boolean

    Send a test event every week (a failure is only a warning).

    • Default: false

WebhookEndpointUpdateRequest

object

Only the fields you send change.

  • url string

    A new address sends the endpoint back to verification.

    • Maximum length: 2048
  • events string[]
    • Allowed values: message.received, message.status, message.created, contact.identity_changed, contact.consent_changed, contact.erased, template.updated, channel.status_changed, channel.health_changed, conversation.updated, contact.reachability_changed, contact.updated, account.service_stop_scheduled, account.service_resumed
  • channel_id Id | null
  • ordering string
    • Allowed values: best_effort, strict
  • max_in_flight integer
    • Minimum: 1
    • Maximum: 32
  • weekly_test boolean

WebhookEndpointTestRequest

object

  • type string

    The event type of the sample (webhook.test if left out).

    • Allowed values: message.received, message.status, message.created, contact.identity_changed, contact.consent_changed, contact.erased, template.updated, channel.status_changed, channel.health_changed, conversation.updated, contact.reachability_changed, contact.updated, account.service_stop_scheduled, account.service_resumed, webhook.test

WebhookEndpointCheck

object

The result of a test event or of the wrong-signature probe.

  • object "webhook_endpoint_check" required
  • endpoint_id Id required
  • kind string required
    • Allowed values: test, probe
  • type string required
  • passed boolean required

    test: your server answered 2xx. probe: your server refused the wrong signature (any answer other than 2xx).

  • status integer | null required

    The HTTP status; null when no answer came (for example a refused address or a timeout).

  • latency_ms integer required
  • response_excerpt string | null required

    The first characters of your server's answer.

  • error string | null required

    Why the request failed, the address masked; null when your server answered.

  • checked_at Timestamp required

DeletedWebhookEndpoint

object

  • object "webhook_endpoint" required
  • id Id required
  • deleted true required