{"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 or Telegram 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`. Clients must accept unknown enum values: v1 may add values such as\n  `author.type` `bot` (a message of UPPU Bot) and `campaign` (a template message of a campaign of the\n  company's panel, D4-12), `handling` `bot` (a channel's, and the\n  `conversation.handling` of message.received and message.created events) and a message `status`\n  `expired` (API-09).\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- The API is part of the company's plan. Without it every request gets 403 `api_disabled` with `details`\n  `{\"resource\": \"team\", \"reason\": \"not_in_plan\"}` and no event is produced; UPPU can also stop one key or\n  one channel (`resource` `api_key` or `channel`). A channel keeps its stored `handling` whatever the\n  modules of the plan.\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). Read and resolve conversations, read a contact with its consent records\nand record a consent on its own with POST /consents (API-03, API-02, API-01). A contact also carries the\nteam's notes and the company's contact field values (D4-6; added within v1, read only).\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); /sandbox/action-target stands in for a\ncompany's own API when UPPU Bot's actions are rehearsed (F4-11, musteri-api-v1.md §24). The live API has\nno /sandbox paths.\n\nUPPU Bot in the inbox (F4-09, musteri-api-v1.md §26): an owner switches a channel to UPPU Bot in the panel;\nthe channel object names the bot, and a conversation's handed_over_at follows the bot's handovers, the\nteam's take-overs and the returns to the bot.\n\nHandover (F4-27, musteri-api-v1.md §30; added within v1): on channels your system (handling api) or UPPU\nBot (handling bot) answers, POST /conversations/{id}/handover gives a conversation to the team and POST\n/conversations/{id}/release gives it back; the conversation shows who holds it (held_by) and why it was\nhanded over (handover_reason), and conversation.updated reports every change to the endpoints subscribed\nto it.\n\nStickers, contact cards and the typing indicator (D6-3, musteri-api-v1.md §35; added within v1): POST\n/messages sends content sticker (a WebP uploaded with POST /media and kind sticker) and contacts on WhatsApp\nand Telegram; POST /conversations/{id}/typing shows \"typing…\" on WhatsApp, Messenger and Telegram (not a\nmessage: nothing stored, no event). GET /channels shows them in capabilities (content_types,\ntyping_indicator).\n\nWebhook endpoints through the API (D6-5: API-11, API-15; musteri-api-v1.md §38; added within v1): with the\nwebhooks:manage scope, which no preset has, /webhook-endpoints lists, creates, changes, deletes, tests,\nprobes, activates and disables the endpoints of the panel's Webhooks page and rotates their secrets; the\nsigning secret is in the answer only when it is created. An endpoint can opt in to a weekly test event\n(warning only). POST /media/{id}/link gives a short-lived signed download link that works without the key\n(GET /media/{id}/download). Staging only (API-14): /sandbox/channels/{id}/health and\n/sandbox/templates/{id}/status send channel.health_changed and template.updated like real ones.\n\nContact changes and the signed sign-in (D5-3, D5-2, musteri-api-v1.md §39; added within v1): the\ncontact.updated event reports a contact's new name, identities, contact fields, merges and their undo to the\nendpoints subscribed to it. A web chat channel can recognise your website's signed-in users: your server\nsigns a short-lived token with the channel's sign-in secret, the page hands it to the chat, and the contact\ngets an identity of type external (your user id); POST /contacts/lookup finds it by that id.\n\nTemplate management and reports (D6-6, musteri-api-v1.md §41; added within v1): with the templates:write\nscope, which no preset has, POST /templates creates a WhatsApp template with the rules of the panel's template\npage and DELETE /templates/{id} deletes it in one language. With the reports:read scope, also in no preset,\nGET /reports/summary and GET /reports/daily return the figures of the panel's Reports page: counts only, for at\nmost 90 days, per channel, agent or group.\n\nMessage time to live (API-09, musteri-api-v1.md §42; added within v1, accepted once UPPU switches it on;\nuntil then ttl gets 422 validation_failed with code unknown_field): POST /messages takes an optional ttl in\nseconds. A message that UPPU has not handed to the provider (Meta, Telegram) when the ttl runs out is not\nsent: its status becomes expired (error.code ttl_expired, a message.status event). If an earlier attempt\nreached the provider and it answered with an error, UPPU stops retrying and the message ends failed with\nthat error instead. A message already handed over is never recalled.\n\nThe service stop for billing (odeme-plani.md OD-07d, musteri-api-v1.md §44; added within v1): when a\ncompany's payment is long overdue, or its subscription ended, its service stops at 10:00 Turkey time on a\nworking day, at least 72 hours after a warning. From the warning on, every response to a caller that\npasses the key's IP allow-list carries UPPU-Service-Stop-At (the stop instant, RFC 3339 UTC; never on\ninvalid_api_key, api_disabled or ip_not_allowed) and every endpoint that subscribes to it (on by default)\nreceives account.service_stop_scheduled. While stopped, every request (reads and the signed media download\nincluded) gets 403 service_stopped with details.reason billing_overdue or subscription_ended, a queued send fails with\nerror.code service_stopped, no webhook event is produced and due deliveries end dead (resend them after\nthe resume). Incoming messages are still recorded. account.service_resumed follows the resume.\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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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. The API never masks text; a message older than the short retention of the special-category data protection may already be masked in storage (see the Message schema).","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The message","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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. The API never masks text; a\nmessage older than the short retention of the special-category data protection may already be masked\nin storage (see the Message schema).\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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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. Telegram has no reply window: a bot can write to a person who\nwrote to it first (their telegram identity exists) until they block it (422 recipient_unreachable);\nTelegram reports only sent (no delivered or read; see capabilities.delivery_statuses). Session content (text,\ninteractive, reaction, location) needs the 24-hour window on WhatsApp, Messenger and Instagram; an approved\ntemplate 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. On Messenger and Instagram the person's reaction counts like a message\n(added within v1; removing a reaction does not). Meta can deliver old messages late; such a message\narrives with the window already closed (service_window.open false in message.received) and a session\nmessage 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 (on Telegram the file travels inside the\nsend call itself). Added within v1: stickers (by media_id only) and contact cards on WhatsApp and Telegram\n(see StickerContent and ContactsContent); other channels get 422 content_not_supported_on_channel. Show\nthe person that an answer is coming with POST /conversations/{id}/typing.\n\nE-mail channels (added within v1): text and media (image, video, audio, document; the file travels as an\nattachment of the e-mail). Address the person by contact_id or by their email identity (the address\nthey wrote from; they must have written to the channel first); a phone gets 422 recipient_invalid. There\nis no reply window. UPPU sends from the channel's sending address, DKIM-signed with the key of its\nverified domain, as a reply in an e-mail thread of the person (subject \"Re: ...\", In-Reply-To and\nReferences): by default the conversation's last received e-mail, or the received e-mail you name in\ncontext.message_id (a person can have several threads in one open conversation; another kind of\nmessage gets 422 reply_target_invalid with details.reason not_an_email). Without a sending address or a\nverified domain the send gets 409 channel_unavailable (details.reason no_sender or sender_unverified;\nemail_off while UPPU's e-mail channel is switched off). E-mail reports only sent (UPPU's mail server took\nthe message) and failed (it could not, or the recipient's server bounced it: error.code\nrecipient_unreachable).\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\nOptional ttl (added within v1, accepted once UPPU switches it on, until then 422 validation_failed with\ncode unknown_field): the message's time to live, whole seconds from 10 to 86400 (24 hours). If UPPU has not\nhanded the message to the provider by created_at + ttl (it waited in the queue, for the rate limit, for a\nretry, or the channel could not send), UPPU stops trying: status expired, error.code ttl_expired, a\nmessage.status event with status expired; nothing reaches Meta or Telegram. If an earlier attempt reached\nthe provider and it answered with an error (for example its rate limit or a temporary error), the ttl only\nends UPPU's retries: status failed with that error (for example provider_rate_limited or\nprovider_unavailable), as when the retries run out without a ttl. The check runs before every\nattempt and every minute, so expired usually follows the deadline within about a minute; when thousands of\nmessages run out at once it can take a few minutes longer, but nothing is sent after the deadline. A message\nhanded over before the deadline is sent and never recalled (Meta's own retry period for an undelivered\nmessage is separate and set on the template, not per message). Without ttl nothing changes. On web chat a\nmessage is delivered when it is written, so ttl has no effect there. ttl is part of the body: the same\nIdempotency-Key with another ttl gets 409 idempotency_key_reused.\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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"},"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, Messenger, Instagram, Telegram) or sent (web chat)","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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 a WhatsApp message the contact sent. Read receipts exist only on\nWhatsApp: a message on any other channel (web chat, Messenger, Instagram, Telegram) is 422\ncontent_not_supported_on_channel with details.type read_receipt and details.channel_type. On a WhatsApp\nchannel, any other target is 422 not_inbound with details.reason: outbound (sent by the business),\nreaction (WhatsApp has no read receipt for reactions), system_notice (a WhatsApp notice such as a number\nchange), not_a_message (an event stored as an unsupported message, such as request_welcome),\nunsupported_message (a message Meta delivered with its own unsupported type,\ncontent.unsupported.provider_type unsupported; Meta does not accept a read receipt for it) or\nno_provider_id. These never reach Meta. not_inbound is final and not retryable: a repeat gets the same\nanswer and nothing is queued. A client that marks every received message as read should treat it as\nnothing to mark and accept reason values it does not know; the values listed never change. The 24-hour\nwindow applies. Returns 202 the first time and 200 with the current state on a repeat. Send an empty\nbody or {}.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The read receipt was already requested","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadReceipt"}}}},"202":{"description":"The read receipt is queued","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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"}}}},"/contacts/{id}":{"get":{"operationId":"getContact","summary":"Get a contact","description":"Needs the contacts:read scope. The same object as POST /contacts/lookup: the identities on the key's\nchannels (superseded ones with current false), the reply window per WhatsApp, Messenger and Instagram\nchannel and the current consent per channel type and purpose. A contact without an identity or a\nconversation on the key's channels is 404: a channel-bound key sees only the contacts of its channel.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The contact","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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"}}}},"/contacts/{id}/consents":{"get":{"operationId":"listContactConsents","summary":"List the consent records of a contact","description":"Needs the contacts:read scope. The contact's consent ledger, newest record first unless order=asc: every\ngrant and withdrawal, also the automatic ones (the person's STOP message, Meta's marketing opt-out) and the\nones the company recorded in the UPPU panel or imported from a file. Records are never changed; the current\nstate per channel type and purpose is the record with the latest collected_at (a withdrawal wins a tie),\nwhich the contact's consents summary shows. A record added later can be older than the current one: the\nnewest record (and the latest contact.consent_changed event) is not always the current state.\ncreated_after and created_before filter on recorded_at. A contact the key cannot see is 404.\n","parameters":[{"$ref":"#/components/parameters/Id"},{"$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 consent records","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentList"}}}},"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"}}}},"/consents":{"post":{"operationId":"recordConsent","summary":"Record a consent","description":"Needs the consents:write scope. Adds a record to the contact's consent ledger (records are never changed): a\ngrant or a withdrawal of the marketing or utility purpose, on a WhatsApp channel (another channel type gets\n422 content_not_supported_on_channel). Address the person by contact_id (the contact needs an identity on\nthe channel), by phone (E.164; the channel's contact with that phone, otherwise a new unverified contact) or\nby an identity that already exists on the channel. source_text (the exact text the person agreed to) and\nsource_ref (your reference to the evidence, no personal data) are required. collected_at is when the person\ngave or withdrew the consent: now if left out, never in the future. A grant without collected_at gets 422\nvalidation_failed (collected_at) while the latest record is a withdrawal: send the time the new consent was\ncollected. Template sends read the record with the latest collected_at (a withdrawal wins a tie). Every\nnew record emits contact.consent_changed; the event reports the record that was written, not the current\nstate (a grant older than a newer withdrawal is recorded and sent as granted while the withdrawal stays in\neffect): read the current state from the contact's consents summary (GET /contacts/{id}). An optional\nIdempotency-Key (company scope): the same key with the same consent (contact, purpose, state, source_text\nand source_ref) returns 200 with the first record and Idempotent-Replayed: true, another consent with the\nsame key gets 409 idempotency_key_reused. Without a key every request adds a record.\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":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentRequest"}}}},"responses":{"200":{"description":"A replay of an earlier record with the same Idempotency-Key and consent","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"},"Idempotent-Replayed":{"description":"Always true on a replay.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Consent"}}}},"201":{"description":"The recorded consent","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Consent"}}}},"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"}}}},"/conversations":{"get":{"operationId":"listConversations","summary":"List conversations","description":"Needs the messages:read scope. Conversations of the key's channels, newest first unless order=asc. Filter\nby contact_id, channel_id and status (open or resolved); an id the key cannot see is 404. created_after and\ncreated_before filter on opened_at. The cursor carries the order; repeat the filters and the time filters\non every page. A conversation resolved or reopened between two pages keeps the cursor valid: the next page\ncontinues after it, so you can resolve the conversations of a page before asking for the next one.\n","parameters":[{"name":"contact_id","in":"query","schema":{"$ref":"#/components/schemas/Id"}},{"name":"channel_id","in":"query","schema":{"$ref":"#/components/schemas/Id"}},{"name":"status","in":"query","schema":{"type":"string","enum":["open","resolved"]}},{"$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 conversations","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationList"}}}},"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"}}}},"/conversations/{id}":{"get":{"operationId":"getConversation","summary":"Get a conversation","description":"Needs the messages:read scope. A conversation outside the key's channels, or past retention, is 404. The\nassignee is never shown, only whether the conversation is assigned. service_window is the contact's current\n24-hour reply window on the conversation's channel (null on web chat, Telegram and e-mail).\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The conversation","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"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"}}}},"/conversations/{id}/resolve":{"post":{"operationId":"resolveConversation","summary":"Resolve a conversation","description":"Needs the conversations:write scope. Send an empty body or {}. Resolves an open conversation the way the team\ninbox does (the same audit record, with your key as the actor); a conversation that is already resolved\nstays as it is. Both answer 200 with the current conversation, so a retry is safe. The person's next\nmessage opens a new conversation. Endpoints subscribed to conversation.updated get it with change resolved\n(added within v1). A read-only company gets 403 tenant_read_only.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The conversation, resolved","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"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"}}}},"/conversations/{id}/handover":{"post":{"operationId":"handOverConversation","summary":"Hand a conversation over to the team","description":"Needs the conversations:write scope; added within v1. On a channel your system (handling api) or UPPU\nBot (handling bot) answers, gives the conversation to the team: held_by becomes team, handed_over_at is set,\nhandover_reason is api, and the conversation joins the team inbox's normal views (Unassigned, waiting\nfor a reply). The team gets UPPU's usual handover help: the channel's automatic assignment when it is on,\nand an e-mail when no one replies within 10 minutes. On a bot channel UPPU Bot stops at once and sends\nnothing more in the conversation. Nothing is sent to the person; send your own message with POST\n/messages if you want to. Send an empty body, {} or {reason, note}: reason is a short code of your own\n(at most 64 characters: letters, digits, dot, underscore, colon, hyphen; no personal data), note a text\nfor the team (at most 1000 characters). Both appear only in the team inbox, next to the handover; the\nAPI and the webhooks never return them. A conversation that is already with the team stays as it is,\nand nothing is written again. Both answer 200 with the current conversation, so a retry is safe.\nEndpoints subscribed to conversation.updated get change handed_over once. An inbox channel gets 422\nhandling_not_supported (details.handling), and so does a company whose plan has no messaging panel\n(details.reason not_in_plan: there is no team to hand over to); a resolved conversation gets 409\nnot_held. A read-only company gets 403 tenant_read_only.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandoverRequest"}}}},"responses":{"200":{"description":"The conversation, with the team","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"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"}}}},"/conversations/{id}/release":{"post":{"operationId":"releaseConversation","summary":"Give a conversation back from the team","description":"Needs the conversations:write scope; added within v1. Send an empty body or {}. Gives a conversation\nthe team holds back to the automation of its channel: on an api channel to your system (held_by api, the\nconversation leaves the team's views for the inbox's API view), on a bot channel to UPPU Bot (held_by bot,\nthe way \"Return to bot\" in the inbox does: UPPU Bot answers only the person's next message, never the\nolder ones). handed_over_at and handover_reason become null and the assignment is removed. A\nconversation the automation already holds stays as it is, and nothing is written again. Both answer 200\nwith the current conversation, so a retry is safe. Endpoints subscribed to conversation.updated get\nchange released (api channel) or returned_to_bot (bot channel), after unassigned when someone was\nassigned. On a bot channel UPPU Bot must be able to take the conversation, otherwise 409 bot_not_ready\nwith details.reason bot_unavailable (UPPU Bot is off on the channel or for the company), bot_busy (it\nis still finishing its turn from before the handover; retry in a few seconds) or reply_first (the\nperson's last message has no reply yet, from the team or your system; reply first). An inbox channel\ngets 422 handling_not_supported, a resolved conversation 409 not_handed_over, a read-only company 403\ntenant_read_only.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The conversation, with the automation","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"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"}}}},"/conversations/{id}/typing":{"post":{"operationId":"sendTypingIndicator","summary":"Show a typing indicator to the contact","description":"Needs the messages:send scope; added within v1. Shows \"typing…\" to the contact of the conversation while\nyour system prepares an answer. Send an empty body or {}; no Idempotency-Key. A typing indicator is not a\nmessage: nothing is stored, nothing is billed and no webhook event follows; it ends when your message\narrives or after a short time set by the network (WhatsApp 25 seconds, Telegram 5 seconds), and cannot\nbe switched off early. Call it again for a longer answer. GET /channels tells you where it\nworks (capabilities.typing_indicator): WhatsApp, Messenger and Telegram; any other channel (web chat,\nInstagram) gets 422 content_not_supported_on_channel with details.type typing_indicator and\ndetails.channel_type.\n\nOn WhatsApp the indicator travels with Meta's mark-as-read call: it marks the newest message the contact\nsent in this conversation (and the earlier ones) as read, the same target rules as POST\n/messages/{id}/read; message_id in the answer names that message. Without such a message from the last 30\ndays the answer is 422 not_inbound with details.reason no_inbound_message. The rules of a send apply:\nthe 24-hour window on WhatsApp and Messenger (422 window_closed), a paused or unavailable channel (409),\na shadow channel (403 shadow_mode), a blocked contact (422 contact_blocked) and, on Telegram, a person\nwho blocked the bot (422 recipient_unreachable). At most one indicator per conversation every few\nseconds (429 rate_limited with details.scope conversation and Retry-After) and a per-key limit of its\nown. The answer is 202 with state queued; the outcome is not reported. A read-only company gets 403\ntenant_read_only.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"202":{"description":"The typing indicator is queued","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TypingIndicator"}}}},"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"}}}},"/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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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"}}},"post":{"operationId":"createTemplate","summary":"Create a WhatsApp template","description":"Needs the templates:write scope (added within v1). No preset includes it and existing keys do not have it:\nthe owner adds it to a key on the developer page on purpose. Creates the template in the WhatsApp Business\naccount of the channel with the rules of the panel's template page: name of lowercase\nletters, digits and underscores; language tr or en; category UTILITY or MARKETING; an optional text header\nwith at most one variable; the body with numbered ({{1}}, {{2}} ...) or named ({{first_name}}) variables and\nan example for each; an optional footer; buttons: up to 3 quick replies, 2 URL buttons (a variable only as\n{{1}} at the end of the address, with an example of that end) and 1 phone number, the quick replies next to\neach other. A header with an image, a video or a document (added within v1) takes a sample file: the\nmedia_id of an upload of the same channel made with POST /media, {\"type\": \"image\", \"image\": {\"media_id\":\n\"...\"}}; a JPEG or PNG image up to 5 MB, an MP4 video up to 16 MB, a PDF document up to 25 MB (otherwise 422\nvalidation_failed with code mime_not_allowed, or 413 media_too_large); an upload of another channel, an\nexpired one or one past its reuse period gets 422 media_unavailable (details.reason). UPPU uploads the\nsample to Meta with Meta's Resumable Upload before it creates the template; Meta's refusal of the sample is\n422 validation_failed with field header.<type>.media_id, code sample_refused and Meta's code in\nprovider_error, and no template is created. The upload and the create run within the time of the request\n(about 26 seconds): when the upload leaves no time for the create, the answer is 503 provider_unavailable\nwith details.reason unreachable and nothing is created (retry, or use a smaller sample). A retry after\nunconfirmed with the same Idempotency-Key and template gets the template Meta created even when the\nsample's upload is no longer usable. A header with a location cannot be created here (422\nvalidation_failed, field header.type, code not_supported). A field that breaks a rule gets 422\nvalidation_failed with the field's path (header.text, body.examples.1, buttons.0.url) and code invalid.\nThe answer is 201 with the template, usually status PENDING: Meta reviews it, mostly within 24 hours, and\ntemplate.updated reports the result. The same name and language again, or a name whose every language was\ndeleted within 30 days, gets 422 validation_failed with field name (code exists or name_blocked); Meta allows\n100 new templates per hour for a WhatsApp account (429 rate_limited, details.reason templates_per_hour, with\nRetry-After). Meta's refusal of the template is 422 validation_failed with field template, code meta_refused\nand Meta's code in provider_error. The call to Meta runs inside the request: one template create or delete at\na time per company (429 rate_limited with details.reason concurrent_writes otherwise) and at most 10 per\nminute per company. When Meta's own rate limit is reached (for example templates created outside UPPU) the\nanswer is 429 rate_limited with details.reason provider_rate_limited, Retry-After and Meta's code in\nprovider_error. When Meta cannot be reached the answer is 503 provider_unavailable with details.reason\nunreachable (nothing was created); when Meta does not answer in time or answers with a temporary error,\nunconfirmed (the template may have been created). A retry with the same Idempotency-Key and template is safe:\nUPPU looks at Meta again and answers 200 with the template and Idempotent-Replayed: true when the earlier\nrequest created it, creates it when it did not, and answers unconfirmed again when Meta still cannot be read.\nWithout a key, read GET /templates?name=&language= after a minute before you retry. A channel that is not\nWhatsApp gets 422 content_not_supported_on_channel (details.type template); a\nkey bound to one channel that names another channel 403 channel_not_allowed; a channel without a usable\nWhatsApp connection 409 channel_unavailable (details.reason; credentials also when Meta refuses the account's\naccess token, with Meta's code in provider_error); a number in shadow mode 403 shadow_mode. An optional\nIdempotency-Key (company scope): the same key with the same template answers 200 with the template as it is\nnow and Idempotent-Replayed: true; another template with the same key gets 409 idempotency_key_reused. A\nread-only company gets 403 tenant_read_only.\n","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional; letters, digits, dot, underscore, colon or hyphen, at most 64. Kept only by an accepted create; after 503 provider_unavailable with details.reason unconfirmed a retry with the same key and template looks at Meta again (for 24 hours).","schema":{"type":"string","pattern":"^[A-Za-z0-9._:-]{1,64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateCreateRequest"}}}},"responses":{"200":{"description":"A replay of an earlier request with the same Idempotency-Key and template, also when the earlier request got 503 unconfirmed and created the template","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"},"Idempotent-Replayed":{"description":"Always true on a replay.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"201":{"description":"The new template, sent to Meta for review","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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"},"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"}}}},"/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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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"}}},"delete":{"operationId":"deleteTemplate","summary":"Delete a WhatsApp template in one language","description":"Needs the templates:write scope (added within v1; in no preset). Deletes this template, in this language\nonly: UPPU sends Meta the template's id together with its name, so the other languages of the name stay.\nDelete every language one by one to remove a name; once every language of a name is deleted, Meta lets you\nuse the name again only after 30 days (a create with it gets 422 validation_failed, field name, code\nname_blocked). The template stays readable with status DELETED. Answers 200 with {object, id, name,\nlanguage, deleted}; a template that is already deleted answers the same without calling Meta. Send an empty\nbody or {}. Meta's refusal is 422 validation_failed with field template, code meta_refused and Meta's code in\nprovider_error; Meta's rate limit (429 provider_rate_limited), 503 provider_unavailable (unconfirmed: the\ntemplate may have been deleted), 409 channel_unavailable, 403 shadow_mode and the limits are those of\ncreating. A template outside the key's channels is 404. A read-only company gets 403 tenant_read_only.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The template is deleted in this language","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletedTemplate"}}}},"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"}}}},"/reports/summary":{"get":{"operationId":"getReportSummary","summary":"Get the report of a period","description":"Needs the reports:read scope (added within v1; in no preset, existing keys do not have it). The figures of\nthe panel's Reports page, counted every night: conversations the contacts opened and those that reached the\nteam, the ones left unanswered on their day, the team's replies, first response and resolution times,\nresolutions and reopenings, the open conversations at the end of the period, SLA targets met and missed,\ntopics and tags; for the whole company also per channel, agent and group. Counts only: no contact,\nconversation or message. The period is days=7, 30 or 90 finished days (30 by default) or from and to\n(calendar days in the company's time zone, at most 90 days); it ends on the last day the nightly count has\nfinished (period.to and period.counted_through; period.pending is true while yesterday is still being\ncounted), so a to in the future or not counted yet is moved back. Any other value of the period gets 422\nvalidation_failed (for example a period longer than 90 days, field from, code max). One of channel_id,\nmember_id or group_id narrows the scope; an id the key cannot see is 404. A key bound to one channel reads\nonly its channel (member_id and group_id get 403 channel_not_allowed). A channel UPPU stopped for the API gets\n403 api_disabled, and its counts are one row with hidden true among the per-channel rows; the totals and the\nagent and group rows count every channel of the company. Durations are given per bucket (1m,\n5m, 15m, 30m, 1h, 4h, 8h, 24h, 72h and over for first responses; 15m, 1h, 4h, 8h, 24h, 72h, 7d and over for\nresolutions: at most that long); median and p90 name the bucket in which half and nine tenths of the\nsamples fall. The report's tier (tier, features) is that of the panel: with basic, agents, groups, SLA,\ntopics and tags are null. Reports are part of the messaging panel: a company whose plan does not include it\ngets 403 api_disabled with details.resource reports and details.reason not_in_plan. At most 2 requests per\nsecond per key, together with GET /reports/daily.\n","parameters":[{"$ref":"#/components/parameters/ReportDays"},{"$ref":"#/components/parameters/ReportFrom"},{"$ref":"#/components/parameters/ReportTo"},{"$ref":"#/components/parameters/ReportChannel"},{"$ref":"#/components/parameters/ReportMember"},{"$ref":"#/components/parameters/ReportGroup"}],"responses":{"200":{"description":"The report of the period and scope","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportSummary"}}}},"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"}}}},"/reports/daily":{"get":{"operationId":"listReportDays","summary":"List the days of a report","description":"Needs the reports:read scope. The figures of each day of the period and scope (the period, the scope and the\nrules of GET /reports/summary), newest day first unless order=asc; a day without figures has zeros, and\ncounted says whether the nightly count has finished that day. open and open_unassigned are the open\nconversations at the end of the day. Pass next_cursor as cursor for the next page and repeat the period and\nthe scope on every page.\n","parameters":[{"$ref":"#/components/parameters/ReportDays"},{"$ref":"#/components/parameters/ReportFrom"},{"$ref":"#/components/parameters/ReportTo"},{"$ref":"#/components/parameters/ReportChannel"},{"$ref":"#/components/parameters/ReportMember"},{"$ref":"#/components/parameters/ReportGroup"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Order"}],"responses":{"200":{"description":"A page of days","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportDayList"}}}},"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 and, added within v1, account.service_stop_scheduled and account.service_resumed) need\nan 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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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 two fields: channel_id (a WhatsApp, Messenger, Instagram, Telegram or, added within v1,\nweb chat channel the key can use; an Instagram channel while UPPU's Instagram outbound media is off gets\n422 content_not_supported_on_channel; the channel's capabilities.content_types show whether media can be\nsent) and file; added within v1, an optional third field kind (see Stickers below). Any other field gets\n422 validation_failed with code unknown_field. At most 25 MB; the type is read from the content and must\nbe one the channel's type can send, otherwise 415 unsupported_media_type. The lists below are UPPU's: for\nWhatsApp and Instagram they stay within the types and sizes in Meta's documentation, which gives\nMessenger's sizes but no list of Messenger types. An MP4 container that reads as video is audio when the\nfile 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. UPPU checks the type\n  and the size only. WhatsApp also requires 8-bit RGB or RGBA images, H.264 video with AAC audio (at most\n  one audio stream) and OGG audio in Opus, mono; it can refuse a file that breaks these rules when the\n  message is sent (message.status failed, for example with media_rejected).\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- Telegram (added within v1): image/jpeg, image/png, image/webp up to 10 MB (Telegram's photo limit);\n  video/mp4; audio/mpeg, audio/mp4, audio/aac, audio/ogg (sent as a voice note); PDF, plain text and Office\n  documents; all up to 25 MB (UPPU's cap, below Telegram's 50 MB).\n- Web chat (added within v1): image/jpeg, image/png, image/webp, image/gif up to 10 MB; video/mp4,\n  video/webm, video/quicktime; audio/mpeg, audio/mp4, audio/aac, audio/ogg, audio/wav; PDF, plain text,\n  Office and OpenDocument documents; all up to 25 MB. UPPU removes an image's metadata (EXIF with the\n  location, XMP, comments) without re-encoding it and keeps its orientation; size and sha256 are those of\n  the stored file. The visitor gets the file in the chat widget through a short-lived link.\n- Stickers (added within v1): send the field kind with the value sticker to upload a sticker for\n  content.sticker.media_id on WhatsApp and Telegram; another channel gets 422\n  content_not_supported_on_channel with details.type sticker, another value of kind 422 validation_failed\n  (code in). The file must be a WebP of exactly 512×512 pixels, static up to 100 KB or animated up to 500\n  KB (WhatsApp's sizes; Telegram: static only). The upload's kind is sticker and the rules are checked\n  here, in this order: another type gets 415 unsupported_media_type; then 422 validation_failed for the\n  file with code sticker_unreadable when UPPU cannot read the WebP header, code\n  sticker_animated_not_allowed for an animated file on a channel that sends static stickers only\n  (whatever its size), 413 media_too_large for a file over the limit of its own kind, static or\n  animated as read from the header (details.kind sticker, max_bytes of that limit, animated; a static\n  file over 500 KB gets the static 100 KB), and code sticker_dimensions for other dimensions. Without\n  kind an upload is checked as before: on WhatsApp a WebP gets 415 unsupported_media_type; on Telegram a\n  WebP is a photo (kind image) that can also be sent as a sticker, with the sticker rules checked in the\n  same order when it is sent.\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|sticker}.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"},"kind":{"description":"Optional, added within v1: sticker uploads the file as a sticker (WhatsApp and Telegram). It is part of the request for the Idempotency-Key.","type":"string","enum":["sticker"]}}}}}},"responses":{"200":{"description":"A replay of an earlier upload with the same Idempotency-Key and file","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"},"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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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.\nTelegram (added within v1): every file of an inbound message (photo, video, video note, animation, voice\nnote, audio, document, sticker) is downloaded by UPPU with the bot's token; neither Telegram's file id nor\nits file address is ever exposed. A file over 20 MB (Telegram's bot download limit) ends as failed with\ntoo_large; a file Telegram does not know any more ends with provider_refused. Temporary errors (Telegram\nunreachable, 5xx, 429, a rejected bot token until it is renewed) are retried in ten steps from 30 seconds\nto 24 hours (about 52 hours with today's schedule), then once a day; the last attempt is made when about\nan hour or less of the 7 days after receipt remains, even when earlier attempts were delayed (usually the\nshortened step exactly an hour before; only a file whose first attempt falls within that last hour is\nretried within it, on the common schedule while the next step fits), so a token renewed before the last\nattempt still brings the file, at most a day later. If the last attempt fails too, the file ends as failed with\ndownload_failed or no_token; a file UPPU could not try at all within those 7 days ends with\ndownload_failed.\nWeb chat (added within v1): a file the visitor sends from the chat widget is stored as it arrives (ready\nat once; failed with quota_exceeded when the company's storage is full, the message is still written).\nThe types and sizes are those of POST /media for web chat; an image's metadata is removed in the same way.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The media file","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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. To hand a\nfile over without your key, create a short-lived signed link (POST /media/{id}/link; added within v1).\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The file","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"},"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"}}}},"/media/{id}/link":{"post":{"operationId":"createMediaLink","summary":"Create a signed download link","description":"Needs the media:read scope (added within v1). Returns a short-lived signed address that downloads the file\nwithout your API key, for example to hand a file to a browser or to another system of yours. Send an empty\nbody or {}. The link works until expires_at (a few minutes; 5 today) and for at most max_uses requests,\nRange requests included. Only the expiry revokes a link: it cannot be cancelled. It is signed for this file of\nthis company only and opens no other file, but anyone who has it can download the file until it expires:\nshare it like the file itself. The file must be ready: 409 media_not_ready while it downloads, 422\nmedia_unavailable when it failed, 410 media_expired after retention. Issuing a link counts as one request\nagainst the per-second limit of GET /media/{id}/content and needs room for one download of the whole file in\nthe key's hourly byte budget (otherwise 429 rate_limited with details.reason bytes); it charges no bytes.\nEvery download through the link then counts against the hourly byte budget of this key, as GET\n/media/{id}/content does. A read-only company can issue links too (it is a read).\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The signed link","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaLink"}}}},"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"}}}},"/media/{id}/download":{"get":{"operationId":"downloadMediaLink","summary":"Download a file through a signed link","security":[],"description":"No API key (added within v1): the query string of the url that POST /media/{id}/link returned authorizes the\nrequest; use that url as it is. Returns the bytes with the headers of GET /media/{id}/content\n(Content-Disposition: attachment, X-Content-Type-Options: nosniff; one Range per request). A link that is\nnot valid, or a link for another file, company or key, answers 404 not_found with details {\"resource\":\n\"media_link\", \"reason\": \"invalid\"}; an expired link the same with reason expired. A link used more than\nmax_uses times answers 429 rate_limited with details.reason link_uses until it expires. Every download\ncounts against the hourly byte budget of the key that issued the link (the budget of GET\n/media/{id}/content; a Range request with the bytes it asks for); when it is used up the answer is 429\nrate_limited with details.reason bytes and Retry-After, and the refused request does not use up the link. A\nfile that is no longer available answers 410 media_expired or 422 media_unavailable; 403 api_disabled when\nthe company's plan no longer includes the API or UPPU stopped the channel. Browsers can open the link; no\nCORS headers are sent.\n","parameters":[{"$ref":"#/components/parameters/Id"},{"name":"team","in":"query","required":true,"description":"The company the link was signed for.","schema":{"$ref":"#/components/schemas/Id"}},{"name":"key_id","in":"query","required":true,"description":"The id of the API key that issued the link (not the key itself); its byte budget pays the downloads.","schema":{"$ref":"#/components/schemas/Id"}},{"name":"expires","in":"query","required":true,"description":"When the link expires, in Unix seconds.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"}},{"name":"signature","in":"query","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{43}$"}}],"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"}}}},"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"}}}},"/webhook-endpoints":{"get":{"operationId":"listWebhookEndpoints","summary":"List webhook endpoints","description":"Needs the webhooks:manage scope (added within v1). No preset includes it and existing keys do not have it:\nthe owner adds it to a key on the developer page on purpose. The company's webhook endpoints, the same ones\nthe panel's Webhooks page shows, newest first unless order=asc. A key bound to one channel sees only the\nendpoints whose channel filter is that channel. The full address and the signing secret are never listed:\nurl is masked (scheme, host and /...) and secret is null.\n","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 webhook endpoints","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointList"}}}},"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"}}},"post":{"operationId":"createWebhookEndpoint","summary":"Create a webhook endpoint","description":"Needs the webhooks:manage scope. The rules of the panel's Webhooks page: https on port 443 only and a public\nhost whose every address is public; a refused address gets 422 validation_failed with field url and the\nreason as its code (for example not_https, port_not_allowed, credentials_in_url, dns_unresolved, private,\nloopback or datacentre). events: at least one type a webhook endpoint can subscribe to; left out, the types that\nare on by default (message.received, message.status, contact.identity_changed, contact.erased,\nchannel.status_changed, contact.reachability_changed). channel_id: null for every channel; left out, every\nchannel, or the key's own channel for a key bound to one channel (which gets 403 channel_not_allowed for\nnull or another channel). ordering best_effort (default) or strict; max_in_flight 1 to 32 (default 8);\nweekly_test (default false). A company has at most 5 endpoints: one more gets 409 plan_limit_reached with\ndetails resource webhook_endpoint, max and used. The new endpoint is pending_verification and gets no events\nuntil it passes a test event and the wrong-signature probe and is activated (POST /webhook-endpoints/{id}/test,\n/probe and /activate). The answer carries the signing secret in secret, only this once: store it on your\nserver. An optional Idempotency-Key (company scope): the same key with the same body answers 200 with the\nendpoint, Idempotent-Replayed: true and secret null (if you lost the first answer, rotate the secret); another\nbody with the same key gets 409 idempotency_key_reused. A read-only company gets 403 tenant_read_only. The\naddress is looked up in DNS during the request, one at a time per company together with test events and\nprobes: a create that would have to wait gets 429 rate_limited with details.reason concurrent_checks and\nRetry-After, and nothing is created.\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":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointCreateRequest"}}}},"responses":{"200":{"description":"A replay of an earlier request with the same Idempotency-Key and body (secret is null)","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"},"Idempotent-Replayed":{"description":"Always true on a replay.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"201":{"description":"The new endpoint with its signing secret (shown only here)","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"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"}}}},"/webhook-endpoints/{id}":{"get":{"operationId":"getWebhookEndpoint","summary":"Get a webhook endpoint","description":"Needs the webhooks:manage scope. An endpoint of another company, or outside the channel of a key bound to one\nchannel, is 404. verification says whether a test event and the wrong-signature probe passed within the last\nhour (the conditions of activation); weekly_test is the optional weekly test event.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The endpoint","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"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"}}},"patch":{"operationId":"updateWebhookEndpoint","summary":"Change a webhook endpoint","description":"Needs the webhooks:manage scope. Changes only the fields you send, with the rules of creating; an empty body\nchanges nothing. A new url sends the endpoint back to pending_verification (its events wait) until it passes\nthe test event and the wrong-signature probe again and is activated; a disabled endpoint stays disabled. A key\nbound to one channel cannot remove the channel filter or move it to another channel (403\nchannel_not_allowed). A read-only company gets 403 tenant_read_only. A body with url is looked up in DNS as\non create, one at a time per company together with test events and probes (429 rate_limited with\ndetails.reason concurrent_checks otherwise, and nothing changes); a body without url is not.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointUpdateRequest"}}}},"responses":{"200":{"description":"The endpoint as changed","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"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"}}},"delete":{"operationId":"deleteWebhookEndpoint","summary":"Delete a webhook endpoint","description":"Needs the webhooks:manage scope. Deletes the endpoint and its deliveries; the events stay available for the\nother endpoints and for GET /events. Answers 200 with {object, id, deleted}. A read-only company gets 403\ntenant_read_only (the panel can still delete it).\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The endpoint is deleted","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletedWebhookEndpoint"}}}},"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"}}}},"/webhook-endpoints/{id}/secret":{"post":{"operationId":"rotateWebhookEndpointSecret","summary":"Rotate the signing secret","description":"Needs the webhooks:manage scope. Send an empty body or {}. Creates a new signing secret and returns it in secret,\nonly in this answer: no endpoint shows a secret again. The secret it replaces keeps signing for 24 hours\n(webhook-signature then carries two v1 values; secret_rotating_until says until when): switch your receiver\nto the new secret within that time. A rotation keeps only the secret it replaces, so rotate again only after\nyour receiver uses the newest secret.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The endpoint with its new signing secret (shown only here)","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"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"}}}},"/webhook-endpoints/{id}/test":{"post":{"operationId":"testWebhookEndpoint","summary":"Send a test event to a webhook endpoint","description":"Needs the webhooks:manage scope. Sends a sample event of type (webhook.test if left out) to the endpoint now,\nwith the endpoint's signature and the published egress IP, test true in the body, and answers with the\nresult: passed (your server answered 2xx), status, latency_ms, the first characters of the answer and the\nerror, if any. The request waits for your server (at most 10 seconds). Tests and probes together: at most 10\nper minute for all keys of the company, one at a time per company and only a few at once for the whole\nservice. A check that would have to wait is not queued: it answers 429 rate_limited with details.reason\nconcurrent_checks and Retry-After, nothing is sent and it is not counted; send it again then. A passing test\nalso clears the warning of a failed weekly test. A test that passed within the last hour is a condition of\nactivation. The event is not stored and not listed by GET /events.\n","parameters":[{"$ref":"#/components/parameters/Id"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointTestRequest"}}}},"responses":{"200":{"description":"The result of the test event","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointCheck"}}}},"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"}}}},"/webhook-endpoints/{id}/probe":{"post":{"operationId":"probeWebhookEndpoint","summary":"Send the wrong-signature probe","description":"Needs the webhooks:manage scope. Send an empty body or {}. Sends webhook.test signed with a wrong secret. It\npasses when your server refuses it (any HTTP answer other than 2xx): that shows your receiver checks the\nsignature. A 2xx answer or a connection error does not pass. A new endpoint, and an endpoint whose address\nchanged, needs a passing probe within the last hour to be activated. Counted with the tests: at most 10 per\nminute for the company, one at a time (429 rate_limited with details.reason concurrent_checks otherwise).\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The result of the probe","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointCheck"}}}},"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"}}}},"/webhook-endpoints/{id}/activate":{"post":{"operationId":"activateWebhookEndpoint","summary":"Activate a webhook endpoint","description":"Needs the webhooks:manage scope. Send an empty body or {}. Starts the deliveries: a pending_verification\nendpoint needs a passing test event and a passing wrong-signature probe within the last hour; a suspended or\ndisabled endpoint a passing test event (and the probe too when its address changed). Otherwise 422\nvalidation_failed with field endpoint and code not_verified. Held events are then delivered in order. An\nendpoint that already receives events stays as it is (200).\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The endpoint","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"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"}}}},"/webhook-endpoints/{id}/disable":{"post":{"operationId":"disableWebhookEndpoint","summary":"Disable a webhook endpoint","description":"Needs the webhooks:manage scope. Send an empty body or {}. Stops the deliveries: pending, waiting and held\ndeliveries become dead (the panel can send them again for 30 days). Activate it again after a passing test\nevent. A disabled endpoint stays disabled (200).\n","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The endpoint","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"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/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 (Meta's own unsupported type: marking it as read gets 422 not_inbound with details.reason\nunsupported_message, as on a real channel). from needs phone (E.164), bsuid or both. context.message_id\nmakes it a reply. The event goes through the normal inbound pipeline: contact, conversation, 24-hour\nwindow, media download and the message.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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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; the\ncontact.identity_changed webhook event follows with reason user_id_update (a live change has reason\nuser_changed_user_id or, when the person changed their number, user_changed_number). A contact without\na BSUID on a simulated channel gets 422 validation_failed (contact_id no_bsuid): simulate an inbound\nmessage with from.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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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/channels/{id}/health":{"post":{"operationId":"simulateChannelHealth","summary":"Staging simulator: change the health of a simulated channel","description":"Staging only (added within v1). Writes the checklist of a simulated channel as a fixed scenario, so the channel\nobject (health) changes and endpoints subscribed to channel.health_changed get the same event a real channel\nsends: green (every item green), amber (account_limited: Meta limits the account) or red (not_registered: the\nnumber is not connected). messaging_limit and quality are the event's messaging_limit and quality; they alone\ndo not change the health, so they send no event. No real channel is touched and no e-mail goes out. The same\nstatus again gives outcome ignored with reason health_unchanged. A real channel gets 422 validation_failed\n(field channel_id, code not_simulated).\n","parameters":[{"$ref":"#/components/parameters/Id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxChannelHealthRequest"}}}},"responses":{"202":{"description":"The health of the simulated channel","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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/templates/{id}/status":{"post":{"operationId":"simulateTemplateStatus","summary":"Staging simulator: change the status of a simulated template","description":"Staging only (added within v1). Sends Meta's template status notice for a template of a simulated channel\nthrough the normal pipeline: the template's status changes (GET /templates shows it) and endpoints subscribed\nto template.updated get the same event a real template sends. status: APPROVED, PENDING, REJECTED (with an\noptional reason, NONE if left out), PAUSED or DISABLED. A template that is not APPROVED cannot be sent (422\ntemplate_not_approved); APPROVED makes it sendable again. A template of a real channel gets 422\nvalidation_failed (field template_id, code not_simulated).\n","parameters":[{"$ref":"#/components/parameters/Id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxTemplateStatusRequest"}}}},"responses":{"202":{"description":"The status notice was processed","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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/action-target":{"get":{"operationId":"sandboxActionTargetRead","summary":"Staging simulator: a stand-in company API, read","description":"Staging only, without a key. Not part of the API you call: UPPU Bot's actions call it on staging in place\nof a company's own API, to rehearse a read. Answers fixed sample data and ignores the query, apart from\nthe options below. At most 120 requests a minute per client address (429 with {\"error\": \"rate_limited\"}).\nA delay is honoured only for UPPU's own address, one request at a time; other requests answer without\nit. The live API has no /sandbox paths.\n","security":[],"parameters":[{"$ref":"#/components/parameters/SandboxActionTargetDelay"},{"$ref":"#/components/parameters/SandboxActionTargetStatus"}],"responses":{"200":{"description":"Sample data; with status=300 or more, {\"error\": \"forced_status\"} and that status","content":{"application/json":{"schema":{"type":"object","properties":{"slots":{"type":"array","items":{"type":"string"}},"checked_at":{"type":"string","format":"date-time"}}}}}},"429":{"description":"{\"error\": \"rate_limited\"}"}}},"post":{"operationId":"sandboxActionTargetWrite","summary":"Staging simulator: a stand-in company API, write","description":"Staging only, without a key. UPPU Bot's write actions call it on staging to rehearse a write that happens\nat most once. The Idempotency-Key header is required (400 with {\"error\": \"idempotency_key_required\"}\notherwise). The write is recorded before the delay, so a client that gives up or dies while waiting still\nleaves it recorded. The same key gets the same reference; receipts counts how often that key arrived. The\nrequest body is not stored.\n","security":[],"parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":96,"pattern":"^[A-Za-z0-9:#_.-]{1,96}$"}},{"$ref":"#/components/parameters/SandboxActionTargetDelay"},{"$ref":"#/components/parameters/SandboxActionTargetStatus"}],"responses":{"201":{"description":"Recorded; with status=300 or more, the same fields plus {\"error\": \"forced_status\"} and that status","content":{"application/json":{"schema":{"type":"object","properties":{"reference":{"type":"string","pattern":"^R-[0-9A-F]{8}$"},"receipts":{"type":"integer","minimum":1}}}}}},"400":{"description":"{\"error\": \"idempotency_key_required\"}"},"429":{"description":"{\"error\": \"rate_limited\"}"}}}},"/sandbox/action-target/receipts":{"get":{"operationId":"sandboxActionTargetReceipts","summary":"Staging simulator: what the stand-in company API received","description":"Staging only, without a key. The last 100 idempotency keys of writes, newest first, with how often each\narrived: a write that UPPU Bot sent once shows receipts 1. Kept for 24 hours; no personal data (a key is a\nbot turn's id and a step id).\n","security":[],"responses":{"200":{"description":"The receipts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"receipts":{"type":"integer"},"first_at":{"type":["string","null"],"format":"date-time"},"last_at":{"type":["string","null"],"format":"date-time"}}}}}}}}},"429":{"description":"{\"error\": \"rate_limited\"}"}}}},"/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.status can carry status expired (added within v1; previous_status queued): the ttl of the send ran\nout before UPPU handed it to the provider, nothing was sent, error.code is ttl_expired. When an earlier\nattempt got an error answer from the provider, the ttl ends the send as failed with that error instead.\nRank expired with accepted and unknown, below sent: in the rare case that the provider took the message\nafter all (UPPU could not reach it and counted the attempt as not delivered), a later sent, failed,\ndelivered or read still moves it forward.\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. On Messenger and Instagram the person's reaction opens the window as a\nmessage does (added within v1). On web chat, Telegram and e-mail service_window is null (no reply window).\ncontact.reachability_changed (added within v1, Telegram) carries {contact_id, channel_id, identity: {type,\nvalue}, reachable, reason, occurred_at}; reason is blocked_by_contact, send_forbidden (both reachable false),\nunblocked_by_contact or contact_wrote (both reachable true).\nchannel.status_changed carries {channel_id, status, previous_status, mode, previous_mode, reason}; when Meta tells\nUPPU that a WhatsApp Business account left UPPU, status is disconnected and reason is partner_removed,\naccount_deleted or account_offboarded (the last two added within v1). Accept unknown reasons.\nconversation.updated (produced within v1; subscribe to it explicitly, new endpoints leave it off)\ncarries ConversationUpdate: the conversation as GET /conversations/{id} returns it, change (handed_over,\nreturned_to_bot, released, resolved, reopened, assigned, unassigned), reason, actor {type, api_key_prefix}\nand occurred_at. It is written only while an endpoint of the company subscribes to it, so GET /events has\nit only then too. Accept unknown changes, reasons and actor types.\ncontact.updated (added within v1; subscribe to it explicitly, new endpoints leave it off) carries\nContactUpdate: the contact as GET /contacts/{id} returns it to a key for all channels, changes (the\nnames of the fields that changed: display_name, identities, merged_into, notes, attributes), reason and\noccurred_at; the envelope's channel is null. One change sends one event, a merge and its undo one for each\nof the two contacts; a change of the team's notes alone sends none. The contact carries its identities on\nall channels, so an endpoint with a channel filter gets it only when all of the contact's identities and\nconversations are on that channel. It is written only while an endpoint of the company subscribes to it.\nUPPU switches it on per environment; until then it can be subscribed to and nothing is sent. The opening of\na contact is not a change (message.received carries it). Accept unknown changes and reasons.\naccount.service_stop_scheduled and account.service_resumed (added within v1; on by default for new\nendpoints, which can leave them out of their event list) go to every subscribed endpoint of the company\nwhatever its channel filter; the envelope's channel is null, so GET /events lists them only for an\nall-channels key. account.service_stop_scheduled carries {stop_at, reason, occurred_at}\n(reason billing_overdue or subscription_ended): the service stops at stop_at unless the payment is recorded\nfirst. If the warning reached nobody and goes again, a second account.service_stop_scheduled names a later\nstop_at: the instant only moves later, and UPPU-Service-Stop-At always carries the current one.\naccount.service_resumed carries {stopped_at, resumed_at, occurred_at}. A stopped company produces no\nother event; its due deliveries end dead with last_error service_stopped and are not sent by themselves\nafter the resume (resend them).\nAn endpoint can opt in to a weekly test event (weekly_test; added within v1): UPPU then sends webhook.test,\ntest true, once a week. A failed weekly test is only a warning on the endpoint (weekly_test.failed_at); the\nendpoint is never suspended for it.\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}},"ServiceStopAt":{"description":"Added within v1: the instant the company's service stops for billing (RFC 3339 UTC), on every response to a caller that passes the key's IP allow-list from the stop warning on, the 403 service_stopped included. Absent while no stop is armed, and never on the refusals before the stop check (invalid_api_key, api_disabled, ip_not_allowed).","schema":{"type":"string","format":"date-time"}}},"parameters":{"Id":{"name":"id","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Id"}},"SandboxActionTargetDelay":{"name":"delay_ms","in":"query","description":"Wait this long before answering (the write is recorded first). Honoured only for a request from UPPU's own address (the published egress IP or UPPU's data-centre range, where UPPU Bot's actions come from), one request at a time and at most 10 a minute per address; any other request answers at once.","schema":{"type":"integer","minimum":0,"maximum":9000}},"SandboxActionTargetStatus":{"name":"status","in":"query","description":"Answer with this HTTP status instead of the default, to rehearse an action error path.","schema":{"type":"integer","minimum":200,"maximum":599}},"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}$"}},"ReportDays":{"name":"days","in":"query","description":"The last 7, 30 or 90 finished days (30 when neither days nor from and to is given). Not together with from and to.","schema":{"type":"string","enum":["7","30","90"]}},"ReportFrom":{"name":"from","in":"query","description":"The first day (YYYY-MM-DD, the company's time zone); needs to. The period is at most 90 days.","schema":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}},"ReportTo":{"name":"to","in":"query","description":"The last day (YYYY-MM-DD); needs from. A day after the last counted day is moved back to it.","schema":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}},"ReportChannel":{"name":"channel_id","in":"query","description":"Only this channel. At most one of channel_id, member_id and group_id.","schema":{"$ref":"#/components/schemas/Id"}},"ReportMember":{"name":"member_id","in":"query","description":"Only this agent, a current member of the company (the id in breakdowns.members); not with a key bound to one channel.","schema":{"$ref":"#/components/schemas/Id"}},"ReportGroup":{"name":"group_id","in":"query","description":"Only this group (the id in breakdowns.groups); not with a key bound to one channel.","schema":{"$ref":"#/components/schemas/Id"}}},"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"}},"UPPU-Service-Stop-At":{"$ref":"#/components/headers/ServiceStopAt"}},"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","handle","contact_url","mode","status","handling","bot","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"]},"handle":{"description":"How people find the channel; on Telegram the bot's @username, on e-mail the sending address (null until it is set), null on other channel types (added within v1).","type":["string","null"]},"contact_url":{"description":"A link that opens a chat with the channel; on Telegram https://t.me/<bot username>, to which ?start=<parameter> may be appended (1-64 of A-Z a-z 0-9 _ -; it arrives as context.start_parameter). On e-mail mailto:<sending address> (null until it is set). Null on other channel types (added within v1).","type":["string","null"]},"mode":{"type":"string","enum":["live","shadow"]},"status":{"type":"string","enum":["pending","active","paused","disconnected"]},"handling":{"description":"Who answers on the channel. inbox is the team inbox, api your own system, bot UPPU Bot. This is the choice the company owner stored: it stays the same when the company's plan changes. While UPPU Bot is not in the plan, a bot channel runs as a team inbox channel. In a company whose plan has no messaging panel nobody answers in UPPU: inbox and bot channels get no assignment and resolve every idle open conversation after 24 hours.","type":"string","enum":["inbox","api","bot"]},"bot":{"description":"The UPPU Bot that answers on a channel whose handling is bot: its display name, which is also the author.label of its messages. Null on other channels (added within v1).","anyOf":[{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string"}}},{"type":"null"}]},"sending":{"type":"string","enum":["active","paused","unavailable"]},"sending_reason":{"description":"Why sending is unavailable; the same codes as details.reason of channel_unavailable. team_disabled is retired within v1 and no longer produced: a company's plan never stops the sends of a connected channel (a channel type removed from the plan keeps working); the value stays listed for older clients. A company's Telegram gate likewise stops new connections only, never the sends of a connected bot. E-mail channels (added within v1): email_off (UPPU's e-mail channel is switched off), no_sender (no sending address is set) and sender_unverified (the domain of the sending address is not verified in the panel: its DKIM record was not found).","type":["string","null"],"enum":["shadow_mode","meta_off","telegram_off","team_disabled","not_linked","not_active","no_token","not_supported","email_off","no_sender","sender_unverified",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"},"email":{"description":"E-mail only, present only on e-mail channels (added within v1). inbound_address is the UPPU address the company forwards its support address to (null while UPPU's e-mail channel is switched off); from_address the sending address of the replies; sender_verified whether its domain is verified (its DKIM record found), without which nothing is sent.","type":"object","additionalProperties":false,"required":["inbound_address","from_address","sender_verified"],"properties":{"inbound_address":{"type":["string","null"]},"from_address":{"type":["string","null"]},"sender_verified":{"type":"boolean"}}},"foreign_apps":{"description":"WhatsApp only, present only on WhatsApp channels (added within v1): the Meta apps other than UPPU's that are subscribed to the channel's WhatsApp Business Account, read with the channel checklist (every 15 minutes) from Meta's subscribed_apps list. Customers do not call Meta directly: an app listed here still receives this account's webhooks from Meta and can act on the account outside UPPU. Remove it from the account in Meta Business Settings and revoke its tokens on a date agreed with UPPU: for a number moved to UPPU this is the revocation step, 7 stable days after the move, and until then red is expected. UPPU only reads the list and never changes it. Not part of health. Null until the list has been read completely.","anyOf":[{"$ref":"#/components/schemas/ChannelForeignApps"},{"type":"null"}]}}},"ChannelForeignApps":{"type":"object","additionalProperties":false,"required":["status","apps","checked_at","changed_at"],"properties":{"status":{"description":"red: at least one other app was subscribed at the last read; green: only UPPU's app, read within the last hour; unknown: no other app at the last read, but that read is older than an hour or UPPU's own app was not subscribed either (the channel's health then reports app_not_subscribed); not_applicable: UPPU does not serve the number in live mode (shadow mode, disconnected), so the account may still use other apps.","type":"string","enum":["green","red","unknown","not_applicable"]},"apps":{"description":"The other apps at the last read, by Meta's app id; name is the app's name on Meta.","type":"array","items":{"type":"object","additionalProperties":false,"required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":["string","null"]}}}},"checked_at":{"description":"The last successful read.","$ref":"#/components/schemas/Timestamp"},"changed_at":{"description":"When the set of other apps last changed. Not the status: the status also follows the age of the read, UPPU's own subscription and the channel's mode.","$ref":"#/components/schemas/NullableTimestamp"}}},"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. Telegram channels (added within v1) have items 1, 7, 8 and 9 only, with their own codes (for example webhook_elsewhere, webhook_missing, webhook_errors, webhook_backlog, token_rejected, sending_paused, telegram_unreachable); unknown codes are to be accepted. On WhatsApp, item 5 (payment method) is red with funding_missing (Meta shows no payment method), billing_fields_missing (the account's currency or time zone is empty), payment_error_131042 (Meta refused a message for a payment reason in the last 7 days) or payment_error_141006 (Meta reported a payment account problem on a message in the last 7 days, or reports it in the account's health status); on item 4, verification_unreadable (Meta does not show the field) is a grey code added within v1.","type":"array","items":{"type":"string"}},"checked_at":{"$ref":"#/components/schemas/NullableTimestamp"}}},"ChannelCapabilities":{"type":"object","additionalProperties":false,"required":["content_types","interactive_types","limits","window_hours","phone_addressing","reaction_emojis","delivery_statuses","typing_indicator"],"properties":{"content_types":{"type":"array","items":{"type":"string"}},"interactive_types":{"description":"The interactive types the channel sends (added within v1); empty when interactive is not a content type. cta_url (WhatsApp, Messenger) and flow (WhatsApp, once WhatsApp Flows are switched on for UPPU) were added within v1.","type":"array","items":{"type":"string"}},"limits":{"description":"Per-field limits. Keys ending in _bytes count UTF-8 bytes (Instagram text_body_bytes), keys ending in _utf16 count UTF-16 code units, as JavaScript's String.length does (Telegram text_body_utf16 and caption_utf16); the other keys count characters, or items for counts such as buttons and quick_replies. The values are UPPU's and may change. Where the provider's documentation states a limit, UPPU's is the same or lower, except that Instagram's documentation also asks for a text shorter than 1,000 characters. Where it states none (for example Messenger's text_body, WhatsApp's template parameters and location fields, file names, Instagram's option_id and most Telegram keys), the value is UPPU's choice and the provider may still refuse a value within it. On WhatsApp interactive_body limits the body of a buttons message and list_body that of a list; button_id limits a reply button's id and option_id a list row's. list_body and button_id were added within v1; until then interactive_body and option_id covered both kinds, so a client that validates with them still sends valid messages. Contact cards (added within v1): contact_cards per message, contact_phones and contact_emails per card, contact_field for every text field of a card (UPPU's values; Telegram takes one card with one phone). Template buttons (added within v1, WhatsApp): template_button_url limits the whole address of a URL button once its variable is filled in, template_button_payload a quick reply's payload (UPPU's values). Link buttons and flows (added within v1): cta_display_text limits a cta_url button's text (the providers' 20), cta_url its address (UPPU's value); flow_cta a flow button's text (WhatsApp advises 30 without emoji; UPPU enforces it), flow_token, flow_screen and flow_data_bytes the flow's token, first screen and the JSON of its first screen's data (UPPU's values).","type":"object","additionalProperties":{"type":"integer"}},"window_hours":{"type":["integer","null"]},"phone_addressing":{"type":"boolean"},"reaction_emojis":{"description":"The only emojis a reaction may carry (Instagram the heart; Telegram its fixed ReactionTypeEmoji list, matched without the variation selector U+FE0F); null when any single emoji is accepted or the channel sends no reactions (added within v1).","type":["array","null"],"items":{"type":"string"}},"delivery_statuses":{"description":"The statuses the channel reports after queued and accepted (added within v1): WhatsApp sent, delivered and read; Messenger delivered and read; Instagram read; Telegram, web chat and e-mail sent. Do not wait for a status the channel never reports.","type":"array","items":{"type":"string","enum":["sent","delivered","read"]}},"typing_indicator":{"description":"Whether POST /conversations/{id}/typing works on the channel (added within v1): WhatsApp, Messenger and Telegram.","type":"boolean"}}},"Message":{"description":"A message of either direction. The API and the webhooks never mask text: the company's own system gets the text as written. When the platform has switched on the company's special-category data protection, a recognized Turkish national ID number in a message older than the short retention (90 days by default, at least 30) is masked in storage for good (***46). The mask covers preview, the text fields of content (template variables included) and the names of the message's files (media.filename and attachments[].media.filename here, filename in GET /media/{id}); ids and context stay. One exception: a file uploaded with POST /media (or in the inbox) and sent in an outbound message keeps its own row, which is not tied to the message, because the upload can be reused in later sends. The short retention does not mask that row: media.filename here and filename in GET /media/{id} keep the uploaded name until the row is deleted on its own schedule (the file 90 days after its last use, the row 30 days later). The content's own filename is masked. A closed company's messages are masked the same way until its data is deleted.","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":{"description":"Outbound: queued, accepted, sent, delivered, read, failed, unknown, or (added within v1) expired: the ttl ran out before UPPU handed the message to the provider, nothing was sent, error.code ttl_expired. Inbound: received. Accept values you do not know.","type":"string","enum":["queued","accepted","sent","delivered","read","failed","unknown","expired","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: the provider's timestamp of the message (Meta's; on Telegram its date, in seconds; the receive time when the provider sent none). Outbound: when the provider accepted it (Meta accepted it, Telegram stored it; web chat: when it was written).","$ref":"#/components/schemas/NullableTimestamp"},"received_at":{"description":"Inbound: when the message reached UPPU; null outbound. On channels with a reply window (WhatsApp, Messenger, Instagram) the window counts from the earlier of sent_at and received_at: a received_at far after sent_at marks a message Meta delivered late, whose window may already be closed. Telegram and web chat have no reply window.","$ref":"#/components/schemas/NullableTimestamp"},"created_at":{"$ref":"#/components/schemas/Timestamp"}}},"MessageAuthor":{"type":"object","additionalProperties":false,"required":["type","label","api_key_prefix"],"properties":{"type":{"description":"contact, agent (the team), system (UPPU), api (your API key), bot (UPPU Bot; label is the bot's display name) or, added within v1, campaign (a campaign of the company's panel; label is the campaign's name).","type":"string","enum":["contact","agent","system","api","bot","campaign"]},"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 and, added within v1, flow_reply.\nMedia 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.\nTelegram (added within v1): a message carries at most one file (every album item is its own message; an\nanimated sticker's file is its thumbnail), which UPPU downloads; Telegram's file ids are never exposed.\nWeb chat (added within v1): a visitor's message carries at most one file (image, video, audio or\ndocument, with an optional caption) with the same media object; it is stored as it arrives.\nUPPU Bot on web chat (added within v1): a question with options is `interactive` (quick_replies whose\nids are the bot's option ids) and the visitor's tap is `interactive_reply` (kind quick_reply, the\noption's id and title; context.message_id is the question). POST /messages still sends no\ninteractive content to web chat.\nStickers and contact cards you send (added within v1) read back with the shapes of received ones: sticker\nwith the upload's media object and animated, contacts with the cards (phones carry bsuid null).\nA template or buttons message sent with a file header (added within v1) carries `header: {type, T: {media,\nfilename?}}` in `template` or `interactive`, with the upload's media object; the template's variables and\nbutton values are not echoed.\nLink buttons and WhatsApp Flows (added within v1): a sent `interactive` of type cta_url or flow reads back as\nsent (a flow with its flow_token, generated when you gave none, and flow_action navigate). A person's\ncompleted WhatsApp Flow is the inbound type `flow_reply`: `{flow_token, flow_id, response,\nresponse_omitted}`. flow_token is the token of the flow message (null when Meta sent none); flow_id is the\nflow's id when the person answered a flow message sent through UPPU (else null); response is the form's\ndata as an object, as the flow defines it, without flow_token. Text values have invisible control\ncharacters removed. A file field (a photo or document picker) keeps its file_name, mime_type and sha256;\nUPPU does not download those files and never exposes the provider's media id. A response larger than\nUPPU keeps (32 KiB of JSON, 8 levels, 500 keys and items) is not stored: response is null and\nresponse_omitted says why (too_large or too_deep). A person's completed flow arrives as unsupported\n(provider_type nfm_reply) while WhatsApp Flows are switched off for UPPU.\nE-mail (added within v1): a received e-mail is `text`: `body` is its plain text (an HTML e-mail is\nturned into text; the quoted earlier messages of a reply are cut, the full e-mail is not kept in the\nmessage), `email` is `{subject, from: {address, name}, reply_to, authenticated, in_reply_to, references,\ntrimmed, truncated, html, attachment_count, attachments_omitted}` (authenticated: UPPU's mail server\nfound a valid DKIM signature of the domain of the person's address, the reply_to when given, else from;\nfalse means the address may be forged; trimmed: a quote was cut; truncated: the text was longer than the\nmessage keeps; html: the text came from HTML; attachments_omitted: attachments over UPPU's limit that\nwere not kept) and `attachments: [{type, media}]` lists its files in order (absent when it has none).\nprovider_message_id is the e-mail's Message-ID without angle brackets (UPPU's own Message-ID on an\ne-mail you sent).\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"]}}},"start_parameter":{"description":"Telegram only, present only when the person opened the bot with a deep link (https://t.me/<bot>?start=<parameter>, delivered as \"/start <parameter>\"): the parameter, 1-64 of A-Z a-z 0-9 _ -. The message itself stays a text \"/start <parameter>\".","type":"string"},"media_group_id":{"description":"Telegram only, present only on an item of an album: the album's id. Every item of an album is its own message with its own media; the caption sits on one of them.","type":"string"}}},"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","ttl_expired","service_stopped","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":{"description":"meta, or telegram (added within v1; code is Telegram's error_code, title its description), or email (added within v1; code is the SMTP reply code of UPPU's mail server, title its masked reply).","type":"string","enum":["meta","telegram","email"]},"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","service_stopped","shadow_mode","not_found","method_not_allowed","idempotency_key_reused","channel_paused","channel_unavailable","tenant_not_ready","plan_limit_reached","media_not_ready","not_held","not_handed_over","bot_not_ready","media_expired","payload_too_large","media_too_large","unsupported_media_type","validation_failed","window_closed","consent_required","consent_withdrawn","contact_blocked","recipient_unreachable","template_not_found","template_not_approved","template_parameters_invalid","template_category_not_allowed","recipient_invalid","recipient_not_found","reply_target_invalid","content_not_supported_on_channel","not_inbound","media_unavailable","handling_not_supported","rate_limited","channel_backlog","internal_error","service_unavailable","provider_unavailable"]},"status":{"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; api_disabled: team, api_key or channel), reason (api_disabled with resource team: not_in_plan, the company's plan does not include the API), required (insufficient_scope), fields (validation_failed: [{field, code, message}]). Added within v1: handling (handling_not_supported: the channel's handling) and reason (handling_not_supported: not_in_plan, the company's plan has no messaging panel; not_held and not_handed_over: conversation_resolved; bot_not_ready: bot_unavailable, bot_busy or reply_first; not_inbound: no_inbound_message for a typing indicator on WhatsApp), scope (rate_limited: conversation, the per-conversation limit of typing indicators, with limit and period_seconds). Added within v1 too: resource, max and used (plan_limit_reached: webhook_endpoint, the limit and the endpoints the company has) and reason (not_found with resource media_link: invalid or expired). Added within v1 with template management and reports: resource reports with reason not_in_plan (api_disabled: the plan has no messaging panel, reports are part of it), resource member and group (not_found), reason templates_per_hour, concurrent_writes and provider_rate_limited (rate_limited; provider_rate_limited: Meta's own rate limit, with Meta's code in provider_error), reason credentials also when Meta refused the access token (channel_unavailable) and reason unreachable or unconfirmed (provider_unavailable: Meta could not be reached, or did not confirm the change because it did not answer in time or had a temporary error, so the change may have been made).","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; Telegram, a message of the contact's chat; e-mail, added within v1: a received e-mail of the contact, whose thread the reply joins).","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"},"ttl":{"description":"Added within v1, accepted once UPPU switches it on (until then 422 validation_failed, code unknown_field). The message's time to live in whole seconds. If UPPU has not handed the message to the provider by created_at + ttl, it is not sent: status expired, error.code ttl_expired (status failed with the provider's error when an earlier attempt got an error answer from the provider). Out of range or not a whole number: 422 validation_failed, field ttl, code between.","type":"integer","minimum":10,"maximum":86400}}},"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 (telegram: the person's Telegram user id as a decimal string; the person must have written to the bot first; email, added within v1: the address the person wrote from, case-insensitive).","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","telegram","email"]},"value":{"type":"string","maxLength":255}}}}}]},"OutboundContent":{"description":"`{\"type\": T, T: {...}}`. Web chat channels take text and, added within v1, media by media_id only (see\nMediaContent). Limits come from the channel's capabilities\n(GET /channels; ChannelCapabilities.limits says how they relate to the providers' own limits). Media\n(image, video, audio, document) takes exactly one of\nmedia_id (POST /media) or an https link that UPPU downloads before sending. Added within v1: sticker\n(StickerContent) and contacts (ContactsContent) on WhatsApp and Telegram; other channels get 422\ncontent_not_supported_on_channel. Telegram takes text (1-4096 UTF-16 units, plain text),\ninteractive buttons and quick_replies (no header, footer or list), reaction (an emoji of\ncapabilities.reaction_emojis; an empty string removes it), location (name and address both or neither:\na venue) and, added within v1, media (image, video, audio, document; see MediaContent), a static sticker\nand one contact card with one phone. Added within v1: interactive cta_url (a link button) on WhatsApp and\nMessenger and interactive flow (a WhatsApp Flow) on WhatsApp; see InteractiveContent.\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"},{"$ref":"#/components/schemas/StickerContent"},{"$ref":"#/components/schemas/ContactsContent"}]},"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 and Telegram; quick_replies on Instagram (see the channel's capabilities.interactive_types). quick_replies has the shape of buttons. On Telegram buttons are an inline keyboard (a tap arrives as interactive_reply, kind button) and quick_replies a one-time reply keyboard whose titles must be unique (a tap arrives as interactive_reply, kind quick_reply). Added within v1: cta_url, one button that opens an https address (WhatsApp and Messenger; see cta_url), and flow, a WhatsApp Flow published in the WhatsApp account (WhatsApp only, when the channel lists it in capabilities.interactive_types; see flow). A type the channel does not list gets 422 content_not_supported_on_channel (details.field content.interactive.type).","type":"string","enum":["buttons","list","quick_replies","cta_url","flow"]},"header":{"description":"A text header, or (added within v1) on a WhatsApp buttons or cta_url message a file header: an image, a video or a document given by the media_id of an upload of the same channel (POST /media; a link is not accepted), with the MIME types and sizes of the media type. A list or flow message takes only a text header. Messenger, Instagram and Telegram take no header: 422 content_not_supported_on_channel (details.field content.interactive.header).","oneOf":[{"type":"object","additionalProperties":false,"required":["type","text"],"properties":{"type":{"const":"text"},"text":{"type":"string"}}},{"$ref":"#/components/schemas/MediaHeader"}]},"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"}}}}},"cta_url":{"description":"cta_url message only (added within v1): one button that opens an https address in the person's browser. display_text is the button's text (capabilities.limits.cta_display_text: 20 on WhatsApp and Messenger). url is an https address made of the characters of a web address (letters, digits, -._~:/?#[]@!$&'()*+,;= and percent-encoding: encode a space as %20 and Turkish letters too), at most capabilities.limits.cta_url characters (the providers give no limit; this is UPPU's). UPPU sends the address as written: no shortener, no tracking. The body is required (WhatsApp interactive_body, Messenger button_text); WhatsApp also takes a header (text or file) and a footer, Messenger neither. Instagram, Telegram and web chat do not send it (422 content_not_supported_on_channel). A tap opens the address and sends nothing back.","type":"object","additionalProperties":false,"required":["display_text","url"],"properties":{"display_text":{"type":"string"},"url":{"type":"string"}}},"flow":{"description":"flow message only (added within v1; WhatsApp, when capabilities.interactive_types lists flow): opens a WhatsApp Flow that the company created and published in its WhatsApp account (Flow Builder in WhatsApp Manager); UPPU does not create or publish flows. Only navigate flows: data_exchange needs the company's own endpoint and encryption, which UPPU does not provide (422 validation_failed, content.interactive.flow.flow_action not_supported). flow_id is the flow's numeric id; flow_cta the button's text (at most capabilities.limits.flow_cta characters, without emoji). flow_token is yours (visible ASCII without spaces, at most capabilities.limits.flow_token characters); when you leave it out UPPU generates one, and the message object shows it. The person's completed form comes back on message.received as content type flow_reply with the same flow_token. flow_action_payload is optional: screen, the id of the entry screen to open (letters, digits and underscores, at most capabilities.limits.flow_screen; the flow's first screen when left out), and data, a non-empty object for that screen (needs screen; its JSON at most capabilities.limits.flow_data_bytes bytes and at most 5 levels deep). The body is required; a header takes only text. The message object echoes the flow with flow_action navigate.","type":"object","additionalProperties":false,"required":["flow_id","flow_cta"],"properties":{"flow_id":{"type":"string","pattern":"^[0-9]{1,32}$"},"flow_cta":{"type":"string"},"flow_token":{"type":"string"},"flow_action":{"type":"string","enum":["navigate"]},"flow_action_payload":{"type":"object","additionalProperties":false,"properties":{"screen":{"type":"string","pattern":"^[A-Za-z0-9_]+$"},"data":{"type":"object"}}}}}}}}},"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,\nInstagram and Telegram channels send media (see capabilities.content_types; Instagram only while UPPU's\nInstagram outbound media is on, otherwise 422 content_not_supported_on_channel); Instagram has no\ndocument (422 content_not_supported_on_channel). Web chat (added within v1) takes media_id only: a link\nis 422 content_not_supported_on_channel (details.field content.{type}.link); the message is sent at once\nand the visitor opens the file in the chat widget through a short-lived link. On Messenger and Instagram the\ncaption is sent as a separate text message right after the file (it has the channel's text limit:\nMessenger 2000 characters, Instagram 1000 bytes), and Meta shows a document under the name it was uploaded\nwith (the upload's file name, or filename with a link); one message, one status. If the file was accepted\nbut the caption could not be sent, the message stays accepted. On Telegram (added within v1) the file is\nuploaded inside the send call and the caption is Telegram's own (at most 1024 UTF-16 units, 422\nvalidation_failed beyond); an audio/ogg file goes out as a voice note. Later sends of the same upload reuse\nTelegram's file id (a document sent under another filename is uploaded again). A file Telegram refuses\nends as failed with media_rejected.\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}}},"StickerContent":{"description":"Added within v1, on WhatsApp and Telegram: `{\"type\": \"sticker\", \"sticker\": {\"media_id\": \"…\"}}`. media_id is\nan upload of the same channel within reusable_until, made with POST /media and kind sticker (on Telegram\nalso a WebP uploaded without kind): a WebP of exactly 512×512 pixels, static up to 100 KB or animated up\nto 500 KB; Telegram sends static stickers only. A link is 422\nvalidation_failed (code not_supported); a sticker has no caption. A file that breaks the rules gets 413\nmedia_too_large or 422 validation_failed for content.sticker.media_id (sticker_dimensions,\nsticker_unreadable, sticker_animated_not_allowed, mime_not_allowed), checked in the order POST /media\ngives: the 413's details carry kind sticker, the max_bytes of the file's own kind (static or animated,\nread from the header) and animated. The message reads back as content sticker with the media object and\nanimated.\n","type":"object","additionalProperties":false,"required":["type","sticker"],"properties":{"type":{"const":"sticker"},"sticker":{"type":"object","additionalProperties":false,"required":["media_id"],"properties":{"media_id":{"$ref":"#/components/schemas/Id"}}}}},"ContactsContent":{"description":"Added within v1, on WhatsApp and Telegram: `{\"type\": \"contacts\", \"contacts\": {\"contacts\": [card, …]}}`, the\nshape a received contact card has. At least one card; at most capabilities.limits.contact_cards cards\n(Telegram: one). Every card needs name.formatted_name and at least one phone number or e-mail address\n(Telegram: exactly one phone; e-mail addresses and org are 422 content_not_supported_on_channel). A\nphone number has 3 to 20 digits and may carry a plus sign, spaces, hyphens, dots and brackets; an e-mail\naddress must be valid. Each text field has at most capabilities.limits.contact_field characters. On\nWhatsApp a name with formatted_name only is sent with first_name set to it. A contact card is personal\ndata of the person it describes: it is kept, exported and erased with the message.\n","type":"object","additionalProperties":false,"required":["type","contacts"],"properties":{"type":{"const":"contacts"},"contacts":{"type":"object","additionalProperties":false,"required":["contacts"],"properties":{"contacts":{"type":"array","items":{"$ref":"#/components/schemas/ContactCard"}}}}}},"ContactCard":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"object","additionalProperties":false,"required":["formatted_name"],"properties":{"formatted_name":{"description":"The name as it is shown.","type":"string","minLength":1},"first_name":{"type":"string"},"last_name":{"type":"string"},"middle_name":{"type":"string"},"prefix":{"type":"string"},"suffix":{"type":"string"}}},"phones":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["phone"],"properties":{"phone":{"type":"string"},"type":{"description":"For example CELL, MAIN, IPHONE, HOME or WORK.","type":"string"}}}},"emails":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["email"],"properties":{"email":{"type":"string"},"type":{"description":"For example HOME or WORK.","type":"string"}}}},"org":{"type":"object","additionalProperties":false,"properties":{"company":{"type":"string"},"department":{"type":"string"},"title":{"type":"string"}}}}},"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. A text header takes its variables; a template whose header is an image, a video\nor a document (added within v1) takes the file as {\"type\": \"image\", \"image\": {\"media_id\": \"…\"}}, the\nupload of the same channel (POST /media, no link; an image JPEG or PNG up to 5 MB, a video MP4 or 3GPP\nup to 16 MB, a document PDF only, up to 25 MB), and a document may carry the filename the person\nsees. Buttons (added within v1): give a value only for a button that takes one, by its position in\nthe template (index, from 0): a URL button whose address ends in a variable needs its parameter (the\nend of the address: characters of a web address, others percent-encoded; with the template's address\nat most capabilities.limits.template_button_url characters), a quick reply may carry a payload (at\nmost capabilities.limits.template_button_payload characters) that comes back in the person's tap.\nButtons without a variable (a fixed address, a phone number, a quick reply without payload) take no\nvalue: they come with the approved template. GET /templates lists what a template needs\n(parameters.header_media, parameters.buttons). A value that does not fit the template is 422\ntemplate_parameters_invalid with the field paths (content.template.header,\ncontent.template.buttons.0.parameter); a file that cannot be used is 422 media_unavailable, 422\nvalidation_failed (mime_not_allowed) or 413 media_too_large for content.template.header.<type>.media_id.\n","type":"object","additionalProperties":false,"properties":{"id":{"$ref":"#/components/schemas/Id"},"name":{"type":"string"},"language":{"type":"string"},"header":{"oneOf":[{"type":"object","additionalProperties":false,"required":["type","parameters"],"properties":{"type":{"const":"text"},"parameters":{"$ref":"#/components/schemas/TemplateValues"}}},{"$ref":"#/components/schemas/MediaHeader"}]},"body":{"type":"object","additionalProperties":false,"required":["parameters"],"properties":{"parameters":{"$ref":"#/components/schemas/TemplateValues"}}},"buttons":{"description":"At most 10 values, one per button that takes a value.","type":"array","items":{"$ref":"#/components/schemas/TemplateButtonValue"}}}}}},"MediaHeader":{"description":"A file header (added within v1): the media_id of an upload of the same channel made with POST /media; only a document takes a filename.","oneOf":[{"type":"object","additionalProperties":false,"required":["type","image"],"properties":{"type":{"const":"image"},"image":{"$ref":"#/components/schemas/HeaderFile"}}},{"type":"object","additionalProperties":false,"required":["type","video"],"properties":{"type":{"const":"video"},"video":{"$ref":"#/components/schemas/HeaderFile"}}},{"type":"object","additionalProperties":false,"required":["type","document"],"properties":{"type":{"const":"document"},"document":{"type":"object","additionalProperties":false,"required":["media_id"],"properties":{"media_id":{"$ref":"#/components/schemas/Id"},"filename":{"type":"string","maxLength":255}}}}}]},"HeaderFile":{"type":"object","additionalProperties":false,"required":["media_id"],"properties":{"media_id":{"$ref":"#/components/schemas/Id"}}},"TemplateButtonValue":{"description":"The value of a template button (added within v1), by the button's position in the template (index, 0 to 9).","oneOf":[{"type":"object","additionalProperties":false,"required":["index","type","parameter"],"properties":{"index":{"type":"integer","minimum":0,"maximum":9},"type":{"const":"url"},"parameter":{"description":"The end of the address that replaces the URL's variable.","type":"string","minLength":1}}},{"type":"object","additionalProperties":false,"required":["index","type","payload"],"properties":{"index":{"type":"integer","minimum":0,"maximum":9},"type":{"const":"quick_reply"},"payload":{"description":"Comes back as the id of the person's interactive_reply (kind button, source template).","type":"string","minLength":1}}}]},"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"]}}},"TypingIndicator":{"description":"Added within v1. The answer of POST /conversations/{id}/typing; nothing is stored.","type":"object","additionalProperties":false,"required":["object","conversation_id","message_id","state"],"properties":{"object":{"const":"typing_indicator"},"conversation_id":{"$ref":"#/components/schemas/Id"},"message_id":{"description":"WhatsApp only, the contact's message the indicator marks as read; null on other channels.","type":["string","null"]},"state":{"description":"Always queued; the outcome is not reported.","type":"string","enum":["queued"]}}},"ContactLookupRequest":{"description":"The channel and exactly one of phone (E.164, WhatsApp channels) or identity. Added within v1: identity type external finds the web chat contact your website signed in with that user id.","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","telegram","email","external"]},"value":{"type":"string","maxLength":255}}}}},"Contact":{"type":"object","additionalProperties":false,"required":["object","id","display_name","blocked","merged_into","identities","service_windows","consents","created_at","notes","attributes"],"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, on Messenger and Instagram the name UPPU reads from Meta's profile endpoint after the contact's first message (Instagram falls back to @username), or on Telegram the name every update carries (first and last name, otherwise @username; present from the first message). On web chat the name your website signs for a signed-in user (added within v1) fills an empty name. An agent-edited name is never overwritten. Null until a name is known.","type":["string","null"]},"blocked":{"type":"boolean"},"merged_into":{"description":"Id of the contact this contact was merged into on verified evidence, null when it was not merged. A merged contact has empty identities, service_windows and consents, is visible while that contact is, and is never a recipient (recipient_not_found). A merge can be undone, so read it again.","type":["string","null"]},"identities":{"description":"Identities on the key's channels; superseded identities have current false. Added within v1: on web chat type external is the user id your website signs for a signed-in user, and email and phone are the values it signs with it; on e-mail channels type email is the address the person wrote from (their Reply-To when they gave one), in lower case. Accept unknown types.","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); on Messenger and Instagram also of their last reaction (added within v1).","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"},"notes":{"description":"The team's notes about the person, written in the inbox contact panel (plain text, at most 5000 characters; null without notes). Not the internal notes of a conversation. Added within v1.","type":["string","null"]},"attributes":{"description":"The company's contact fields that have a value for this person, by field name (the name the owner or an admin gave the field in the panel). A text field is a string, a number field a number, a date field a \"YYYY-MM-DD\" string and a list field one of its options. An empty object when no field has a value. Read only in v1: the team fills them in the inbox contact panel. Added within v1.","type":"object","additionalProperties":{"type":["string","number"]}}}},"ConsentRequest":{"description":"A consent record to add (POST /consents): the fields of an inline consent plus the channel and the person.","type":"object","additionalProperties":false,"required":["channel_id","to","purpose","state","source_text","source_ref"],"properties":{"channel_id":{"$ref":"#/components/schemas/Id"},"to":{"$ref":"#/components/schemas/Recipient"},"purpose":{"type":"string","enum":["marketing","utility"]},"state":{"type":"string","enum":["granted","withdrawn"]},"source_text":{"description":"The exact text the person agreed to (or the withdrawal they asked for).","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, never in the future.","type":"string","format":"date-time"}}},"ConsentList":{"type":"object","additionalProperties":false,"required":["object","data","next_cursor"],"properties":{"object":{"const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Consent"}},"next_cursor":{"type":["string","null"]}}},"Consent":{"description":"A record of the consent ledger; records are only added, never changed.","type":"object","additionalProperties":false,"required":["object","id","contact_id","channel_type","purpose","state","source","source_text","source_ref","collected_at","recorded_at","api_key_prefix"],"properties":{"object":{"const":"consent"},"id":{"$ref":"#/components/schemas/Id"},"contact_id":{"$ref":"#/components/schemas/Id"},"channel_type":{"type":"string"},"purpose":{"type":"string","enum":["marketing","utility"]},"state":{"type":"string","enum":["granted","withdrawn"]},"source":{"description":"api (recorded with an API key: POST /consents or the inline consent of a template send), inbound_keyword (the person wrote STOP, DUR or İPTAL; both purposes), meta_opt_out (Meta's marketing opt-out, error 131050), meta_user_preferences (the person stopped the business's marketing messages in WhatsApp; marketing withdrawals only, turning them back on is not a consent and is not recorded), agent_recorded (an owner or admin of the company recorded it in the UPPU panel) or import (a row of a CSV file the company imported in the UPPU panel). v1 may add values; accept unknown values.","type":"string"},"source_text":{"description":"The exact text the person agreed to; null for automatic withdrawals and for withdrawals recorded in the panel or imported without a note.","type":["string","null"]},"source_ref":{"description":"Your reference to the evidence; null for automatic withdrawals and for imported rows without a reference. For agent_recorded it is how the person told the company (conversation, phone_call, written, in_person or other), followed by \":\" and the reference the panel user gave, if any.","type":["string","null"]},"collected_at":{"description":"When the person gave or withdrew the consent.","$ref":"#/components/schemas/Timestamp"},"recorded_at":{"description":"When UPPU recorded it.","$ref":"#/components/schemas/Timestamp"},"api_key_prefix":{"description":"The prefix of the API key that recorded it; null otherwise.","type":["string","null"]}}},"ConversationList":{"type":"object","additionalProperties":false,"required":["object","data","next_cursor"],"properties":{"object":{"const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Conversation"}},"next_cursor":{"type":["string","null"]}}},"Conversation":{"description":"A conversation of a contact on a channel. A contact has at most one open conversation per channel; a message after the conversation was resolved opens a new one. held_by and handover_reason were added within v1 with the handover endpoints; ignore unknown fields.","type":"object","additionalProperties":false,"required":["object","id","channel_id","contact_id","status","handling","assigned","handed_over_at","held_by","handover_reason","opened_at","resolved_at","last_inbound_at","last_message_at","service_window"],"properties":{"object":{"const":"conversation"},"id":{"$ref":"#/components/schemas/Id"},"channel_id":{"$ref":"#/components/schemas/Id"},"contact_id":{"$ref":"#/components/schemas/Id"},"status":{"type":"string","enum":["open","resolved"]},"handling":{"description":"The channel's stored handling (inbox the team inbox, api your own system, bot UPPU Bot), also while a module of the company's plan is off (see the channel's handling); accept unknown values.","type":"string","enum":["inbox","api","bot"]},"assigned":{"description":"Whether a team member is assigned; the assignee is never shown.","type":"boolean"},"handed_over_at":{"description":"On api and bot channels: null while the automation (your system or UPPU Bot) holds the conversation, otherwise when it was handed over to the team. On bot channels UPPU Bot hands over by itself (a handover step of its flow, a request for a person, an error), a team member's reply, claim or assignment in the inbox takes the conversation over, and returning it to the bot in the inbox sets it back to null. Conversations that are open when a channel moves to UPPU Bot or away from it are handed over to the team. inbox channels ignore it: the team holds their conversations. In a company whose plan has no messaging panel, bot channels ignore it too (see the channel's handling). Your system hands over and releases with POST /conversations/{id}/handover and /release (added within v1).","$ref":"#/components/schemas/NullableTimestamp"},"held_by":{"description":"Who holds the conversation (added within v1): api (your system, on an api channel), bot (UPPU Bot, on a bot channel) or team (handed over, and every conversation of an inbox channel). It follows the channel's stored handling and handed_over_at, like handling; on a resolved conversation it names who held it last (a conversation reopened by the person's message stays with them). When an owner changes the channel's handling in the panel, the channel's open conversations change hands without a conversation.updated event: moving an api channel to UPPU Bot or to the team inbox gives your system's open conversations to the team, and moving an inbox channel to api gives you its open conversations that were not handed over. Only removing UPPU Bot from a channel sends handed_over (bot_detached) for each conversation the bot held. The channel (GET /channels) and the conversation.handling of message.received show the new handling; re-read the channel's open conversations (GET /conversations?channel_id=…&status=open) when it changes. Accept unknown values.","type":"string","enum":["api","bot","team"]},"handover_reason":{"description":"Why the conversation was last handed over to the team (added within v1); null while the automation holds it, and when the handover has no recorded reason (open conversations stamped when their channel moved to UPPU Bot, or when UPPU Bot came back to the company's plan; the reason of an earlier handover that was released or returned is never carried over). api is your own handover (POST /conversations/{id}/handover); the others are UPPU Bot's, for example flow (a handover step of its flow), keyword (the person asked for a person), agent_takeover (a team member replied, claimed or assigned it), bot_detached (the bot was removed from the channel), external_reply (someone replied outside UPPU), staff_initiated (the person answered a template of the team) and schedule (a new conversation came outside the bot's schedule on the channel). Accept unknown values.","type":["string","null"],"enum":["flow","keyword","low_confidence","max_attempts","llm_unavailable","llm_busy","action_error","action_unknown","identity_unverified","window_closed","stale","policy_refused","unsupported_content","budget_exceeded","loop_detected","turn_failed","step_budget","pipeline_stalled","send_failed","bot_unavailable","bot_detached","agent_takeover","external_reply","staff_initiated","api","schedule",null]},"opened_at":{"description":"When the conversation was opened; a reopened conversation keeps it.","$ref":"#/components/schemas/Timestamp"},"resolved_at":{"description":"When the conversation was resolved; null while it is open.","$ref":"#/components/schemas/NullableTimestamp"},"last_inbound_at":{"description":"The provider's timestamp of the person's last message in this conversation (Meta's; on Telegram the message's date; the receive time when the provider sent none, for example a Telegram button tap or a web chat message), never later than when UPPU received it; null if the person never wrote in it, for example a conversation opened by your template. On Messenger and Instagram the person's reaction counts as a message here, since it opens the reply window (added within v1).","$ref":"#/components/schemas/NullableTimestamp"},"last_message_at":{"description":"The last activity (inbound, outbound or system message; UPPU's automatic privacy notice on WhatsApp, Messenger and Instagram leaves it unchanged, the time stays that of the contact's message); api and bot channels resolve the idle conversations the automation holds from it. In a company whose plan has no messaging panel, inbox and bot channels resolve every idle open conversation from it, after the same 24 hours (see the channel's handling).","$ref":"#/components/schemas/Timestamp"},"service_window":{"description":"The contact's current 24-hour reply window on the conversation's channel, as in message.received (counted from the person's latest message on the channel, in any conversation, or on Messenger and Instagram their latest reaction); null on web chat, Telegram and e-mail, which have no reply window.","anyOf":[{"type":"object","additionalProperties":false,"required":["open","closes_at"],"properties":{"open":{"type":"boolean"},"closes_at":{"$ref":"#/components/schemas/NullableTimestamp"}}},{"type":"null"}]}}},"HandoverRequest":{"description":"The optional body of POST /conversations/{id}/handover (added within v1). Both fields appear only in the team inbox, next to the handover; the API and the webhooks never return them.","type":"object","additionalProperties":false,"properties":{"reason":{"description":"A short code of your own for why you hand the conversation over, for example customer_request. No personal data.","type":"string","maxLength":64,"pattern":"^[A-Za-z0-9._:-]+$"},"note":{"description":"A note for the team (line breaks allowed, no other control characters).","type":"string","maxLength":1000}}},"ConversationUpdate":{"description":"The data of a conversation.updated event (added within v1). Ordered per contact, together with the contact's message events. Never the assignee's identity, the handover note or your reason and note.","type":"object","additionalProperties":false,"required":["conversation","change","reason","actor","occurred_at"],"properties":{"conversation":{"$ref":"#/components/schemas/Conversation"},"change":{"description":"What changed: handed_over (to the team: UPPU Bot's handover, a team member's take-over, the bot removed from the channel, or your handover), returned_to_bot (back to UPPU Bot from the inbox or with POST /conversations/{id}/release), released (back to your system on an api channel), resolved, reopened, assigned or unassigned. A conversation that opens already assigned (the inbox's automatic assignment) sends assigned before the message.received of its first message. When a team member is removed, leaves, loses the reply permission or deletes their account, or the company closes, the member's open conversations send unassigned (member_removed, member_left, role_changed, account_deleted or tenant_closed) and their resolved ones send nothing. A change of the channel's handling sends nothing either (see the conversation's held_by). Accept unknown values.","type":"string","enum":["handed_over","returned_to_bot","released","resolved","reopened","assigned","unassigned"]},"reason":{"description":"For handed_over the conversation's handover_reason. For assigned, unassigned, resolved and reopened what caused it, for example claim, manual and reply (a team member in the inbox), auto (automatic assignment), idle (resolved after 24 hours without activity), api (your key), visitor_message (the person wrote within the reopen period), bot_handoff (the channel's automatic assignment after a handover, UPPU Bot's or yours), bot_idle, bot_end, returned_to_bot or member_removed. Null for returned_to_bot and released, and when nothing is recorded. Accept unknown values.","type":["string","null"]},"actor":{"description":"Who made the change: member (a team member in the inbox), api (an API key; api_key_prefix names it, so your system can recognise its own changes), bot (UPPU Bot) or system (UPPU on its own, for example an automatic assignment). Accept unknown types.","type":"object","additionalProperties":false,"required":["type","api_key_prefix"],"properties":{"type":{"type":"string","enum":["member","api","bot","system"]},"api_key_prefix":{"type":["string","null"]}}},"occurred_at":{"$ref":"#/components/schemas/Timestamp"}}},"ContactUpdate":{"description":"The data of a contact.updated event (added within v1). Ordered per contact, together with the contact's message events; the envelope's channel is null. Never an earlier value.","type":"object","additionalProperties":false,"required":["contact","changes","reason","occurred_at"],"properties":{"contact":{"$ref":"#/components/schemas/Contact"},"changes":{"description":"The fields of the contact that changed, in this order: display_name, identities (an identity added, replaced or superseded, or moved by a merge or its undo), merged_into, notes (only together with another change: the notes alone send no event) and attributes. The service windows and the consent summary follow the identities and conversations and are not listed. Never empty. Accept unknown values.","type":"array","items":{"type":"string","enum":["display_name","identities","merged_into","notes","attributes"]}},"reason":{"description":"What changed the contact: member_edited (a team member in the panel), provider_name (the name Messenger, Instagram or Telegram gives), pre_chat (the web chat pre-chat form filled an empty name), identity_added (the channel named another identity of the person), identity_rotated (WhatsApp changed the user id; contact.identity_changed follows the same change), signed_handoff (your website's signed sign-in on the web chat; also when replacing or turning off its secret signs out the browsers of signed-in users, whose web_visitor identities become current false), merged or merge_reverted (one event for each of the two contacts). Accept unknown values.","type":"string","enum":["member_edited","provider_name","pre_chat","identity_added","identity_rotated","signed_handoff","merged","merge_reverted"]},"occurred_at":{"$ref":"#/components/schemas/Timestamp"}}},"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":{"description":"The content object of POST /messages with the template's own examples; null when the template cannot be sent. A variable without an example reads <name> and a URL button's parameter without one <1>; a file header carries a placeholder media_id made of zeros only: upload the file with POST /media and give its id.","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","header_media","buttons"],"properties":{"format":{"type":"string","enum":["named","positional"]},"header":{"type":"array","items":{"$ref":"#/components/schemas/TemplateVariable"}},"body":{"type":"array","items":{"$ref":"#/components/schemas/TemplateVariable"}},"header_media":{"description":"Added within v1: image, video or document when the template's header is a file (send it in content.template.header with the media_id of an upload), null otherwise.","type":["string","null"],"enum":["image","video","document",null]},"buttons":{"description":"Added within v1: the buttons that take a value, by their position in the template. A URL button whose address has a variable needs its parameter (required true; example is the end of the address in the template's example); a quick reply may carry a payload. Buttons without a variable are not listed.","type":"array","items":{"$ref":"#/components/schemas/TemplateButtonField"}}}},"TemplateButtonField":{"type":"object","additionalProperties":false,"required":["index","type","text","required","example"],"properties":{"index":{"type":"integer","minimum":0},"type":{"type":"string","enum":["url","quick_reply"]},"text":{"type":["string","null"]},"required":{"type":"boolean"},"example":{"type":["string","null"]}}},"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"]}}},"TemplateCreateRequest":{"description":"A WhatsApp template to create (POST /templates, added within v1): the rules of the panel's template page. Variables are either numbered ({{1}}, {{2}} ... from 1 without gaps) or named ({{first_name}}: lowercase letters, digits and underscores), never both; each variable once, every variable with a one-line example of at most 200 characters, and the body neither starts nor ends with a variable.","type":"object","additionalProperties":false,"required":["channel_id","name","language","category","body"],"properties":{"channel_id":{"description":"A WhatsApp channel of the key; the template belongs to the channel's WhatsApp Business account.","type":"string","pattern":"^[0-9a-hjkmnp-tv-z]{26}$"},"name":{"description":"1 to 512 lowercase letters, digits and underscores, for example order_update.","type":"string","pattern":"^[a-z0-9_]{1,512}$"},"language":{"type":"string","enum":["tr","en"]},"category":{"description":"Upper or lower case. Authentication templates cannot be created.","type":"string","enum":["UTILITY","MARKETING","utility","marketing"]},"header":{"$ref":"#/components/schemas/TemplateCreateHeader"},"body":{"$ref":"#/components/schemas/TemplateCreateBody"},"footer":{"description":"One line of at most 60 characters, without variables.","type":"object","additionalProperties":false,"required":["text"],"properties":{"text":{"type":"string","maxLength":60}}},"buttons":{"description":"At most 3 quick replies, 2 URL buttons and 1 phone number button; the quick replies next to each other, before or after the other buttons. Button texts are one line of at most 25 characters without variables and differ from each other.","type":"array","items":{"$ref":"#/components/schemas/TemplateCreateButton"}}}},"TemplateCreateHeader":{"description":"A text header: one line of at most 60 characters with at most one variable ({{1}} in a numbered template) and its example. Or (added within v1) a file header with a sample file: the media_id of an upload of the same channel made with POST /media (a JPEG or PNG image up to 5 MB, an MP4 video up to 16 MB, a PDF document up to 25 MB). UPPU uploads the sample to Meta for the review (Meta's Resumable Upload); every message sent with the template still carries its own file. A header with a location cannot be created through UPPU (422 validation_failed, code not_supported).","oneOf":[{"type":"object","additionalProperties":false,"required":["type","text"],"properties":{"type":{"const":"text"},"text":{"type":"string","maxLength":60},"example":{"description":"The example of the header's variable.","type":"string","maxLength":200}}},{"$ref":"#/components/schemas/TemplateCreateMediaHeader"}]},"TemplateCreateMediaHeader":{"description":"A file header of a new template (added within v1): the sample file is the media_id of an upload of the same channel made with POST /media.","oneOf":[{"type":"object","additionalProperties":false,"required":["type","image"],"properties":{"type":{"const":"image"},"image":{"$ref":"#/components/schemas/HeaderFile"}}},{"type":"object","additionalProperties":false,"required":["type","video"],"properties":{"type":{"const":"video"},"video":{"$ref":"#/components/schemas/HeaderFile"}}},{"type":"object","additionalProperties":false,"required":["type","document"],"properties":{"type":{"const":"document"},"document":{"$ref":"#/components/schemas/HeaderFile"}}}]},"TemplateCreateBody":{"type":"object","additionalProperties":false,"required":["text"],"properties":{"text":{"description":"Plain text of at most 1024 characters; line breaks allowed.","type":"string","maxLength":1024},"examples":{"description":"An example for every variable: an object by variable name (or number), or for a numbered template a list in the order of the numbers.","anyOf":[{"type":"object","additionalProperties":{"type":"string","maxLength":200}},{"type":"array","items":{"type":"string","maxLength":200}}]}}},"TemplateCreateButton":{"description":"quick_reply {type, text}; url {type, text, url, example}: an https or http address of at most 2000 characters whose only variable is {{1}} at its end, with example the end of an example address (the characters of a web address only); phone_number {type, text, phone_number}: E.164 with a plus sign.","type":"object","additionalProperties":false,"required":["type","text"],"properties":{"type":{"type":"string","enum":["quick_reply","url","phone_number"]},"text":{"type":"string","maxLength":25},"url":{"type":"string","maxLength":2000},"example":{"type":"string","maxLength":2000},"phone_number":{"type":"string","pattern":"^\\+[1-9][0-9]{4,14}$"}}},"DeletedTemplate":{"type":"object","additionalProperties":false,"required":["object","id","name","language","deleted"],"properties":{"object":{"const":"template"},"id":{"$ref":"#/components/schemas/Id"},"name":{"type":"string"},"language":{"type":"string"},"deleted":{"const":true}}},"ReportSummary":{"description":"The report of a period (GET /reports/summary, added within v1): the nightly counters of the panel's Reports page. Counts only. A section the report's tier does not include is null.","type":"object","additionalProperties":false,"required":["object","tier","features","period","scope","totals","first_response","resolution","sla","topics","tags","untagged","breakdowns","updated_at"],"properties":{"object":{"const":"report_summary"},"tier":{"description":"The report's tier (basic or full; every company has full until the plan sets it).","type":"string","enum":["basic","full"]},"features":{"$ref":"#/components/schemas/ReportFeatures"},"period":{"$ref":"#/components/schemas/ReportPeriod"},"scope":{"$ref":"#/components/schemas/ReportScope"},"totals":{"$ref":"#/components/schemas/ReportTotals"},"first_response":{"$ref":"#/components/schemas/ReportDurations"},"resolution":{"$ref":"#/components/schemas/ReportDurations"},"sla":{"description":"SLA targets met and missed (counted from the targets' moments); null without the sla feature.","anyOf":[{"$ref":"#/components/schemas/ReportSla"},{"type":"null"}]},"topics":{"description":"The topics of new conversations, most first; null without the topics feature.","anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/ReportTopic"}},{"type":"null"}]},"tags":{"description":"The other tags of new conversations, most first (a deleted tag has no name); null without the topics feature.","anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/ReportTag"}},{"type":"null"}]},"untagged":{"description":"New conversations without a tag; null without the topics feature.","type":["integer","null"]},"breakdowns":{"description":"Per channel, agent and group, only for the whole company (scope all); null otherwise. The totals and the agent and group rows count every channel of the company, also the channels UPPU stopped for the API.","anyOf":[{"$ref":"#/components/schemas/ReportBreakdowns"},{"type":"null"}]},"updated_at":{"description":"When the counters were last written.","$ref":"#/components/schemas/NullableTimestamp"}}},"ReportFeatures":{"description":"The sections of the report's tier.","type":"object","additionalProperties":false,"required":["volume","times","channels","members","groups","sla","topics"],"properties":{"volume":{"type":"boolean"},"times":{"type":"boolean"},"channels":{"type":"boolean"},"members":{"type":"boolean"},"groups":{"type":"boolean"},"sla":{"type":"boolean"},"topics":{"type":"boolean"}}},"ReportPeriod":{"type":"object","additionalProperties":false,"required":["from","to","days","preset","timezone","counted_through","pending"],"properties":{"from":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}-[0-9]{2}$"},"to":{"description":"The last day; never after the last counted day.","type":"string","pattern":"^[0-9]{4}-[0-9]{2}-[0-9]{2}$"},"days":{"type":"integer","minimum":1,"maximum":90},"preset":{"description":"7, 30 or 90 for the days parameter (and its default); null for from and to.","type":["integer","null"]},"timezone":{"description":"The company's time zone; days are calendar days there.","type":"string"},"counted_through":{"description":"The last day the nightly count finished; null before the first count.","type":["string","null"],"pattern":"^[0-9]{4}-[0-9]{2}-[0-9]{2}$"},"pending":{"description":"True while yesterday is not counted yet (the period ends earlier).","type":"boolean"}}},"ReportScope":{"type":"object","additionalProperties":false,"required":["type","id","name"],"properties":{"type":{"type":"string","enum":["all","channel","member","group"]},"id":{"description":"The channel's, agent's or group's id; null for the whole company.","anyOf":[{"$ref":"#/components/schemas/Id"},{"type":"null"}]},"name":{"type":["string","null"]}}},"ReportTotals":{"description":"The counts of the period. opened: conversations the contacts opened; team_opened: those that reached the team (on a channel your system or UPPU Bot answers, at the handover); unanswered: of these, without the team's first response on that day; replies: the team's messages; resolved and reopened: every resolve and reopen; open_at_end and open_unassigned_at_end: the team's open conversations at the end of the period's last day; tagged: new conversations with a tag (null without the topics feature).","type":"object","additionalProperties":false,"required":["opened","team_opened","unanswered","unanswered_share","replies","resolved","reopened","open_at_end","open_unassigned_at_end","tagged"],"properties":{"opened":{"type":"integer","minimum":0},"team_opened":{"type":"integer","minimum":0},"unanswered":{"type":"integer","minimum":0},"unanswered_share":{"description":"unanswered of team_opened in percent; null when team_opened is 0.","type":["integer","null"]},"replies":{"type":"integer","minimum":0},"resolved":{"type":"integer","minimum":0},"reopened":{"type":"integer","minimum":0},"open_at_end":{"type":"integer","minimum":0},"open_unassigned_at_end":{"type":"integer","minimum":0},"tagged":{"type":["integer","null"]}}},"ReportDurations":{"description":"First response times (from reaching the team to the team's first response) or resolution times (from the start of the cycle to the resolve, without the time the conversation waited). buckets counts the samples per bucket: at most 1m, 5m, 15m, 30m, 1h, 4h, 8h, 24h, 72h, or over for first responses; 15m, 1h, 4h, 8h, 24h, 72h, 7d, or over for resolutions. median and p90 are the buckets in which half and nine tenths of the samples fall (null without samples); average_seconds is seconds divided by count.","type":"object","additionalProperties":false,"required":["count","seconds","average_seconds","median","p90","buckets"],"properties":{"count":{"type":"integer","minimum":0},"seconds":{"type":"integer","minimum":0},"average_seconds":{"type":["integer","null"]},"median":{"type":["string","null"]},"p90":{"type":["string","null"]},"buckets":{"type":"object","additionalProperties":{"type":"integer","minimum":0}}}},"ReportSla":{"type":"object","additionalProperties":false,"required":["targets_set","first_response","resolution"],"properties":{"targets_set":{"description":"Whether the company has SLA targets today.","type":"boolean"},"first_response":{"$ref":"#/components/schemas/ReportSlaCount"},"resolution":{"$ref":"#/components/schemas/ReportSlaCount"}}},"ReportSlaCounts":{"type":"object","additionalProperties":false,"required":["first_response","resolution"],"properties":{"first_response":{"$ref":"#/components/schemas/ReportSlaCount"},"resolution":{"$ref":"#/components/schemas/ReportSlaCount"}}},"ReportSlaCount":{"type":"object","additionalProperties":false,"required":["met","missed","share"],"properties":{"met":{"type":"integer","minimum":0},"missed":{"type":"integer","minimum":0},"share":{"description":"met of met and missed in percent; null when both are 0.","type":["integer","null"]}}},"ReportTopic":{"type":"object","additionalProperties":false,"required":["id","name","count"],"properties":{"id":{"$ref":"#/components/schemas/Id"},"name":{"type":"string"},"count":{"type":"integer","minimum":0}}},"ReportTag":{"type":"object","additionalProperties":false,"required":["id","name","deleted","count"],"properties":{"id":{"$ref":"#/components/schemas/Id"},"name":{"type":["string","null"]},"deleted":{"type":"boolean"},"count":{"type":"integer","minimum":0}}},"ReportBreakdowns":{"type":"object","additionalProperties":false,"required":["channels","members","groups"],"properties":{"channels":{"description":"The channels the key can see by id, name and type; deleted channels are one row with deleted true and no name, and the channels UPPU stopped for the API are one last row with hidden true and no id, name or type.","type":"array","items":{"$ref":"#/components/schemas/ReportRow"}},"members":{"description":"The agents (members who take conversations, and current members with counts) by id and name; the counts of people who are no longer members are one row with former true and no name. null without the members feature.","anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/ReportRow"}},{"type":"null"}]},"groups":{"description":"The groups; deleted groups are one row with deleted true and no name. null without the groups feature.","anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/ReportRow"}},{"type":"null"}]}}},"ReportRow":{"type":"object","additionalProperties":false,"required":["id","name","type","deleted","former","hidden","totals","first_response","resolution","sla"],"properties":{"id":{"description":"The channel's, agent's or group's id; null for the row of deleted channels or groups, of former members and of channels the key cannot see.","anyOf":[{"$ref":"#/components/schemas/Id"},{"type":"null"}]},"name":{"type":["string","null"]},"type":{"description":"The channel's type (channels only; null otherwise).","type":["string","null"]},"deleted":{"type":"boolean"},"former":{"type":"boolean"},"hidden":{"description":"True only for the one channel row that holds the counts of the channels the key cannot see (channels UPPU stopped for the API); its id, name and type are null. Always false for agents and groups.","type":"boolean"},"totals":{"$ref":"#/components/schemas/ReportTotals"},"first_response":{"$ref":"#/components/schemas/ReportDurations"},"resolution":{"$ref":"#/components/schemas/ReportDurations"},"sla":{"anyOf":[{"$ref":"#/components/schemas/ReportSlaCounts"},{"type":"null"}]}}},"ReportDayList":{"type":"object","additionalProperties":false,"required":["object","data","next_cursor"],"properties":{"object":{"const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ReportDay"}},"next_cursor":{"type":["string","null"]}}},"ReportDay":{"description":"The counts of one day (GET /reports/daily). answered and unanswered split team_opened by the team's first response on the day; open and open_unassigned are the team's open conversations at the end of the day.","type":"object","additionalProperties":false,"required":["object","day","counted","opened","team_opened","answered","unanswered","replies","resolved","reopened","open","open_unassigned","tagged","first_response","resolution","sla"],"properties":{"object":{"const":"report_day"},"day":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}-[0-9]{2}$"},"counted":{"description":"Whether the nightly count finished the day (zeros of a day not counted are unknown).","type":"boolean"},"opened":{"type":"integer","minimum":0},"team_opened":{"type":"integer","minimum":0},"answered":{"type":"integer","minimum":0},"unanswered":{"type":"integer","minimum":0},"replies":{"type":"integer","minimum":0},"resolved":{"type":"integer","minimum":0},"reopened":{"type":"integer","minimum":0},"open":{"type":"integer","minimum":0},"open_unassigned":{"type":"integer","minimum":0},"tagged":{"type":["integer","null"]},"first_response":{"$ref":"#/components/schemas/ReportDurations"},"resolution":{"$ref":"#/components/schemas/ReportDurations"},"sla":{"anyOf":[{"$ref":"#/components/schemas/ReportSlaCounts"},{"type":"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","contact.reachability_changed","contact.updated","account.service_stop_scheduled","account.service_resumed","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 test events (the webhook page's and the API's test event and probe, and the optional weekly test event).","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. Added within v1, while WhatsApp Flows are\nswitched on: flow_reply: {flow_token?, response}, a completed form; with context.message_id set to a flow\nmessage and no flow_token, the token of that message is used.\n","type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["text","image","video","audio","document","sticker","location","contacts","interactive_reply","reaction","unsupported","flow_reply"]}}},"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","channel_health","template_status"]},"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"}]}}},"SandboxChannelHealthRequest":{"description":"A health scenario for a simulated channel.","type":"object","additionalProperties":false,"required":["status"],"properties":{"status":{"type":"string","enum":["green","amber","red"]},"messaging_limit":{"description":"The messaging limit in the event (default TIER_1K).","type":"string","enum":["TIER_250","TIER_1K","TIER_10K","TIER_100K","TIER_UNLIMITED"]},"quality":{"description":"The quality rating in the event (default GREEN).","type":"string","enum":["GREEN","YELLOW","RED","UNKNOWN"]}}},"SandboxTemplateStatusRequest":{"description":"A new status of a simulated channel's template (Meta's event value).","type":"object","additionalProperties":false,"required":["status"],"properties":{"status":{"type":"string","enum":["APPROVED","PENDING","REJECTED","PAUSED","DISABLED"]},"reason":{"description":"Only with REJECTED; Meta's reason value, NONE if left out.","type":"string","enum":["ABUSIVE_CONTENT","INCORRECT_CATEGORY","INVALID_FORMAT","NONE","PROMOTIONAL","SCAM","TAG_CONTENT_MISMATCH"]}}},"MediaLink":{"description":"A short-lived signed download link of a media file. url works without the API key until expires_at, for at\nmost max_uses requests; only the expiry revokes it.\n","type":"object","additionalProperties":false,"required":["object","media_id","url","expires_at","max_uses"],"properties":{"object":{"const":"media_link"},"media_id":{"$ref":"#/components/schemas/Id"},"url":{"description":"The full address on this API host, with team, key_id, expires and signature in the query string.","type":"string"},"expires_at":{"$ref":"#/components/schemas/Timestamp"},"max_uses":{"type":"integer"}}},"WebhookEndpointList":{"type":"object","additionalProperties":false,"required":["object","data","next_cursor"],"properties":{"object":{"const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}},"next_cursor":{"type":["string","null"]}}},"WebhookEndpoint":{"description":"A webhook endpoint of the company, as the panel's Webhooks page shows it. url is masked (scheme, host, the port\nwhen it is not 443, and /...): the full address is never shown. secret is the signing secret, only in the\nanswer that creates the endpoint and in the answer of a rotation; null everywhere else. Accept unknown event\ntypes, states and reasons.\n","type":"object","additionalProperties":false,"required":["object","id","url","events","channel_id","ordering","max_in_flight","api_version","state","consecutive_failures","failing_since","suspended_at","suspended_reason","disabled_at","last_success_at","last_failure_at","verification","secret_rotating_until","weekly_test","secret","created_at"],"properties":{"object":{"const":"webhook_endpoint"},"id":{"$ref":"#/components/schemas/Id"},"url":{"type":"string"},"events":{"description":"The subscribed event types (webhook.test always goes and is not listed).","type":"array","items":{"type":"string"}},"channel_id":{"description":"The channel filter; null for every channel.","anyOf":[{"$ref":"#/components/schemas/Id"},{"type":"null"}]},"ordering":{"type":"string","enum":["best_effort","strict"]},"max_in_flight":{"type":"integer"},"api_version":{"type":"string"},"state":{"description":"pending_verification: new or with a new address, gets no events until it is verified and activated.\nactive. failing: 20 failures in a row, one attempt a minute. suspended: failing for 72 hours or answered\n410, events are held for at most 7 days. disabled: turned off.\n","type":"string","enum":["pending_verification","active","failing","suspended","disabled"]},"consecutive_failures":{"type":"integer"},"failing_since":{"$ref":"#/components/schemas/NullableTimestamp"},"suspended_at":{"$ref":"#/components/schemas/NullableTimestamp"},"suspended_reason":{"description":"gone (the endpoint answered 410) or failing (72 hours).","type":["string","null"]},"disabled_at":{"$ref":"#/components/schemas/NullableTimestamp"},"last_success_at":{"$ref":"#/components/schemas/NullableTimestamp"},"last_failure_at":{"$ref":"#/components/schemas/NullableTimestamp"},"verification":{"description":"Whether a test event and the wrong-signature probe passed within the last hour, and their last HTTP status.","type":"object","additionalProperties":false,"required":["test_passed","probe_passed","probe_required","last_test_status","last_probe_status"],"properties":{"test_passed":{"type":"boolean"},"probe_passed":{"type":"boolean"},"probe_required":{"description":"Activation needs the probe too (a new endpoint or a new address).","type":"boolean"},"last_test_status":{"type":["integer","null"]},"last_probe_status":{"type":["integer","null"]}}},"secret_rotating_until":{"description":"Until when the secret replaced by the last rotation still signs; null when none does.","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$"},"weekly_test":{"description":"The optional weekly test event: UPPU sends webhook.test once a week to an active or failing endpoint that\nopted in. A failure only sets failed_at (a warning); the endpoint is never suspended for it. A passing\nweekly test or test event, turning the weekly test off and a new url clear failed_at. last_status is null\nwhen no HTTP answer came (for example a refused address or a timeout).\n","type":"object","additionalProperties":false,"required":["enabled","last_sent_at","last_status","failed_at"],"properties":{"enabled":{"type":"boolean"},"last_sent_at":{"$ref":"#/components/schemas/NullableTimestamp"},"last_status":{"type":["integer","null"]},"failed_at":{"$ref":"#/components/schemas/NullableTimestamp"}}},"secret":{"description":"whsec_ and 44 base64 characters; only in the answer that creates the endpoint and in the answer of a rotation.","type":["string","null"],"pattern":"^whsec_[A-Za-z0-9+/]{43}=$"},"created_at":{"$ref":"#/components/schemas/Timestamp"}}},"WebhookEndpointCreateRequest":{"type":"object","additionalProperties":false,"required":["url"],"properties":{"url":{"description":"Your https address on port 443, at most 2048 characters.","type":"string","maxLength":2048},"events":{"description":"At least one type; left out, the types that are on by default.","type":"array","items":{"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","contact.reachability_changed","contact.updated","account.service_stop_scheduled","account.service_resumed"]}},"channel_id":{"description":"Only the events of this channel; null for every channel.","anyOf":[{"$ref":"#/components/schemas/Id"},{"type":"null"}]},"ordering":{"type":"string","enum":["best_effort","strict"],"default":"best_effort"},"max_in_flight":{"type":"integer","minimum":1,"maximum":32,"default":8},"weekly_test":{"description":"Send a test event every week (a failure is only a warning).","type":"boolean","default":false}}},"WebhookEndpointUpdateRequest":{"description":"Only the fields you send change.","type":"object","additionalProperties":false,"properties":{"url":{"description":"A new address sends the endpoint back to verification.","type":"string","maxLength":2048},"events":{"type":"array","items":{"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","contact.reachability_changed","contact.updated","account.service_stop_scheduled","account.service_resumed"]}},"channel_id":{"anyOf":[{"$ref":"#/components/schemas/Id"},{"type":"null"}]},"ordering":{"type":"string","enum":["best_effort","strict"]},"max_in_flight":{"type":"integer","minimum":1,"maximum":32},"weekly_test":{"type":"boolean"}}},"WebhookEndpointTestRequest":{"type":"object","additionalProperties":false,"properties":{"type":{"description":"The event type of the sample (webhook.test if left out).","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","contact.reachability_changed","contact.updated","account.service_stop_scheduled","account.service_resumed","webhook.test"]}}},"WebhookEndpointCheck":{"description":"The result of a test event or of the wrong-signature probe.","type":"object","additionalProperties":false,"required":["object","endpoint_id","kind","type","passed","status","latency_ms","response_excerpt","error","checked_at"],"properties":{"object":{"const":"webhook_endpoint_check"},"endpoint_id":{"$ref":"#/components/schemas/Id"},"kind":{"type":"string","enum":["test","probe"]},"type":{"type":"string"},"passed":{"description":"test: your server answered 2xx. probe: your server refused the wrong signature (any answer other than 2xx).","type":"boolean"},"status":{"description":"The HTTP status; null when no answer came (for example a refused address or a timeout).","type":["integer","null"]},"latency_ms":{"type":"integer"},"response_excerpt":{"description":"The first characters of your server's answer.","type":["string","null"]},"error":{"description":"Why the request failed, the address masked; null when your server answered.","type":["string","null"]},"checked_at":{"$ref":"#/components/schemas/Timestamp"}}},"DeletedWebhookEndpoint":{"type":"object","additionalProperties":false,"required":["object","id","deleted"],"properties":{"object":{"const":"webhook_endpoint"},"id":{"$ref":"#/components/schemas/Id"},"deleted":{"const":true}}}}}}
