{"openapi":"3.1.0","info":{"title":"UPPU API","version":"v1","description":"The UPPU customer API v1 (docs/tasarim/musteri-api-v1.md). Customers send, receive and read\nmessages only through UPPU; they never call Meta directly.\n\n- Authentication: `Authorization: Bearer uppu_live_<prefix>_<secret>` (staging issues `uppu_test_`).\n  A key of the other environment gets 401 `wrong_environment`.\n- Requests from a browser (any `Origin` header) get 403 `browser_request_refused`; no CORS headers are sent.\n- Ids are lowercase ULIDs; every object carries `object`. Times are RFC 3339 UTC with milliseconds;\n  null fields are always present. Clients must ignore unknown fields and treat an unknown\n  `content.type` as `unsupported`.\n- Every response carries `X-Request-Id` (a client value of at most 64 characters is echoed).\n- Every error uses the same envelope; `code` is the stable contract (see /docs/errors).\n\nEvents reach your systems as signed webhooks (see webhooks.event below; F3-18) and, for 7 days, through\nGET /events. Send with POST /messages (Idempotency-Key required), mark inbound messages read, look up\ncontacts and read templates (F3-17b). Upload media with POST /media and download inbound files with\nGET /media/{id}/content (F3-14). Consents, contact reads and conversations follow in F3-13.\n\nStaging only: a simulated WhatsApp channel (created on the developer page of the staging panel) and the\n/sandbox endpoints let you integrate without a real number (F3-23). The live API has no /sandbox paths.\n"},"servers":[{"url":"https://messaging.uppu.net/v1","description":"Live (uppu_live_ keys)"},{"url":"https://messaging-staging.uppu.net/v1","description":"Staging (uppu_test_ keys)"}],"security":[{"apiKey":[]}],"paths":{"/channels":{"get":{"operationId":"listChannels","summary":"List the channels this key can use","description":"Needs no scope. A channel-bound key sees only its channel.","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Order"},{"$ref":"#/components/parameters/CreatedAfter"},{"$ref":"#/components/parameters/CreatedBefore"}],"responses":{"200":{"description":"A page of channels","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelList"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/channels/{id}":{"get":{"operationId":"getChannel","summary":"Get a channel","description":"Needs no scope. A channel the key cannot see is 404.","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The channel","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Channel"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/messages/{id}":{"get":{"operationId":"getMessage","summary":"Get a message of either direction","description":"Needs the messages:read scope. A message outside the key's channels, or past retention, is 404.","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The message","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/messages":{"get":{"operationId":"listMessages","summary":"List messages of either direction (reconciliation after a webhook gap)","description":"Needs the messages:read scope. Messages of the key's channels, newest first unless order=asc. Filter by\nchannel_id, conversation_id, contact_id and direction; an id the key cannot see is 404. The cursor\ncarries the order; repeat the filters and the time filters on every page.\n","parameters":[{"name":"channel_id","in":"query","schema":{"$ref":"#/components/schemas/Id"}},{"name":"conversation_id","in":"query","schema":{"$ref":"#/components/schemas/Id"}},{"name":"contact_id","in":"query","schema":{"$ref":"#/components/schemas/Id"}},{"name":"direction","in":"query","schema":{"type":"string","enum":["in","out"]}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Order"},{"$ref":"#/components/parameters/CreatedAfter"},{"$ref":"#/components/parameters/CreatedBefore"}],"responses":{"200":{"description":"A page of messages","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageList"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"sendMessage","summary":"Send a message","description":"Needs the messages:send scope; an inline consent also needs consents:write. At most 20 requests per second\nper key. The Idempotency-Key header is required and stays unique per channel for good: the same key with\nthe same body returns 200 with the current state of the first message and Idempotent-Replayed: true, the\nsame key with another body gets 409 idempotency_key_reused. Use a new UUID or ULID per logical send; a\nrefused send keeps no key, so it can be retried with the same key once the condition changed.\n\nAddress the recipient by contact_id (recommended), by phone (E.164, WhatsApp only; a contact of the channel\nthat owns the phone, otherwise a new unverified contact) or by an identity that already exists on the\nchannel. UPPU picks Meta's address itself. Session content (text, interactive, reaction, location) needs\nthe 24-hour window; an approved template does not. The window is counted like Meta counts it: from Meta's\ntimestamp of the person's last message (its sent_at, never later than its received_at), not from the\nmoment the message reached UPPU. Meta can deliver old messages late; such a message arrives with the\nwindow already closed (service_window.open false in message.received) and a session message then gets\n422 window_closed: on WhatsApp send a template. Template rules: MARKETING needs a recorded marketing\nconsent, UTILITY a recorded utility consent or a message from the person on this number after the latest\nwithdrawal, AUTHENTICATION is never sent. A policy refusal writes nothing but a ledger row; a validation\nerror writes nothing. Media (image, video, audio, document) is sent by media_id (POST /media) or by an https\nlink that UPPU downloads, stores and uploads to Meta before sending; contacts and stickers come in v1.1.\n\nA send to a contact without an open conversation is recorded in a new, already resolved conversation; the\nreply of the person opens a conversation as usual. Statuses follow as message.status webhooks (accepted,\nsent, delivered, read, failed) with your client_reference. Do not resend on unknown.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageRequest"}}}},"responses":{"200":{"description":"A replay of an earlier send with the same Idempotency-Key and body","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"Idempotent-Replayed":{"description":"Always true on a replay.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"202":{"description":"Accepted; status queued (WhatsApp) or sent (web chat)","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"413":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/messages/{id}/read":{"post":{"operationId":"markMessageRead","summary":"Mark an inbound message as read","description":"Needs the messages:send scope. Only inbound WhatsApp messages (422 not_inbound; web chat has no read\nreceipts). The 24-hour window applies. Returns 202 the first time and 200 with the current state on a\nrepeat. Send an empty body or {}.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The read receipt was already requested","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadReceipt"}}}},"202":{"description":"The read receipt is queued","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadReceipt"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/contacts/lookup":{"post":{"operationId":"lookupContact","summary":"Find a contact by phone or identity","description":"Needs the contacts:read scope. Personal data never goes into a URL: the phone or identity is in the body.\nGive the channel and exactly one of phone (E.164, WhatsApp channels) or identity. A contact that is not\nfound on the channel is 404. This is a read: it also works for read-only companies.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactLookupRequest"}}}},"responses":{"200":{"description":"The contact","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/templates":{"get":{"operationId":"listTemplates","summary":"List the WhatsApp templates of the key's channels","description":"Needs the templates:read scope. Templates belong to the WhatsApp Business account of a channel; channel_ids\nlists the key's channels that can use a template. Deleted templates are left out unless status asks for\nthem. sendable tells whether POST /messages accepts the template today; send_example is a ready content\nobject built from the template's own example values.\n","parameters":[{"name":"channel_id","in":"query","schema":{"$ref":"#/components/schemas/Id"}},{"name":"status","in":"query","description":"Meta's status, for example APPROVED.","schema":{"type":"string"}},{"name":"name","in":"query","schema":{"type":"string"}},{"name":"language","in":"query","description":"For example tr or en_US.","schema":{"type":"string"}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Order"},{"$ref":"#/components/parameters/CreatedAfter"},{"$ref":"#/components/parameters/CreatedBefore"}],"responses":{"200":{"description":"A page of templates","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateList"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/templates/{id}":{"get":{"operationId":"getTemplate","summary":"Get a template","description":"Needs the templates:read scope. A template outside the key's channels is 404.","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The template","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/events":{"get":{"operationId":"listEvents","summary":"Pull the events of the last 7 days (webhook fallback)","description":"Needs the events:read scope; at most 2 requests per second per key. Returns the same event ids and the\nsame bodies that the webhooks deliver, oldest first, for 7 days after an event was created. Pass the\nnext_cursor of the previous page as after. An event is listed about 10 seconds after it was recorded,\nonce every transaction that may still write an earlier id has finished, so a cursor never skips an\nevent. A channel-bound key sees only the events of its channel; events without a channel\n(template.updated) need an all-channels key. Test events are never listed.\n","parameters":[{"name":"after","in":"query","description":"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.","schema":{"$ref":"#/components/schemas/Id"}},{"name":"types","in":"query","description":"Comma-separated event types, for example message.received,message.status.","schema":{"type":"string"}},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"Events, oldest first","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventList"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/media":{"post":{"operationId":"uploadMedia","summary":"Upload a media file to send later","description":"Needs the media:write scope; at most 2 requests per second and 1 GB per day per key. A multipart/form-data\nbody with exactly two fields: channel_id (a WhatsApp, Messenger or Instagram channel the key can use; a\nweb chat channel, and an Instagram channel while UPPU's Instagram outbound media is off, gets 422\ncontent_not_supported_on_channel; the channel's capabilities.content_types show whether media can be\nsent) and file. At most 25 MB; the type is read from the content and must be one the channel's type can\nsend, otherwise 415 unsupported_media_type (Meta's lists are UNVERIFIED). An MP4 container that reads as\nvideo is audio when the file part is declared audio/mp4, and video otherwise.\n\n- WhatsApp: image/jpeg, image/png up to 5 MB; video/mp4, video/3gpp, audio/aac, audio/amr, audio/mpeg,\n  audio/mp4, audio/ogg up to 16 MB; PDF, plain text and Office documents up to 25 MB.\n- Messenger: image/jpeg, image/png, image/gif up to 8 MB; video/mp4; audio/aac, audio/mpeg, audio/mp4,\n  audio/wav; PDF, plain text and Office documents; all up to 25 MB.\n- Instagram (when outbound media is on): image/jpeg, image/png up to 8 MB; video/mp4, video/quicktime,\n  video/webm; audio/aac, audio/mp4, audio/wav; up to 25 MB. No documents.\n\nA company stores at most 20 GB (413 media_too_large, details.reason tenant_quota).\n\nThe upload can be sent for 7 days (reusable_until) as content.{image|video|audio|document}.media_id; the\nfile is kept 90 days after the last send that used it. An optional Idempotency-Key (company scope): the same\nkey with the same file returns 200 with the first upload and Idempotent-Replayed: true, another request\nwith the same key gets 409 idempotency_key_reused.\n","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional; letters, digits, dot, underscore, colon or hyphen, at most 64.","schema":{"type":"string","pattern":"^[A-Za-z0-9._:-]{1,64}$"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","additionalProperties":false,"required":["channel_id","file"],"properties":{"channel_id":{"$ref":"#/components/schemas/Id"},"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"A replay of an earlier upload with the same Idempotency-Key and file","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"Idempotent-Replayed":{"description":"Always true on a replay.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Media"}}}},"201":{"description":"The stored file; state ready","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Media"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"413":{"$ref":"#/components/responses/Error"},"415":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/media/{id}":{"get":{"operationId":"getMedia","summary":"Get a media file's metadata","description":"Needs the media:read scope. Inbound files (the media.id of a message.received image, video, audio,\ndocument or sticker), your uploads and the files UPPU fetched from your links. A file of a channel the key\ncannot see is 404. state is pending while UPPU downloads the file, ready once it is stored, failed with\nfailure_reason, expired after retention (inbound 90 days; an upload 90 days after its last send).\nMessenger and Instagram: every media attachment of a message is its own file\n(content.{type}.media and content.{type}.attachments), whatever attachment comes before it, as is the\npreview of a shared post or a reel sent without a media attachment (content.unsupported.media).\nInstagram stories (a story reply's story, a story mention) are never downloaded: Meta does not allow\nstoring story content. Meta's attachment links are short-lived: UPPU downloads them at once and retries\ntemporary errors within the link's 24 hours. If a temporary error lasts so long that the next retry would\nfall outside those 24 hours, the file ends as failed with download_failed (about 16 hours of continuous\noutage with today's schedule). An expired link (Meta answers 403, 404 or 410, or the download could not\nstart within 24 hours) ends as failed with provider_expired.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The media file","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Media"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/media/{id}/content":{"get":{"operationId":"downloadMedia","summary":"Download a media file","description":"Needs the media:read scope; at most 10 requests per second and 2 GB per hour per key. Returns the bytes\nwith Content-Type, Content-Length, ETag (\"<sha256>\"), Content-Disposition: attachment,\nX-Content-Type-Options: nosniff and Cache-Control: private, no-store; Range requests work (one range per\nrequest). The hourly byte budget is charged with the bytes a single range asks for; a request without\nRange, with several ranges or with If-Range is charged the whole file. While the file is still\ndownloading the answer is 409 media_not_ready with Retry-After: 2 at once; the request is not held. A\nfailed file is 422 media_unavailable (details.reason), a file past retention 410 media_expired. There are\nno signed URLs.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The file","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"ETag":{"description":"The SHA-256 of the file, quoted.","schema":{"type":"string"}}},"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"206":{"description":"A range of the file","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"410":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/sandbox/inbound":{"post":{"operationId":"simulateInbound","summary":"Staging simulator: a message from the contact","description":"Staging only (https://messaging-staging.uppu.net/v1 with a uppu_test_ key); the live API has no /sandbox\npaths (404). Needs the messages:send scope and a simulated channel, created on the developer page of the\nstaging panel; any other channel gets 422 validation_failed (channel_id not_simulated). Nothing reaches Meta.\n\nThe content has the shape of a received message's content (message.received): text, image, video, audio,\ndocument, sticker (media_id of a file uploaded to the same channel with POST /media, otherwise a sample\nfile), location, contacts, interactive_reply (a button or list reply; source template for a template's\nquick-reply button), reaction (message_id of a message on this channel; an empty emoji removes it) and\nunsupported. from needs phone (E.164), bsuid or both. context.message_id makes it a reply. The event goes\nthrough the normal inbound pipeline: contact, conversation, 24-hour window, media download and the\nmessage.received webhook event are the same as for a real message.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxInboundRequest"}}}},"responses":{"202":{"description":"The simulated event was processed; message is the recorded inbound message.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxEvent"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/sandbox/statuses":{"post":{"operationId":"simulateStatus","summary":"Staging simulator: a delivery status","description":"Staging only. A delivery status for an outbound message on a simulated channel: sent, delivered, read or\nfailed (provider_code is Meta's error code, default 131026). Statuses only move forward; a status that\ndoes not advance is ignored (reason status_stale). The message.status webhook event follows as for a\nreal status. Without this call a simulated send walks through accepted, sent, delivered and read by\nitself; the last two digits of the recipient's phone choose another outcome (see the developer page).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxStatusRequest"}}}},"responses":{"202":{"description":"The simulated status was processed; message is the message with its current status.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxEvent"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/sandbox/identity-rotation":{"post":{"operationId":"simulateIdentityRotation","summary":"Staging simulator: the contact's WhatsApp user id changes","description":"Staging only. The contact's current BSUID on a simulated channel changes to new_bsuid, as Meta reports\nwith user_id_update; the contact.identity_changed webhook event follows. A contact without a BSUID on a\nsimulated channel gets 422 validation_failed (contact_id no_bsuid): simulate an inbound message with\nfrom.bsuid first.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxIdentityRotationRequest"}}}},"responses":{"202":{"description":"The simulated identity change was processed","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxEvent"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/sandbox/contacts/{id}/close-window":{"post":{"operationId":"simulateWindowClose","summary":"Staging simulator: close the 24-hour window","description":"Staging only. Moves the contact's last inbound message on simulated channels more than 24 hours back:\na session message then gets 422 window_closed and only a template can be sent. No webhook event. Send an\nempty body or {}.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"202":{"description":"The window is closed (outcome ignored with reason no_inbound_message if it never opened)","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxEvent"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/openapi.json":{"get":{"operationId":"getOpenApi","summary":"This document","security":[],"responses":{"200":{"description":"The OpenAPI document of v1","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"webhooks":{"event":{"post":{"operationId":"receiveEvent","summary":"An event UPPU sends to your webhook endpoint","description":"One event per request, compact UTF-8 JSON, at most 256 KB, from the published egress IP. Verify the\nsignature over the raw body: base64(HMAC-SHA256(base64decode(secret without \"whsec_\"),\nwebhook-id + \".\" + webhook-timestamp + \".\" + body)), compared in constant time; refuse a timestamp more\nthan 300 seconds away; drop repeats by webhook-id (keep ids for at least 35 days). During a secret\nrotation webhook-signature carries two space-separated v1 values. Answer 2xx within 10 seconds and\nprocess later. Any other answer is retried 12 times over about 76 hours; 410 suspends the endpoint.\nDelivery is at least once and FIFO per contact; message.status carries previous_status for reordering.\nmessage.received carries service_window {open, closes_at}, counted from Meta's timestamp of the message:\na message Meta delivered late (received_at far after sent_at) can arrive with open false; on WhatsApp\nanswer it with a template.\n","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same across retries, resends and endpoints.","schema":{"$ref":"#/components/schemas/Id"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds, new on every attempt.","schema":{"type":"string"}},{"name":"webhook-signature","in":"header","required":true,"description":"One or two values \"v1,<base64>\" separated by a space.","schema":{"type":"string"}},{"name":"Uppu-Event-Type","in":"header","required":true,"schema":{"type":"string"}},{"name":"Uppu-Delivery-Id","in":"header","required":true,"description":"The delivery to this endpoint (a resend keeps it).","schema":{"$ref":"#/components/schemas/Id"}},{"name":"Uppu-Attempt","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}}}},"responses":{"2XX":{"description":"Received; UPPU does not send this event to the endpoint again."}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"uppu_live_<12 characters>_<43 characters>"}},"headers":{"RequestId":{"description":"The request id; it also appears in every error and in the request log.","schema":{"type":"string","maxLength":64}}},"parameters":{"Id":{"name":"id","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Id"}},"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},"Cursor":{"name":"cursor","in":"query","description":"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.","schema":{"type":"string"}},"Order":{"name":"order","in":"query","description":"Sort order. With a cursor and no order, the order of the cursor applies.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},"CreatedAfter":{"name":"created_after","in":"query","schema":{"type":"string","format":"date-time"}},"CreatedBefore":{"name":"created_before","in":"query","schema":{"type":"string","format":"date-time"}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"description":"Unique per channel for good (1 to 64 characters: letters, digits, dot, underscore, colon, hyphen). Use a UUID or ULID per logical send.","schema":{"type":"string","pattern":"^[A-Za-z0-9._:-]{1,64}$"}}},"responses":{"Error":{"description":"An error (see /docs/errors)","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"Retry-After":{"description":"Seconds to wait (429, 503 and retryable 409).","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"schemas":{"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":{"type":"object","additionalProperties":false,"required":["object","data","next_cursor"],"properties":{"object":{"const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Channel"}},"next_cursor":{"type":["string","null"]}}},"Channel":{"type":"object","additionalProperties":false,"required":["object","id","type","name","display_phone","mode","status","handling","sending","sending_reason","health","messaging_limit","quality","capabilities","created_at"],"properties":{"object":{"const":"channel"},"id":{"$ref":"#/components/schemas/Id"},"type":{"type":"string","enum":["whatsapp","instagram","messenger","telegram","web","email"]},"name":{"type":"string"},"display_phone":{"description":"E.164 with a plus sign; null for web channels.","type":["string","null"]},"mode":{"type":"string","enum":["live","shadow"]},"status":{"type":"string","enum":["pending","active","paused","disconnected"]},"handling":{"type":"string","enum":["inbox","api"]},"sending":{"type":"string","enum":["active","paused","unavailable"]},"sending_reason":{"description":"Why sending is unavailable; the same codes as details.reason of channel_unavailable.","type":["string","null"],"enum":["shadow_mode","meta_off","team_disabled","not_linked","not_active","no_token","not_supported",null]},"health":{"anyOf":[{"$ref":"#/components/schemas/ChannelHealth"},{"type":"null"}]},"messaging_limit":{"description":"Meta's messaging limit tier (WhatsApp only).","type":["string","null"]},"quality":{"description":"Meta's quality rating (WhatsApp only).","type":["string","null"]},"capabilities":{"$ref":"#/components/schemas/ChannelCapabilities"},"created_at":{"$ref":"#/components/schemas/Timestamp"}}},"ChannelHealth":{"type":"object","additionalProperties":false,"required":["status","reasons","checked_at"],"properties":{"status":{"type":"string","enum":["green","amber","red","unknown"]},"reasons":{"description":"Reason codes of the checklist items that are not green.","type":"array","items":{"type":"string"}},"checked_at":{"$ref":"#/components/schemas/NullableTimestamp"}}},"ChannelCapabilities":{"type":"object","additionalProperties":false,"required":["content_types","limits","window_hours","phone_addressing"],"properties":{"content_types":{"type":"array","items":{"type":"string"}},"limits":{"description":"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 UNVERIFIED and may change).","type":"object","additionalProperties":{"type":"integer"}},"window_hours":{"type":["integer","null"]},"phone_addressing":{"type":"boolean"}}},"Message":{"type":"object","additionalProperties":false,"required":["object","id","channel_id","channel_type","conversation_id","contact_id","direction","author","content","context","template","preview","status","status_changed_at","error","client_reference","provider_message_id","sent_at","received_at","created_at"],"properties":{"object":{"const":"message"},"id":{"$ref":"#/components/schemas/Id"},"channel_id":{"$ref":"#/components/schemas/Id"},"channel_type":{"type":"string"},"conversation_id":{"$ref":"#/components/schemas/Id"},"contact_id":{"$ref":"#/components/schemas/Id"},"direction":{"type":"string","enum":["in","out"]},"author":{"$ref":"#/components/schemas/MessageAuthor"},"content":{"$ref":"#/components/schemas/MessageContent"},"context":{"anyOf":[{"$ref":"#/components/schemas/MessageContext"},{"type":"null"}]},"template":{"anyOf":[{"$ref":"#/components/schemas/MessageTemplate"},{"type":"null"}]},"preview":{"type":"string"},"status":{"type":"string","enum":["queued","accepted","sent","delivered","read","failed","unknown","received"]},"status_changed_at":{"$ref":"#/components/schemas/NullableTimestamp"},"error":{"anyOf":[{"$ref":"#/components/schemas/DeliveryError"},{"type":"null"}]},"client_reference":{"type":["string","null"],"maxLength":64},"provider_message_id":{"type":["string","null"]},"sent_at":{"description":"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).","$ref":"#/components/schemas/NullableTimestamp"},"received_at":{"description":"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.","$ref":"#/components/schemas/NullableTimestamp"},"created_at":{"$ref":"#/components/schemas/Timestamp"}}},"MessageAuthor":{"type":"object","additionalProperties":false,"required":["type","label","api_key_prefix"],"properties":{"type":{"type":"string","enum":["contact","agent","system","api"]},"label":{"type":["string","null"]},"api_key_prefix":{"type":["string","null"]}}},"MessageContent":{"description":"`{\"type\": T, T: {...}}`. Types: text, image, video, audio, document, sticker, location, contacts,\ninteractive, interactive_reply, template, reaction, unsupported, system. Media of both directions carries\n`media: {id, mime_type, size, sha256, filename, state, failure_reason}`: `id` is UPPU's media id (GET\n/media/{id}/content), `size` and `sha256` stay null until the file is stored (state pending -> ready).\nA media object whose `id` is null belongs to an old inbound message stored before UPPU kept its files\n(WhatsApp before F3-14, Messenger and Instagram before their media support): it stays pending, never\nbecomes ready and cannot be downloaded, so do not poll for it.\nMessenger and Instagram (added within v1): a message's type is the type of its first media attachment,\nwhatever attachment comes before it (a link preview, a location, a template, a shared post, a reel or a\nstory mention followed by a photo is an `image` message). A message with several media attachments\ncarries `attachments: [{type, media}]` next to `media`, every media attachment (image, video, audio,\ndocument, sticker) in Meta's order (the first is the same file as `media`; the key is absent when there is\nat most one media attachment). Other attachment types (location, link previews, templates, shared posts,\nreels, story mentions) are not listed and have no file; the `preview` text counts every attachment. A\nmessage without a media attachment takes the type of its first attachment: a shared post or reel\n(`unsupported`) carries the preview's file as `unsupported.media` when Meta gave one; an Instagram story\nmention has no file (Meta does not allow storing story content). On Messenger and Instagram a caption\nreaches the person as a separate text message after the file. Meta's attachment links are never exposed.\n","type":"object","required":["type"],"properties":{"type":{"type":"string"}},"additionalProperties":true},"MessageContext":{"type":"object","additionalProperties":false,"required":["message_id","provider_message_id","forwarded","frequently_forwarded","referral"],"properties":{"message_id":{"type":["string","null"]},"provider_message_id":{"type":["string","null"]},"forwarded":{"type":"boolean"},"frequently_forwarded":{"type":"boolean"},"referral":{"type":["object","null"]},"story":{"description":"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).","type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"type":["string","null"]}}}}},"MessageTemplate":{"type":"object","additionalProperties":false,"required":["id","name","language","category"],"properties":{"id":{"type":["string","null"]},"name":{"type":["string","null"]},"language":{"type":["string","null"]},"category":{"type":["string","null"]}}},"DeliveryError":{"type":"object","additionalProperties":false,"required":["code","message","retryable","provider_error"],"properties":{"code":{"type":"string","enum":["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"]},"message":{"type":"string"},"retryable":{"type":"boolean"},"provider_error":{"anyOf":[{"$ref":"#/components/schemas/ProviderError"},{"type":"null"}]}}},"ProviderError":{"type":"object","additionalProperties":false,"required":["provider","code","subcode","title","details"],"properties":{"provider":{"const":"meta"},"code":{"type":"integer"},"subcode":{"type":["integer","null"]},"title":{"type":["string","null"]},"details":{"type":["string","null"]}}},"ErrorEnvelope":{"type":"object","additionalProperties":false,"required":["error"],"properties":{"error":{"$ref":"#/components/schemas/Error"}}},"Error":{"type":"object","additionalProperties":false,"required":["type","code","status","retryable","message","request_id","doc_url","details","provider_error"],"properties":{"type":{"type":"string","enum":["authentication","permission","invalid_request","conflict","policy","rate_limit","server"]},"code":{"type":"string","enum":["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"]},"status":{"type":"integer"},"retryable":{"type":"boolean"},"message":{"description":"Follows Accept-Language (tr by default, en).","type":"string"},"request_id":{"type":"string","maxLength":64},"doc_url":{"type":["string","null"]},"details":{"description":"For example resource (not_found), required (insufficient_scope), fields (validation_failed: [{field, code, message}]).","type":"object"},"provider_error":{"anyOf":[{"$ref":"#/components/schemas/ProviderError"},{"type":"null"}]}}},"MessageList":{"type":"object","additionalProperties":false,"required":["object","data","next_cursor"],"properties":{"object":{"const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"next_cursor":{"type":["string","null"]}}},"SendMessageRequest":{"type":"object","additionalProperties":false,"required":["channel_id","to","content"],"properties":{"channel_id":{"$ref":"#/components/schemas/Id"},"to":{"$ref":"#/components/schemas/Recipient"},"context":{"description":"Reply to a message of the same channel and contact that has a provider id (WhatsApp).","type":"object","additionalProperties":false,"required":["message_id"],"properties":{"message_id":{"$ref":"#/components/schemas/Id"}}},"content":{"$ref":"#/components/schemas/OutboundContent"},"client_reference":{"description":"Your opaque reference (no personal data); echoed in GET and in every message.status event.","type":"string","minLength":1,"maxLength":64},"consent":{"$ref":"#/components/schemas/InlineConsent"}}},"Recipient":{"description":"Exactly one of contact_id, phone (E.164 with a plus sign, WhatsApp only) or an identity that already exists on the channel.","oneOf":[{"type":"object","additionalProperties":false,"required":["contact_id"],"properties":{"contact_id":{"$ref":"#/components/schemas/Id"}}},{"type":"object","additionalProperties":false,"required":["phone"],"properties":{"phone":{"type":"string","pattern":"^\\+[1-9][0-9]{4,19}$"}}},{"type":"object","additionalProperties":false,"required":["identity"],"properties":{"identity":{"type":"object","additionalProperties":false,"required":["type","value"],"properties":{"type":{"type":"string","enum":["bsuid","psid","igsid","web_visitor"]},"value":{"type":"string","maxLength":255}}}}}]},"OutboundContent":{"description":"`{\"type\": T, T: {...}}`. Web chat channels take text only. Limits come from the channel's capabilities\n(GET /channels; Meta's limits are UNVERIFIED). Media (image, video, audio, document) takes exactly one of\nmedia_id (POST /media) or an https link that UPPU downloads before sending; contacts and stickers come in\nv1.1 (422 content_not_supported_on_channel).\n","oneOf":[{"$ref":"#/components/schemas/TextContent"},{"$ref":"#/components/schemas/InteractiveContent"},{"$ref":"#/components/schemas/ReactionContent"},{"$ref":"#/components/schemas/LocationContent"},{"$ref":"#/components/schemas/TemplateContent"},{"$ref":"#/components/schemas/MediaContent"}]},"TextContent":{"type":"object","additionalProperties":false,"required":["type","text"],"properties":{"type":{"const":"text"},"text":{"type":"object","additionalProperties":false,"required":["body"],"properties":{"body":{"type":"string","minLength":1},"preview_url":{"type":"boolean"}}}}},"InteractiveContent":{"type":"object","additionalProperties":false,"required":["type","interactive"],"properties":{"type":{"const":"interactive"},"interactive":{"type":"object","additionalProperties":false,"required":["type","body"],"properties":{"type":{"description":"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.","type":"string","enum":["buttons","list","quick_replies"]},"header":{"type":"object","additionalProperties":false,"required":["type","text"],"properties":{"type":{"const":"text"},"text":{"type":"string"}}},"body":{"type":"object","additionalProperties":false,"required":["text"],"properties":{"text":{"type":"string"}}},"footer":{"type":"object","additionalProperties":false,"required":["text"],"properties":{"text":{"type":"string"}}},"buttons":{"description":"Buttons and quick replies messages only.","type":"array","items":{"$ref":"#/components/schemas/InteractiveOption"}},"button":{"description":"List message only; the label of the list button.","type":"string"},"sections":{"description":"List message only.","type":"array","items":{"type":"object","additionalProperties":false,"required":["rows"],"properties":{"title":{"type":"string"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/InteractiveOption"}}}}}}}}},"InteractiveOption":{"type":"object","additionalProperties":false,"required":["id","title"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"description":"List rows only.","type":"string"}}},"ReactionContent":{"type":"object","additionalProperties":false,"required":["type","reaction"],"properties":{"type":{"const":"reaction"},"reaction":{"type":"object","additionalProperties":false,"required":["message_id","emoji"],"properties":{"message_id":{"$ref":"#/components/schemas/Id"},"emoji":{"description":"An empty string removes the reaction (UNVERIFIED).","type":"string"}}}}},"LocationContent":{"type":"object","additionalProperties":false,"required":["type","location"],"properties":{"type":{"const":"location"},"location":{"type":"object","additionalProperties":false,"required":["latitude","longitude"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180},"name":{"type":"string"},"address":{"type":"string"}}}}},"MediaContent":{"description":"An image, video, audio or document: `{\"type\": \"document\", \"document\": {\"media_id\": \"…\", \"filename\": \"…\",\n\"caption\": \"…\"}}`. Give exactly one of media_id (an upload of the same channel, within reusable_until) or\nlink (https on port 443, public addresses only, no redirects, at most the type's size, 15 seconds; a\nfailure ends as failed with media_fetch_failed). Audio has no caption; only a document has a filename.\nThe type's MIME types and sizes are those of POST /media for the channel's type. WhatsApp, Messenger and\nInstagram channels send media (see capabilities.content_types; Instagram only while UPPU's Instagram\noutbound media is on, otherwise 422 content_not_supported_on_channel); Instagram has no document (422\ncontent_not_supported_on_channel), web chat no media at all. On Messenger and Instagram the caption is\nsent as a separate text message right after the file (it has the channel's text limit: Messenger 2000\ncharacters, Instagram 1000 bytes), and Meta shows a document under the name it was uploaded with (the\nupload's file name, or filename with a link); one message, one status. If the file was accepted but the\ncaption could not be sent, the message stays accepted.\n","type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"type":"string","enum":["image","video","audio","document"]},"image":{"$ref":"#/components/schemas/MediaSource"},"video":{"$ref":"#/components/schemas/MediaSource"},"audio":{"$ref":"#/components/schemas/MediaSource"},"document":{"$ref":"#/components/schemas/MediaSource"}}},"MediaSource":{"type":"object","additionalProperties":false,"properties":{"media_id":{"$ref":"#/components/schemas/Id"},"link":{"type":"string","maxLength":2048,"pattern":"^https://"},"caption":{"type":"string"},"filename":{"type":"string","maxLength":255}}},"Media":{"type":"object","additionalProperties":false,"required":["object","id","channel_id","direction","kind","mime_type","size","sha256","filename","state","failure_reason","reusable_until","created_at"],"properties":{"object":{"const":"media"},"id":{"$ref":"#/components/schemas/Id"},"channel_id":{"type":["string","null"]},"direction":{"type":"string","enum":["in","out"]},"kind":{"type":"string","enum":["image","video","audio","document","sticker"]},"mime_type":{"type":["string","null"]},"size":{"description":"Bytes; null until the file is stored.","type":["integer","null"]},"sha256":{"description":"The hex SHA-256 of the stored file; null until it is stored.","type":["string","null"]},"filename":{"type":["string","null"]},"state":{"type":"string","enum":["pending","ready","failed","expired"]},"failure_reason":{"description":"Set when state is failed: too_large, mime_not_allowed, quota_exceeded, empty, no_provider_id,\nno_token, provider_refused, provider_expired, download_failed, link_refused, link_failed,\nupload_failed, channel_unavailable.\n","type":["string","null"]},"reusable_until":{"description":"Uploads only; the last moment the file can be sent with media_id.","$ref":"#/components/schemas/NullableTimestamp"},"created_at":{"$ref":"#/components/schemas/Timestamp"}}},"MessageMedia":{"description":"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.","type":"object","additionalProperties":false,"required":["id","mime_type","size","sha256","filename","state","failure_reason"],"properties":{"id":{"type":["string","null"]},"mime_type":{"type":["string","null"]},"size":{"type":["integer","null"]},"sha256":{"type":["string","null"]},"filename":{"type":["string","null"]},"state":{"type":"string","enum":["pending","ready","failed","expired"]},"failure_reason":{"type":["string","null"]}}},"TemplateContent":{"type":"object","additionalProperties":false,"required":["type","template"],"properties":{"type":{"const":"template"},"template":{"description":"Give id, or name and language (a name and language with several rows resolves to the single APPROVED\none). POSITIONAL templates take a list ({{1}} first), NAMED templates an object of name and value.\nValues are text only. Button parameters are not supported yet; templates whose buttons have no\nvariable are sent as they are.\n","type":"object","additionalProperties":false,"properties":{"id":{"$ref":"#/components/schemas/Id"},"name":{"type":"string"},"language":{"type":"string"},"header":{"type":"object","additionalProperties":false,"required":["type","parameters"],"properties":{"type":{"const":"text"},"parameters":{"$ref":"#/components/schemas/TemplateValues"}}},"body":{"type":"object","additionalProperties":false,"required":["parameters"],"properties":{"parameters":{"$ref":"#/components/schemas/TemplateValues"}}}}}}},"TemplateValues":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","additionalProperties":{"type":"string"}}]},"InlineConsent":{"description":"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.","type":"object","additionalProperties":false,"required":["purpose","state","source_text","source_ref"],"properties":{"purpose":{"type":"string","enum":["marketing","utility"]},"state":{"type":"string","enum":["granted","withdrawn"]},"source_text":{"description":"The exact text the person agreed to.","type":"string","maxLength":2000},"source_ref":{"description":"Your reference to the evidence (no personal data), for example web-form:checkout:7781.","type":"string","maxLength":191},"collected_at":{"description":"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.","type":"string","format":"date-time"}}},"ReadReceipt":{"type":"object","additionalProperties":false,"required":["object","message_id","state"],"properties":{"object":{"const":"read_receipt"},"message_id":{"$ref":"#/components/schemas/Id"},"state":{"type":"string","enum":["queued","accepted","failed","unknown"]}}},"ContactLookupRequest":{"description":"The channel and exactly one of phone (E.164, WhatsApp channels) or identity.","type":"object","additionalProperties":false,"required":["channel_id"],"properties":{"channel_id":{"$ref":"#/components/schemas/Id"},"phone":{"type":"string","pattern":"^\\+[1-9][0-9]{4,19}$"},"identity":{"type":"object","additionalProperties":false,"required":["type","value"],"properties":{"type":{"type":"string","enum":["bsuid","psid","igsid","web_visitor"]},"value":{"type":"string","maxLength":255}}}}},"Contact":{"type":"object","additionalProperties":false,"required":["object","id","display_name","blocked","merged_into","identities","service_windows","consents","created_at"],"properties":{"object":{"const":"contact"},"id":{"$ref":"#/components/schemas/Id"},"display_name":{"description":"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.","type":["string","null"]},"blocked":{"type":"boolean"},"merged_into":{"type":["string","null"]},"identities":{"description":"Identities on the key's channels; superseded identities have current false.","type":"array","items":{"type":"object","additionalProperties":false,"required":["channel_id","type","value","current"],"properties":{"channel_id":{"$ref":"#/components/schemas/Id"},"type":{"type":"string"},"value":{"type":"string"},"current":{"type":"boolean"}}}},"service_windows":{"description":"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).","type":"array","items":{"type":"object","additionalProperties":false,"required":["channel_id","open","closes_at"],"properties":{"channel_id":{"$ref":"#/components/schemas/Id"},"open":{"type":"boolean"},"closes_at":{"$ref":"#/components/schemas/NullableTimestamp"}}}},"consents":{"description":"The current consent per channel type and purpose.","type":"array","items":{"type":"object","additionalProperties":false,"required":["channel_type","purpose","state","recorded_at"],"properties":{"channel_type":{"type":"string"},"purpose":{"type":"string","enum":["marketing","utility"]},"state":{"type":"string","enum":["granted","withdrawn"]},"recorded_at":{"$ref":"#/components/schemas/NullableTimestamp"}}}},"created_at":{"$ref":"#/components/schemas/NullableTimestamp"}}},"TemplateList":{"type":"object","additionalProperties":false,"required":["object","data","next_cursor"],"properties":{"object":{"const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Template"}},"next_cursor":{"type":["string","null"]}}},"Template":{"type":"object","additionalProperties":false,"required":["object","id","name","language","category","status","quality","parameter_format","channel_ids","components","parameters","sendable","unsendable_reason","send_example","status_changed_at","created_at"],"properties":{"object":{"const":"template"},"id":{"$ref":"#/components/schemas/Id"},"name":{"type":"string"},"language":{"type":"string"},"category":{"description":"Meta's category (UTILITY, MARKETING, AUTHENTICATION).","type":"string"},"status":{"description":"Meta's status (APPROVED, PENDING, REJECTED, PAUSED, DISABLED ...).","type":"string"},"quality":{"type":["string","null"]},"parameter_format":{"type":"string","enum":["NAMED","POSITIONAL"]},"channel_ids":{"type":"array","items":{"$ref":"#/components/schemas/Id"}},"components":{"type":"array","items":{"$ref":"#/components/schemas/TemplateComponent"}},"parameters":{"$ref":"#/components/schemas/TemplateParameterSchema"},"sendable":{"type":"boolean"},"unsendable_reason":{"type":["string","null"],"enum":["not_approved","paused","category_not_allowed","unsupported_component",null]},"send_example":{"anyOf":[{"$ref":"#/components/schemas/TemplateContent"},{"type":"null"}]},"status_changed_at":{"$ref":"#/components/schemas/NullableTimestamp"},"created_at":{"$ref":"#/components/schemas/NullableTimestamp"}}},"TemplateComponent":{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"type":"string"},"format":{"type":"string"},"text":{"type":"string"},"buttons":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["type","text"],"properties":{"type":{"type":"string"},"text":{"type":["string","null"]},"url":{"type":"string"},"phone_number":{"type":"string"}}}}}},"TemplateParameterSchema":{"type":"object","additionalProperties":false,"required":["format","header","body"],"properties":{"format":{"type":"string","enum":["named","positional"]},"header":{"type":"array","items":{"$ref":"#/components/schemas/TemplateVariable"}},"body":{"type":"array","items":{"$ref":"#/components/schemas/TemplateVariable"}}}},"TemplateVariable":{"type":"object","additionalProperties":false,"required":["name","example"],"properties":{"name":{"description":"The variable name, or its number for POSITIONAL templates.","type":"string"},"example":{"type":["string","null"]}}},"EventList":{"type":"object","additionalProperties":false,"required":["object","data","next_cursor"],"properties":{"object":{"const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"next_cursor":{"description":"The id of the last event of this page when more events follow; pass it as after.","type":["string","null"]}}},"Event":{"description":"A webhook event; data reuses the REST resources (message, channel ...). Ignore unknown fields and types.","type":"object","required":["id","object","type","api_version","created_at","team_id","channel","test","data"],"properties":{"id":{"$ref":"#/components/schemas/Id"},"object":{"const":"event"},"type":{"type":"string","enum":["message.received","message.status","message.created","contact.identity_changed","contact.consent_changed","contact.erased","template.updated","channel.status_changed","channel.health_changed","conversation.updated","webhook.test"]},"api_version":{"const":"v1"},"created_at":{"$ref":"#/components/schemas/Timestamp"},"team_id":{"$ref":"#/components/schemas/Id"},"channel":{"anyOf":[{"type":"object","required":["id","type"],"properties":{"id":{"$ref":"#/components/schemas/Id"},"type":{"type":"string"}}},{"type":"null"}]},"test":{"description":"True only for the test events of the webhook page.","type":"boolean"},"data":{"type":"object"}}},"SandboxInboundRequest":{"description":"Staging simulator (F3-23): a message from the contact on a simulated channel.","type":"object","additionalProperties":false,"required":["channel_id","from","content"],"properties":{"channel_id":{"$ref":"#/components/schemas/Id"},"from":{"description":"The sender; phone (E.164), bsuid or both, and an optional profile name.","type":"object","additionalProperties":false,"properties":{"phone":{"type":"string","pattern":"^\\+[1-9][0-9]{4,19}$"},"bsuid":{"type":"string","maxLength":140},"name":{"type":"string","maxLength":128}}},"content":{"description":"{\"type\": \"<type>\", \"<type>\": {...}} in the shape of a received message's content. text: {body};\nimage, video, document: {media_id?, caption?} (document also filename?); audio: {media_id?, voice?};\nsticker: {media_id?, animated?}; location: {latitude, longitude, name?, address?, url?}; contacts:\n{contacts: [{name: {formatted_name, ...}, phones?, emails?, org?, addresses?, urls?, birthday?}]};\ninteractive_reply: {source?: interactive|template, kind?: button|list, id, title, description?};\nreaction: {message_id, emoji}; unsupported: {} or nothing.\n","type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["text","image","video","audio","document","sticker","location","contacts","interactive_reply","reaction","unsupported"]}}},"context":{"description":"The message on this channel that the contact replies to.","type":"object","additionalProperties":false,"required":["message_id"],"properties":{"message_id":{"$ref":"#/components/schemas/Id"}}}}},"SandboxStatusRequest":{"description":"Staging simulator (F3-23): a delivery status for an outbound message on a simulated channel.","type":"object","additionalProperties":false,"required":["message_id","status"],"properties":{"message_id":{"$ref":"#/components/schemas/Id"},"status":{"type":"string","enum":["sent","delivered","read","failed"]},"provider_code":{"description":"Meta's error code, only with failed (default 131026).","type":"integer","minimum":1}}},"SandboxIdentityRotationRequest":{"description":"Staging simulator (F3-23): the contact's new WhatsApp business-scoped user id.","type":"object","additionalProperties":false,"required":["contact_id","new_bsuid"],"properties":{"contact_id":{"$ref":"#/components/schemas/Id"},"new_bsuid":{"type":"string","maxLength":140}}},"SandboxEvent":{"description":"The result of a simulator call (F3-23). outcome: processed; ignored (the normal pipeline skipped the event\non purpose, reason is its code, for example status_stale or rotation_unmatched); failed (the event could\nnot be processed); queued (not processed yet).\n","type":"object","additionalProperties":false,"required":["object","type","outcome","reason","provider_message_id","contact_id","message"],"properties":{"object":{"const":"sandbox_event"},"type":{"type":"string","enum":["inbound","status","identity_rotation","close_window"]},"outcome":{"type":"string","enum":["processed","ignored","failed","queued"]},"reason":{"type":["string","null"]},"provider_message_id":{"type":["string","null"]},"contact_id":{"anyOf":[{"$ref":"#/components/schemas/Id"},{"type":"null"}]},"message":{"anyOf":[{"$ref":"#/components/schemas/Message"},{"type":"null"}]}}}}}}
