Skip to content

Developers

API reference v1

Every endpoint, parameter and schema of the UPPU customer API, generated from the OpenAPI document. For a guided start, read the quickstart; for events, the webhooks guide.

  • API version v1
  • OpenAPI 3.1.0
  • Generated from c8719e1 · 20 October 2026

Servers and authentication

Servers
EnvironmentBase URLKey
Livehttps://messaging.uppu.net/v1uppu_live_…
Staginghttps://messaging-staging.uppu.net/v1uppu_test_…
  • Send Authorization: Bearer <key> on every request. A key of the other environment gets 401 wrong_environment.
  • Call from your server. A request with an Origin header gets 403 browser_request_refused, so there is no “try it” button here.
  • Scopes are listed with each endpoint; a missing scope gets 403 insufficient_scope with details.required.

Conventions

  • IDs are lowercase ULIDs; times are RFC 3339 UTC with milliseconds; null fields are always present.
  • Unknown request fields get 422 validation_failed (unknown_field). Ignore unknown response fields and event types, accept unknown enum values and treat an unknown content.type as unsupported.
  • Lists take limit, cursor, order, created_after and created_before, and return next_cursor.
  • Rate limits, retries and the Idempotency-Key rules are explained on the developers overview.

Errors

Every error is an ErrorEnvelope. error.code is the stable contract and error.retryable tells you whether to retry. The error codes page lists every code.

Channels

The channels your key can use, with their status and what each one can send. A key bound to one channel sees only that channel.

GET/v1/channels

List the channels this key can use

  • Scope none (any key)

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

Parameters

GET /v1/channels: Parameters
NameInTypeDescription
limitqueryinteger

min 1 · max 100 · default 25

cursorquerystring

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.

orderquerystring

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

default "desc"

  • asc
  • desc
created_afterquerystring (date-time)
created_beforequerystring (date-time)

Responses

GET /v1/channels: Responses
StatusDescriptionBody
200A page of channelsChannelList
400 401 403 409 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

GET/v1/channels/{id}

Get a channel

  • Scope none (any key)

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

Parameters

GET /v1/channels/{id}: Parameters
NameInTypeDescription
idrequiredpathId

Responses

GET /v1/channels/{id}: Responses
StatusDescriptionBody
200The channelChannel
400 401 403 404 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

Messages

Send messages, read them back for reconciliation and mark inbound WhatsApp messages as read.

GET/v1/messages/{id}

Get a message of either direction

  • Scope messages:read

Needs the messages:read scope. A message outside the key's channels, or past retention, is 404.

Parameters

GET /v1/messages/{id}: Parameters
NameInTypeDescription
idrequiredpathId

Responses

GET /v1/messages/{id}: Responses
StatusDescriptionBody
200The messageMessage
400 401 403 404 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

GET/v1/messages

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

  • 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.

Parameters

GET /v1/messages: Parameters
NameInTypeDescription
channel_idqueryId
conversation_idqueryId
contact_idqueryId
directionquerystring
  • in
  • out
limitqueryinteger

min 1 · max 100 · default 25

cursorquerystring

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.

orderquerystring

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

default "desc"

  • asc
  • desc
created_afterquerystring (date-time)
created_beforequerystring (date-time)

Responses

GET /v1/messages: Responses
StatusDescriptionBody
200A page of messagesMessageList
400 401 403 404 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

POST/v1/messages

Send a message

  • Scope messages:send
  • Idempotency-Key required

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. Session content (text, interactive, reaction, location) needs the 24-hour window; 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. 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; contacts and stickers come in v1.1.

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.

Parameters

POST /v1/messages: Parameters
NameInTypeDescription
Idempotency-Keyrequiredheaderstring

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

POST /v1/messages: Request body
FieldTypeDescription
channel_idrequiredId
torequiredRecipient
contextobject

Reply to a message of the same channel and contact that has a provider id (WhatsApp).

contentrequiredOutboundContent
client_referencestring

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

min length 1 · max length 64

consentInlineConsent

Responses

POST /v1/messages: Responses
StatusDescriptionBody
200A replay of an earlier send with the same Idempotency-Key and body

Headers Idempotent-Replayed

Message
202Accepted; status queued (WhatsApp) or sent (web chat)Message
400 401 403 404 409 413 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

POST/v1/messages/{id}/read

Mark an inbound message as read

  • Scope messages:send

Needs the messages:send scope. Only inbound WhatsApp messages (422 not_inbound; web chat has no read receipts). 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

POST /v1/messages/{id}/read: Parameters
NameInTypeDescription
idrequiredpathId

Responses

POST /v1/messages/{id}/read: Responses
StatusDescriptionBody
200The read receipt was already requestedReadReceipt
202The read receipt is queuedReadReceipt
400 401 403 404 409 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

Contacts

Find a contact by phone number or identity. Personal data goes in the request body, never in a URL.

POST/v1/contacts/lookup

Find a contact by phone or identity

  • 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

POST /v1/contacts/lookup: Request body
FieldTypeDescription
channel_idrequiredId
phonestring

pattern ^\+[1-9][0-9]{4,19}$

identityobject

Responses

POST /v1/contacts/lookup: Responses
StatusDescriptionBody
200The contactContact
400 401 403 404 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

Templates

The WhatsApp message templates of your channels, with what you need to send them.

GET/v1/templates

List the WhatsApp templates of the key's channels

  • 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

GET /v1/templates: Parameters
NameInTypeDescription
channel_idqueryId
statusquerystring

Meta's status, for example APPROVED.

namequerystring
languagequerystring

For example tr or en_US.

limitqueryinteger

min 1 · max 100 · default 25

cursorquerystring

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.

orderquerystring

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

default "desc"

  • asc
  • desc
created_afterquerystring (date-time)
created_beforequerystring (date-time)

Responses

GET /v1/templates: Responses
StatusDescriptionBody
200A page of templatesTemplateList
400 401 403 404 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

GET/v1/templates/{id}

Get a template

  • Scope templates:read

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

Parameters

GET /v1/templates/{id}: Parameters
NameInTypeDescription
idrequiredpathId

Responses

GET /v1/templates/{id}: Responses
StatusDescriptionBody
200The templateTemplate
400 401 403 404 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

Events

The pull fallback for webhooks: the same events and bodies for 7 days.

GET/v1/events

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

  • 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) need an all-channels key. Test events are never listed.

Parameters

GET /v1/events: Parameters
NameInTypeDescription
afterqueryId

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.

typesquerystring

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

limitqueryinteger

min 1 · max 100 · default 25

Responses

GET /v1/events: Responses
StatusDescriptionBody
200Events, oldest firstEventList
400 401 403 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

Media

Put a file on UPPU before you send it, read a file's metadata and download inbound files.

POST/v1/media

Upload a media file to send later

  • 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 exactly two fields: channel_id (a WhatsApp, Messenger or Instagram channel the key can use; a web chat channel, and 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. 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 (Meta's lists are not verified). 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.
  • 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.

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}.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

POST /v1/media: Parameters
NameInTypeDescription
Idempotency-Keyheaderstring

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

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

Request body

multipart/form-data

POST /v1/media: Request body
FieldTypeDescription
channel_idrequiredId
filerequiredstring (binary)

Responses

POST /v1/media: Responses
StatusDescriptionBody
200A replay of an earlier upload with the same Idempotency-Key and file

Headers Idempotent-Replayed

Media
201The stored file; state readyMedia
400 401 403 404 409 413 415 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

GET/v1/media/{id}

Get a media file's metadata

  • 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.

Parameters

GET /v1/media/{id}: Parameters
NameInTypeDescription
idrequiredpathId

Responses

GET /v1/media/{id}: Responses
StatusDescriptionBody
200The media fileMedia
400 401 403 404 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

GET/v1/media/{id}/content

Download a media file

  • 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. There are no signed URLs.

Parameters

GET /v1/media/{id}/content: Parameters
NameInTypeDescription
idrequiredpathId

Responses

GET /v1/media/{id}/content: Responses
StatusDescriptionBody
200The file

Headers ETag

File bytes
206A range of the fileFile bytes
400 401 403 404 409 410 422 429 500 503An error in the standard envelope. Error codesErrorEnvelope

OpenAPI document

This reference's source, served without a key.

GET/v1/openapi.json

This document

  • Authentication none

Responses

GET /v1/openapi.json: Responses
StatusDescriptionBody
200The OpenAPI document of v1application/json

Webhooks

UPPU posts events to your HTTPS endpoint. Setup, signature verification with a test vector and delivery rules are in the webhooks guide.

POSTYour 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. After any other answer the event is retried: up to 12 attempts in all 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.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.

Headers

Webhook request headers
NameTypeDescription
webhook-idrequiredId

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

webhook-timestamprequiredstring

Unix seconds, new on every attempt.

webhook-signaturerequiredstring

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

Uppu-Event-Typerequiredstring
Uppu-Delivery-IdrequiredId

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

Uppu-Attemptrequiredstring

Request body

application/json · Event

Your response

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

Schemas

Objects used by the endpoints and events above. Fields marked “required” are always present; nullable fields are present with null.

Id

Type string

pattern ^[0-9a-hjkmnp-tv-z]{26}$

Timestamp

Type 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

Type 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

ChannelList: Fields
FieldTypeDescription
objectrequired"list"
datarequiredarray of Channel
next_cursorrequiredstring | null

Channel

Channel: Fields
FieldTypeDescription
objectrequired"channel"
idrequiredId
typerequiredstring
  • whatsapp
  • instagram
  • messenger
  • telegramComing soon
  • web
  • email (reserved, not in use)
namerequiredstring
display_phonerequiredstring | null

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

moderequiredstring
  • live
  • shadow
statusrequiredstring
  • pending
  • active
  • paused
  • disconnected
handlingrequiredstring
  • inbox
  • api
sendingrequiredstring
  • active
  • paused
  • unavailable
sending_reasonrequiredstring | null

Why sending is unavailable; the same codes as details.reason of channel_unavailable.

  • shadow_mode
  • meta_off
  • team_disabled
  • not_linked
  • not_active
  • no_token
  • not_supported
  • null
healthrequiredChannelHealth | null
messaging_limitrequiredstring | null

Meta's messaging limit tier (WhatsApp only).

qualityrequiredstring | null

Meta's quality rating (WhatsApp only).

capabilitiesrequiredChannelCapabilities
created_atrequiredTimestamp

ChannelHealth

ChannelHealth: Fields
FieldTypeDescription
statusrequiredstring
  • green
  • amber
  • red
  • unknown
reasonsrequiredarray of string

Reason codes of the checklist items that are not green.

checked_atrequiredNullableTimestamp

ChannelCapabilities

ChannelCapabilities: Fields
FieldTypeDescription
content_typesrequiredarray of string
limitsrequiredobject of integer

Per-field limits. Keys ending in _bytes count UTF-8 bytes (Instagram text_body_bytes); the other keys count characters, or items for counts such as buttons and quick_replies (Meta's limits are not verified and may change).

window_hoursrequiredinteger | null
phone_addressingrequiredboolean

Message

Message: Fields
FieldTypeDescription
objectrequired"message"
idrequiredId
channel_idrequiredId
channel_typerequiredstring
conversation_idrequiredId
contact_idrequiredId
directionrequiredstring
  • in
  • out
authorrequiredMessageAuthor
contentrequiredMessageContent
contextrequiredMessageContext | null
templaterequiredMessageTemplate | null
previewrequiredstring
statusrequiredstring
  • queued
  • accepted
  • sent
  • delivered
  • read
  • failed
  • unknown
  • received
status_changed_atrequiredNullableTimestamp
errorrequiredDeliveryError | null
client_referencerequiredstring | null

max length 64

provider_message_idrequiredstring | null
sent_atrequiredNullableTimestamp

Inbound: Meta's timestamp of the message (the receive time when Meta sent none). Outbound: when Meta accepted it (web chat: when it was written).

received_atrequiredNullableTimestamp

Inbound: when the message reached UPPU; null outbound. The reply 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.

created_atrequiredTimestamp

MessageAuthor

MessageAuthor: Fields
FieldTypeDescription
typerequiredstring
  • contact
  • agent
  • system
  • api
labelrequiredstring | null
api_key_prefixrequiredstring | null

MessageContent

{"type": T, T: {...}}. Types: text, image, video, audio, document, sticker, location, contacts, interactive, interactive_reply, template, reaction, unsupported, system. 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, Messenger and Instagram messages from before UPPU kept media files): 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.

MessageContent: Fields
FieldTypeDescription
typerequiredstring

Other keys may appear: read the ones you know and ignore the rest.

MessageContext

MessageContext: Fields
FieldTypeDescription
message_idrequiredstring | null
provider_message_idrequiredstring | null
forwardedrequiredboolean
frequently_forwardedrequiredboolean
referralrequiredobject | null
storyobject

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).

story.idrequiredstring | null

MessageTemplate

MessageTemplate: Fields
FieldTypeDescription
idrequiredstring | null
namerequiredstring | null
languagerequiredstring | null
categoryrequiredstring | null

DeliveryError

DeliveryError: Fields
FieldTypeDescription
coderequiredstring
  • 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
  • provider_error
messagerequiredstring
retryablerequiredboolean
provider_errorrequiredProviderError | null

ProviderError

ProviderError: Fields
FieldTypeDescription
providerrequired"meta"
coderequiredinteger
subcoderequiredinteger | null
titlerequiredstring | null
detailsrequiredstring | null

ErrorEnvelope

ErrorEnvelope: Fields
FieldTypeDescription
errorrequiredError

Error

Error: Fields
FieldTypeDescription
typerequiredstring
  • authentication
  • permission
  • invalid_request
  • conflict
  • policy
  • rate_limit
  • server
coderequiredstring
  • 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
  • shadow_mode
  • not_found
  • method_not_allowed
  • idempotency_key_reused
  • channel_paused
  • channel_unavailable
  • tenant_not_ready
  • media_not_ready
  • media_expired
  • payload_too_large
  • media_too_large
  • unsupported_media_type
  • validation_failed
  • window_closed
  • consent_required
  • consent_withdrawn
  • contact_blocked
  • 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
  • rate_limited
  • channel_backlog
  • internal_error
  • service_unavailable
statusrequiredinteger
retryablerequiredboolean
messagerequiredstring

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

request_idrequiredstring

max length 64

doc_urlrequiredstring | null
detailsrequiredobject

For example resource (not_found), required (insufficient_scope), fields (validation_failed: [{field, code, message}]).

provider_errorrequiredProviderError | null

MessageList

MessageList: Fields
FieldTypeDescription
objectrequired"list"
datarequiredarray of Message
next_cursorrequiredstring | null

SendMessageRequest

SendMessageRequest: Fields
FieldTypeDescription
channel_idrequiredId
torequiredRecipient
contextobject

Reply to a message of the same channel and contact that has a provider id (WhatsApp).

context.message_idrequiredId
contentrequiredOutboundContent
client_referencestring

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

min length 1 · max length 64

consentInlineConsent

Recipient

Exactly one of contact_id, phone (E.164 with a plus sign, WhatsApp only) or an identity that already exists on the channel.

One of:

Recipient: variant 1
FieldTypeDescription
contact_idrequiredId
Recipient: variant 2
FieldTypeDescription
phonerequiredstring

pattern ^\+[1-9][0-9]{4,19}$

Recipient: variant 3
FieldTypeDescription
identityrequiredobject
identity.typerequiredstring
  • bsuid
  • psid
  • igsid
  • web_visitor
identity.valuerequiredstring

max length 255

OutboundContent

{"type": T, T: {...}}. Web chat channels take text only. Limits come from the channel's capabilities (GET /channels; Meta's limits are not verified). Media (image, video, audio, document) takes exactly one of media_id (POST /media) or an https link that UPPU downloads before sending; contacts and stickers come in v1.1 (422 content_not_supported_on_channel).

One of:

TextContent

InteractiveContent

ReactionContent

LocationContent

TemplateContent

MediaContent

TextContent

TextContent: Fields
FieldTypeDescription
typerequired"text"
textrequiredobject
text.bodyrequiredstring

min length 1

text.preview_urlboolean

InteractiveContent

InteractiveContent: Fields
FieldTypeDescription
typerequired"interactive"
interactiverequiredobject
interactive.typerequiredstring

buttons and list on WhatsApp; buttons and quick_replies on Messenger; quick_replies on Instagram (see the channel's capabilities). quick_replies has the shape of buttons.

  • buttons
  • list
  • quick_replies
interactive.headerobject
interactive.header.typerequired"text"
interactive.header.textrequiredstring
interactive.bodyrequiredobject
interactive.body.textrequiredstring
interactive.footerobject
interactive.footer.textrequiredstring
interactive.buttonsarray of InteractiveOption

Buttons and quick replies messages only.

interactive.buttonstring

List message only; the label of the list button.

interactive.sectionsarray of object

List message only.

interactive.sections[].titlestring
interactive.sections[].rowsrequiredarray of InteractiveOption

InteractiveOption

InteractiveOption: Fields
FieldTypeDescription
idrequiredstring
titlerequiredstring
descriptionstring

List rows only.

ReactionContent

ReactionContent: Fields
FieldTypeDescription
typerequired"reaction"
reactionrequiredobject
reaction.message_idrequiredId
reaction.emojirequiredstring

An empty string removes the reaction (not verified).

LocationContent

LocationContent: Fields
FieldTypeDescription
typerequired"location"
locationrequiredobject
location.latituderequirednumber

min -90 · max 90

location.longituderequirednumber

min -180 · max 180

location.namestring
location.addressstring

MediaContent

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 and Instagram 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 no media at all. 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.

MediaContent: Fields
FieldTypeDescription
typerequiredstring
  • image
  • video
  • audio
  • document
imageMediaSource
videoMediaSource
audioMediaSource
documentMediaSource

MediaSource

MediaSource: Fields
FieldTypeDescription
media_idId
linkstring

pattern ^https:// · max length 2048

captionstring
filenamestring

max length 255

Media

Media: Fields
FieldTypeDescription
objectrequired"media"
idrequiredId
channel_idrequiredstring | null
directionrequiredstring
  • in
  • out
kindrequiredstring
  • image
  • video
  • audio
  • document
  • sticker
mime_typerequiredstring | null
sizerequiredinteger | null

Bytes; null until the file is stored.

sha256requiredstring | null

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

filenamerequiredstring | null
staterequiredstring
  • pending
  • ready
  • failed
  • expired
failure_reasonrequiredstring | null

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_untilrequiredNullableTimestamp

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

created_atrequiredTimestamp

TemplateContent

TemplateContent: Fields
FieldTypeDescription
typerequired"template"
templaterequiredobject

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. Button parameters are not supported yet; templates whose buttons have no variable are sent as they are.

template.idId
template.namestring
template.languagestring
template.headerobject
template.header.typerequired"text"
template.header.parametersrequiredTemplateValues
template.bodyobject
template.body.parametersrequiredTemplateValues

TemplateValues

Any of:

array of string

object of string

InlineConsent

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.

InlineConsent: Fields
FieldTypeDescription
purposerequiredstring
  • marketing
  • utility
staterequiredstring
  • granted
  • withdrawn
source_textrequiredstring

The exact text the person agreed to.

max length 2000

source_refrequiredstring

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

max length 191

collected_atstring (date-time)

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.

ReadReceipt

ReadReceipt: Fields
FieldTypeDescription
objectrequired"read_receipt"
message_idrequiredId
staterequiredstring
  • queued
  • accepted
  • failed
  • unknown

ContactLookupRequest

The channel and exactly one of phone (E.164, WhatsApp channels) or identity.

ContactLookupRequest: Fields
FieldTypeDescription
channel_idrequiredId
phonestring

pattern ^\+[1-9][0-9]{4,19}$

identityobject
identity.typerequiredstring
  • bsuid
  • psid
  • igsid
  • web_visitor
identity.valuerequiredstring

max length 255

Contact

Contact: Fields
FieldTypeDescription
objectrequired"contact"
idrequiredId
display_namerequiredstring | null

The name shown in the inbox: the name an agent wrote, the WhatsApp profile name, or on Messenger and Instagram the name UPPU reads from Meta's profile endpoint after the contact's first message (Instagram falls back to @username). An agent-edited name is never overwritten. Null until a name is known.

blockedrequiredboolean
merged_intorequiredstring | null
identitiesrequiredarray of object

Identities on the key's channels; superseded identities have current false.

identities[].channel_idrequiredId
identities[].typerequiredstring
identities[].valuerequiredstring
identities[].currentrequiredboolean
service_windowsrequiredarray of object

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).

service_windows[].channel_idrequiredId
service_windows[].openrequiredboolean
service_windows[].closes_atrequiredNullableTimestamp
consentsrequiredarray of object

The current consent per channel type and purpose.

consents[].channel_typerequiredstring
consents[].purposerequiredstring
  • marketing
  • utility
consents[].staterequiredstring
  • granted
  • withdrawn
consents[].recorded_atrequiredNullableTimestamp
created_atrequiredNullableTimestamp

TemplateList

TemplateList: Fields
FieldTypeDescription
objectrequired"list"
datarequiredarray of Template
next_cursorrequiredstring | null

Template

Template: Fields
FieldTypeDescription
objectrequired"template"
idrequiredId
namerequiredstring
languagerequiredstring
categoryrequiredstring

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

statusrequiredstring

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

qualityrequiredstring | null
parameter_formatrequiredstring
  • NAMED
  • POSITIONAL
channel_idsrequiredarray of Id
componentsrequiredarray of TemplateComponent
parametersrequiredTemplateParameterSchema
sendablerequiredboolean
unsendable_reasonrequiredstring | null
  • not_approved
  • paused
  • category_not_allowed
  • unsupported_component
  • null
send_examplerequiredTemplateContent | null
status_changed_atrequiredNullableTimestamp
created_atrequiredNullableTimestamp

TemplateComponent

TemplateComponent: Fields
FieldTypeDescription
typerequiredstring
formatstring
textstring
buttonsarray of object
buttons[].typerequiredstring
buttons[].textrequiredstring | null
buttons[].urlstring
buttons[].phone_numberstring

TemplateParameterSchema

TemplateParameterSchema: Fields
FieldTypeDescription
formatrequiredstring
  • named
  • positional
headerrequiredarray of TemplateVariable
bodyrequiredarray of TemplateVariable

TemplateVariable

TemplateVariable: Fields
FieldTypeDescription
namerequiredstring

The variable name, or its number for POSITIONAL templates.

examplerequiredstring | null

EventList

EventList: Fields
FieldTypeDescription
objectrequired"list"
datarequiredarray of Event
next_cursorrequiredstring | null

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

Event

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

Event: Fields
FieldTypeDescription
idrequiredId
objectrequired"event"
typerequiredstring
  • message.received
  • message.status
  • message.created
  • contact.identity_changed
  • contact.consent_changed
  • contact.erased
  • template.updated
  • channel.status_changed
  • channel.health_changed
  • conversation.updated (defined, not sent yet)
  • webhook.test
api_versionrequired"v1"
created_atrequiredTimestamp
team_idrequiredId
channelrequiredobject | null
testrequiredboolean

True only for the test events of the webhook page.

datarequiredobject

Generated from openapi.json (commit c8719e1, 20 October 2026) · Live OpenAPI document