Getting started with the UPPU API
The UPPU API v1 lets your own systems send and receive WhatsApp, Messenger, Instagram, Telegram and web chat messages through UPPU. Your code talks only to UPPU: UPPU talks to the messaging networks, keeps the reply window and consent rules, and sends every event to you as a signed webhook.
This guide takes you from the first key to production. The API reference lists every endpoint and field, the webhooks page every event, and the code examples show the common tasks in PHP, Node.js, Python and C#.
Environments
| Production | Staging | |
|---|---|---|
| Base URL | https://messaging.uppu.net/v1 |
https://messaging-staging.uppu.net/v1 |
| Key prefix | uppu_live_ |
uppu_test_ |
| Simulated WhatsApp channel | no | yes |
- A key works only in its own environment. A staging key sent to production, or the other way round, gets
401 wrong_environment. - Call the API from your servers only. A request with an
Originheader (a browser) gets403 browser_request_refused, and no CORS headers are sent. Never put a key into a web page or a mobile app. - Requests and responses are UTF-8 JSON. A request body is at most 1 MB; a file upload at most 25 MB.
- The machine-readable description is the OpenAPI document at
GET /v1/openapi.json(no key needed). The API reference shows the same document as a page.
Before you start
- Your company's plan includes the API. UPPU sets the plan. Without the API every request gets
403 api_disabledwithdetails.reasonnot_in_plan, and no events are sent. Ask your UPPU contact. - A channel is connected (WhatsApp, Messenger, Instagram, Telegram or web chat). On a channel that your own system answers, the company owner sets the channel's Handling to External system (API) on the channels page: the team inbox then does not auto-assign its conversations, and agents can still read and reply.
- The company owner creates a key in the UPPU panel: the company's settings, then Developers. Only the owner manages keys and webhooks, after confirming the password.
API keys
- Format:
uppu_live_(oruppu_test_), a 12-character prefix,_and a 43-character secret. The panel shows the full key once, at creation. Store it in your secret store and never write it to a log; UPPU shows only the prefix afterwards. - Header: every request carries
Authorization: Bearer <key>. - Options: a name and an author label (the inbox shows your messages as "API · label"), the scopes, one channel or all channels, an optional list of allowed IP ranges (CIDR) and an optional expiry.
- Channel binding: a key bound to one channel sees only that channel. An id from another channel, or another company, answers
404 not_found: ids cannot be probed. - Rotation: "Rotate" creates a new key with the same settings, and the old key keeps working for a grace period (24 hours by default, at most 7 days). Deploy the new key, then let the old one expire.
- Revocation takes effect on the next request. The owner can also change the scopes and the channel of an active key without changing its secret.
- UPPU e-mails the owner about a key that has not been used for 90 days; it never revokes a key on its own.
Scopes
A key carries scopes (the panel calls them permissions). The panel offers three presets; "custom" picks scopes one by one. A missing scope gets 403 insufficient_scope with details.required.
| Scope | Allows | Endpoints | Bot | Notification sender | Read only |
|---|---|---|---|---|---|
| Every key | Read the channels of the key | GET /v1/channelsGET /v1/channels/{id} | yes | yes | yes |
messages:send | Send messages, read receipts and typing indicators | POST /v1/conversations/{id}/typingPOST /v1/messagesPOST /v1/messages/{id}/read | yes | yes | — |
messages:read | Read messages and conversations | GET /v1/conversationsGET /v1/conversations/{id}GET /v1/messagesGET /v1/messages/{id} | yes | yes | yes |
conversations:write | Resolve and hand over conversations | POST /v1/conversations/{id}/handoverPOST /v1/conversations/{id}/releasePOST /v1/conversations/{id}/resolve | yes | — | — |
media:write | Upload media | POST /v1/media | yes | yes | — |
media:read | Download media | GET /v1/media/{id}GET /v1/media/{id}/contentPOST /v1/media/{id}/link | yes | — | yes |
templates:read | Read templates | GET /v1/templatesGET /v1/templates/{id} | yes | yes | yes |
contacts:read | Look up and read contacts | GET /v1/contacts/{id}GET /v1/contacts/{id}/consentsPOST /v1/contacts/lookup | yes | yes | yes |
consents:write | Record consents | POST /v1/consents | — | yes | — |
events:read | Read events | GET /v1/events | — | yes | yes |
webhooks:manage | Manage webhook endpoints | DELETE /v1/webhook-endpoints/{id}GET /v1/webhook-endpointsGET /v1/webhook-endpoints/{id}PATCH /v1/webhook-endpoints/{id}POST /v1/webhook-endpointsPOST /v1/webhook-endpoints/{id}/activatePOST /v1/webhook-endpoints/{id}/disablePOST /v1/webhook-endpoints/{id}/probePOST /v1/webhook-endpoints/{id}/secretPOST /v1/webhook-endpoints/{id}/test | — | — | — |
reports:read | Read reports | GET /v1/reports/dailyGET /v1/reports/summary | — | — | — |
templates:write | Create and delete WhatsApp templates | DELETE /v1/templates/{id}POST /v1/templates | — | — | — |
The simulator endpoints (/v1/sandbox/...) exist only on staging and need messages:send; /v1/sandbox/action-target needs no key (see the staging simulator below).
The staging simulator
On staging the company owner can add up to three simulated WhatsApp channels on the Developers page. A simulated channel behaves like a real WhatsApp channel through the API and the webhooks (reply window, consent rules, idempotency, statuses, media, events), but nothing reaches Meta: statuses arrive a few seconds apart as message.status events. Create a test key bound to the simulated channel with the Bot preset.
Magic recipients. The last two digits of the contact's phone number choose what happens to a message you send:
| Last two digits | Result |
|---|---|
| any other | accepted, sent, delivered, read |
01 |
accepted, sent, delivered (never read) |
02 |
accepted, sent (no delivery report) |
03 |
accepted, then failed with recipient_unreachable |
04 |
accepted, then failed with window_closed |
05 |
accepted, then failed with marketing_opt_out (UPPU records the withdrawal) |
06 |
accepted, then failed with payment_required |
07 |
refused at once: failed with invalid_parameter |
08 |
rate limited: UPPU retries after 10, 60 and 300 seconds, then failed with provider_rate_limited |
09 |
no answer: unknown, then delivered |
Use numbers reserved for fiction in tests, for example +447700900100. Once ttl is switched on, a send to 08 with a short ttl (for example 30 seconds) ends sooner: UPPU stops retrying when the ttl runs out, and the message ends failed with provider_rate_limited, not expired, because the network answered with an error.
Template set (all in language tr): uppu_sim_order_update (UTILITY, two positional body parameters), uppu_sim_appointment (UTILITY, named customer_name and appointment_time, two quick-reply buttons), uppu_sim_payment (UTILITY, a text header with a variable), uppu_sim_campaign (MARKETING: needs a recorded consent), uppu_sim_login_code (AUTHENTICATION: always refused) and uppu_sim_pending (not approved).
Simulator endpoints (test key, messages:send):
POST /v1/sandbox/inbound: a message from a person, in the shape of a received message's content. While the simulated channel listsflow,flow_reply({"response": {…}}, withcontext.message_idof your flow message) simulates the person completing your form.POST /v1/sandbox/statuses: a delivery status for one of your messages.POST /v1/sandbox/identity-rotation: the person's WhatsApp user id changes.POST /v1/sandbox/contacts/{id}/close-window: moves the person's last message 25 hours back, so the reply window is closed.POST /v1/sandbox/channels/{id}/health: the simulated channel's health becomesgreen,amberorred, with thechannel.health_changedevent of a real channel.POST /v1/sandbox/templates/{id}/status: a simulated template becomesAPPROVED,PENDING,REJECTED,PAUSEDorDISABLED, with thetemplate.updatedevent of a real template.
Simulated traffic is not counted in the company's usage. UPPU can delete the data of simulated channels on request; real channels are never touched.
/v1/sandbox/action-target is not part of the API you call: on staging, UPPU Bot's actions call it in place of a company's own API. It needs no key and limits each client address; the reference describes it.
Production has no /v1/sandbox paths.
Your first message
The steps use staging and curl. Put the key into the environment first:
export UPPU_API_BASE=https://messaging-staging.uppu.net/v1
export UPPU_API_KEY=uppu_test_...
1. Find your channel. Note the id of the simulated channel.
curl -s "$UPPU_API_BASE/channels" -H "Authorization: Bearer $UPPU_API_KEY"
2. Let a person write to you. WhatsApp lets you send a free-form message only within 24 hours of the person's last message, so simulate one. The answer is a sandbox_event whose contact_id is the person.
curl -s -X POST "$UPPU_API_BASE/sandbox/inbound" \
-H "Authorization: Bearer $UPPU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"channel_id": "<channel id>", "from": {"phone": "+447700900100", "name": "Deniz"}, "content": {"type": "text", "text": {"body": "Hello"}}}'
3. Reply. Every send carries an Idempotency-Key: the same key is reused for every retry of this send, so a retry never sends twice.
curl -s -X POST "$UPPU_API_BASE/messages" \
-H "Authorization: Bearer $UPPU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ticket-42-reply-1" \
-d '{"channel_id": "<channel id>", "to": {"contact_id": "<contact id>"}, "content": {"type": "text", "text": {"body": "Thanks, we are on it."}}, "client_reference": "ticket-42"}'
The answer is 202 with the message, status queued.
4. Follow the status. GET /v1/messages/{id} shows it move to accepted, sent, delivered and read; with a webhook endpoint the same steps arrive as message.status events.
curl -s "$UPPU_API_BASE/messages/<message id>" -H "Authorization: Bearer $UPPU_API_KEY"
5. Send a template. An approved template also goes outside the 24-hour window:
{
"channel_id": "<channel id>",
"to": {"contact_id": "<contact id>"},
"content": {
"type": "template",
"template": {"name": "uppu_sim_order_update", "language": "tr", "body": {"parameters": ["Deniz", "A-1001"]}}
},
"client_reference": "order-A-1001"
}
Sending messages
POST /v1/messages sends every kind of message; GET /v1/channels tells you what a channel accepts (capabilities.content_types, capabilities.limits).
- Recipient (
to): exactly one ofcontact_id(recommended),phone(E.164 with+, WhatsApp only; an unknown number becomes a new contact) oridentity, an identity that already exists on the channel (bsuid,psid,igsid,web_visitor,telegram). Find a contact by phone withPOST /v1/contacts/lookup: personal data never goes into a URL. - Content:
text,interactive(buttons, list, quick replies),image,video,audio,document(bymedia_idfromPOST /v1/media, or by anhttpslink that UPPU downloads),reaction,locationand, on WhatsApp,template. On WhatsApp and Telegram alsosticker(a 512×512 WebP uploaded withPOST /v1/mediaand the form fieldkind=sticker, sent bymedia_id; static up to 100 KB, animated up to 500 KB, Telegram static only) andcontacts(contact cards in the shape of a received one: a name and at least one phone number or e-mail address; Telegram takes one card with one phone). On WhatsApp and Messengerinteractivealso takescta_url, one button that opens anhttpsaddress in the person's browser ("cta_url": {"display_text": "…", "url": "…"}; the button text has at most 20 characters, the address goes as written and uses only the characters of a web address: encode a space as%20). On WhatsAppflowopens a WhatsApp Flow that you published in your WhatsApp account ("flow": {"flow_id": "…", "flow_cta": "…"}; navigate flows only) once the channel listsflowincapabilities.interactive_types; the person's completed form comes back onmessage.receivedasflow_reply, with theflow_tokenof your message. client_reference: your own reference (at most 64 characters, no personal data). It comes back in the message and in everymessage.statusevent.ttl(optional): the message's time to live, in whole seconds from 10 to 86400. If UPPU could not hand the message to WhatsApp, Messenger, Instagram or Telegram within that time (it waited in the queue, for the rate limit or for a retry, or the channel could not send), it is not sent:statusexpired,error.codettl_expiredand amessage.statusevent. If an earlier attempt reached the network and it answered with an error (for example its rate limit), UPPU stops retrying when thettlruns out and the message endsfailedwith that error instead (for exampleprovider_rate_limited). A message already handed over is never recalled; on web chat the message is delivered at once, sottlchanges nothing there. The field works once UPPU switches it on; until then it gets422 validation_failed(unknown_field).- Answer:
202with the message (statusqueued). Then the statuses:accepted,sent,delivered,readorfailed(andexpiredwhen attlran out); a channel reports only some of them (capabilities.delivery_statuses).unknownmeans UPPU could not confirm whether the network took the message: never resend it automatically. - Read receipts:
POST /v1/messages/{id}/readmarks a WhatsApp message of the person as read. - Typing indicator:
POST /v1/conversations/{id}/typingshows "typing…" to the person while you prepare an answer (WhatsApp, Messenger and Telegram;capabilities.typing_indicator). It is not a message: nothing is stored or billed and no webhook follows. It ends when your message arrives or after a few seconds (WhatsApp 25, Telegram 5); on WhatsApp it also marks the person's newest message as read. At most one every few seconds per conversation. - Conversations:
GET /v1/conversationslists them andPOST /v1/conversations/{id}/resolvecloses one (conversations:write); the person's next message opens a new one. On the channels your system or UPPU Bot answers,handoverandreleasemove a conversation between them and your team (see handover).
The reply window and templates
- WhatsApp, Messenger and Instagram accept text, interactive messages, media, reactions and locations only within 24 hours of the person's last message. The window counts from the network's timestamp of that message; a message the network delivers late can arrive with the window already closed (
service_window.openis false inmessage.received). On Messenger and Instagram a reaction from the person opens the window too. Outside the window the send gets422 window_closed: on WhatsApp send an approved template, on Messenger and Instagram wait until the person writes or reacts again. - Telegram has no window, but a bot can write only to a person who wrote to it first.
- Web chat needs an open conversation. A file goes only by
media_id(no link); the visitor opens it in the chat window. - Template rules (WhatsApp): a MARKETING template needs a recorded marketing consent; a UTILITY template needs a recorded utility consent or a message from the person after their latest withdrawal; AUTHENTICATION templates are never sent. Record a consent with
POST /v1/consents, or inline on a template send (both needconsents:write). A person who writes STOP, DUR or İPTAL withdraws both purposes. - Template headers and buttons (WhatsApp): a template whose header is an image, a video or a PDF document takes the file as
"header": {"type": "image", "image": {"media_id": "…"}}: upload it first withPOST /v1/mediaon the same channel. A URL button whose address ends in a variable takes its value by the button's position,"buttons": [{"index": 0, "type": "url", "parameter": "A-1001"}]; a quick reply may carry apayload, which comes back as theidof the person's tap. Buttons without a variable need nothing.GET /v1/templatessays what each template needs (parameters.header_media,parameters.buttons) and gives asend_example. A buttons message on WhatsApp takes an image, video or document header the same way.
Idempotency
POST /v1/messagesrequires anIdempotency-Keyheader: 1 to 64 letters, digits,.,_,:or-.- The same key with the same body answers
200with the current state of the first message andIdempotent-Replayed: true. The same key with another body gets409 idempotency_key_reused. - A key stays unique per channel for good. Use one key per logical send (a UUID, a ULID or your own id such as
order-1001-shipped) and reuse it for every retry of that send: after a network error, a429or a5xx. - Build every value of the body from the logical send, never from the attempt or the current time, so a retry is the same request.
- A refused send (for example
window_closed) keeps no key: the same key can be used again once the condition has changed. - To send again a message that ended as
failed, use a new key. POST /v1/consents,POST /v1/mediaandPOST /v1/templatestake an optional key; read receipts need none (a repeat answers200).
Rate limits
| Limit | Value |
|---|---|
| Per key, every endpoint | 100 requests in any second and 3,000 per minute |
| Per key, sends and read receipts (POST /v1/messages, POST /v1/messages/{id}/read) | 20 requests per second |
| Per key, file uploads (POST /v1/media) | 2 requests per second and 1 GB per day |
| Per key, file downloads (GET /v1/media/{id}/content) | 10 requests per second and 2 GB per hour |
| Per key, missed events (GET /v1/events) | 2 requests per second |
| Typing indicators (POST /v1/conversations/{id}/typing) | 20 requests per second per key and one every 3 seconds per conversation |
| Per company, webhook endpoint test events and probes (POST /v1/webhook-endpoints/{id}/test and /probe) | 10 requests per minute, one at a time |
| Signed media links (POST /v1/media/{id}/link) | A link works for 300 seconds and 30 requests; each download counts against the hourly media bytes of the key that issued it |
| Per key, reports (GET /v1/reports/summary and /daily) | 2 requests per second |
| Per company, template creates and deletes (POST /v1/templates, DELETE /v1/templates/{id}) | 10 requests per minute, one at a time; Meta allows 100 new templates per hour for a WhatsApp account |
| Invalid keys from one network | 30 per minute, then every request gets 429 for 10 minutes |
| Waiting sends per channel | From 10,000 waiting sends, or when the oldest has waited 15 minutes, a new send gets 429 channel_backlog |
- A request over a limit gets
429 rate_limitedwithRetry-After(seconds). Wait that long and send the same request again. - Successful responses carry
RateLimit-Limit,RateLimit-RemainingandRateLimit-Resetfor the per-key limit; the sending limit is separate, so keep your own sending pace below it. - UPPU paces delivery to the networks itself. A large backlog on a channel gets
429 channel_backlog; retry afterRetry-After.
Errors
Every error has the same envelope:
{
"error": {
"type": "policy",
"code": "window_closed",
"status": 422,
"retryable": false,
"message": "The contact has not written in the last 24 hours: only an approved template can be sent.",
"request_id": "01jace0e1v2m4n6p8q0r2s4t6a",
"doc_url": "https://messaging.uppu.net/docs/errors#window_closed",
"details": {"window_closed_at": "2026-10-19T08:58:00.000Z", "hint": "send_template"},
"provider_error": null
}
}
codeis the stable contract; branch on it.messageis for people and followsAccept-Language(Turkish by default, English on request).typeis one ofauthentication,permission,invalid_request,conflict,policy,rate_limitandserver.retryable: truemeans: wait (Retry-Afterwhen present) and send the same request again with the sameIdempotency-Key.- A validation error lists the fields:
details.fieldsholds{field, code, message}items. - Every response carries
X-Request-Id. Send your own (at most 64 visible characters) to find a request later, and quote it when you contact UPPU. - Errors that happen after the
202, while the network delivers the message, arrive asfailedinmessage.statusand in the message'serror. - The error codes page lists every code with its HTTP status and meaning.
Webhooks
UPPU sends every event (a received message, a status, a changed identity) to your HTTPS endpoint as a signed POST. Verify the signature over the raw body, answer 2xx within 10 seconds, drop repeats by webhook-id and process the event afterwards. Events you missed stay available for 7 days at GET /v1/events. The webhooks page explains the set-up (in the panel or through the API), the signature, retries and every event with an example. To hand a file over without your key, POST /v1/media/{id}/link returns a signed download link that works for a few minutes; its downloads count against your key's hourly media bytes.
Handover to your team
On a channel your own system answers (handling api) or UPPU Bot answers (bot), the automation holds each conversation (held_by is api or bot) until it is handed over to your team (held_by team). Both calls need conversations:write (the Bot preset has it).
- Hand over:
POST /v1/conversations/{id}/handover, with an empty body or{"reason": "address_change", "note": "The person wants to change the delivery address."}. The conversation joins the team inbox (Unassigned); when the channel's automatic assignment is on, an available agent gets it, and the team gets an e-mail when nobody replies within 10 minutes. On a bot channel UPPU Bot stops at once. Nothing is sent to the person: send your own message first if you want to. Yourreason(a short code without personal data) andnoteappear only in the team inbox; the API and the webhooks never return them. - Release:
POST /v1/conversations/{id}/releasegives the conversation back: to your system on an api channel, to UPPU Bot on a bot channel. UPPU Bot then answers only the person's next message, so the person's last message must have a reply first (from the team or from your system), otherwise409 bot_not_readywithdetails.reasonreply_first. - Idempotent: a conversation already in the requested state answers
200again and nothing is written twice, so retry freely. A resolved conversation gets409 not_heldor409 not_handed_over; a team inbox channel422 handling_not_supported. - State: the conversation carries
held_byandhandover_reason:apifor your own handover, or UPPU Bot's reason, for examplekeyword(the person asked for a person) oragent_takeover(an agent replied). - Events: subscribe your endpoint to
conversation.updatedto follow every handover, return and release, and every resolve, reopen and assignment of the team (the event). New endpoints leave it off. A change of the channel's handling in the panel moves the channel's open conversations without an event: read them again when the channel'shandlingchanges.
Templates through the API
With the templates:write scope, which no preset has (choose it under "custom"), your system creates and deletes the WhatsApp templates of a channel's WhatsApp Business account, with the rules of the panel's template page.
- Create:
POST /v1/templates. The answer is201with the template, usuallyPENDING: Meta reviews it, mostly within 24 hours, andtemplate.updatedreports the result.
{
"channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
"name": "order_update",
"language": "tr",
"category": "UTILITY",
"header": {"type": "text", "text": "Sipariş {{1}}", "example": "A-1001"},
"body": {"text": "Merhaba {{1}}, siparişiniz yola çıktı.", "examples": ["Ayşe"]},
"footer": {"text": "Bilgilendirme"},
"buttons": [{"type": "quick_reply", "text": "Teşekkürler"}]
}
- Rules: a name of lowercase letters, digits and underscores; language
troren; categoryUTILITYorMARKETING; variables either numbered ({{1}},{{2}}…) or named ({{first_name}}), each with a one-line example (examplesis a list for numbered variables or an object by name); an optional text header with at most one variable and a footer without variables; up to 3 quick replies, 2 URL buttons ({"type": "url", "text": …, "url": …, "example": …}: a variable only as{{1}}at the end of the address, with an example of that end) and 1 phone number button (E.164 with a plus sign), the quick replies next to each other. A header with an image, a video or a document takes a sample file that you upload first to the same channel withPOST /v1/media:"header": {"type": "image", "image": {"media_id": "…"}}(a JPEG or PNG image up to 5 MB, an MP4 video up to 16 MB, a PDF document up to 25 MB). UPPU uploads the sample to Meta for the review; every message you send with the template still carries its own file. The upload and the create run within your request (about 26 seconds): when the upload takes too long you get503 provider_unavailablewithdetails.reasonunreachableand nothing is created; retry, or use a smaller sample. A broken rule gets422 validation_failedwith the field's path, for examplebody.examples.1. - Limits: the same name and language twice, or a name whose every language was deleted within 30 days, gets
422(fieldname); Meta allows 100 new templates per hour for a WhatsApp account (429withRetry-After). UPPU calls Meta during your request, one create or delete at a time per company (otherwise429withdetails.reasonconcurrent_writes). When Meta's own rate limit is reached you get429withdetails.reasonprovider_rate_limited,Retry-Afterand Meta's code inprovider_error. When Meta cannot be reached you get503 provider_unavailable; withdetails.reasonunconfirmed(no answer in time, or a temporary error at Meta) the template may have been created. Retry with the sameIdempotency-Keyand template: UPPU looks at Meta again and answers200with the template (Idempotent-Replayed: true) if the earlier request created it, or creates it if it did not. Without a key, readGET /v1/templates?name=&language=before you retry. When Meta refuses the access token of the channel's account you get409 channel_unavailablewithdetails.reasoncredentials. An optionalIdempotency-Keymakes a retry safe. - Delete:
DELETE /v1/templates/{id}deletes the template in that language only; the other languages of the name stay. The template stays readable with statusDELETED, and deleting it again answers the same.
Reports
With the reports:read scope, also in no preset, GET /v1/reports/summary returns the figures of the panel's Reports page and GET /v1/reports/daily the same figures day by day. They are counted every night: counts only, no contact, conversation or message.
- Period:
days=7,30(the default) or90finished days, orfromandto(calendar days in your company's time zone, at most 90 days). The period ends on the last day the nightly count has finished:period.tosays which, andperiod.pendingistruewhile yesterday is still being counted. - Scope: the whole company, or one of
channel_id,member_idandgroup_id(the ids in the summary'sbreakdowns). A key bound to one channel reads only its channel. A channel UPPU stopped for the API is one row withhiddentrueand no id or name among the channels; the totals count every channel of your company. - Figures: the conversations the contacts opened and those that reached your team, the ones left unanswered on their day, the team's replies, first response and resolution times (per bucket, with the median, the 90th percentile and the average), resolutions and reopenings, the open conversations at the end, SLA targets met and missed, topics and tags; for the whole company also per channel, agent and group.
- Reports are part of the messaging panel: a company whose plan does not include it gets
403 api_disabledwithdetails.resourcereports. At most 2 requests per second per key.
Compatibility
Within v1 UPPU adds, and never removes or renames:
- new fields, endpoints, event types, content types, error codes and enum values may appear at any time;
- ignore fields and events you do not know, treat an unknown
content.typeasunsupported, and accept enum values you do not know (for example a newauthor.typeor the messagestatusexpired); - breaking changes come only as
/v2, with at least 12 months of overlap.
Personal data
- Key your users by
contact_id, never by phone number: a phone can be absent, and WhatsApp user ids can change (contact.identity_changed). - Do not put personal data into
client_reference,Idempotency-Key,source_refor URLs. - When UPPU erases a person, you get
contact.erased: delete your copy too.
Going live
- A production key (
uppu_live_) created in the production panel, stored in your secret store and bound to the right channel. - A webhook endpoint registered and activated in production, with signature verification, de-duplication by
webhook-idand a fast2xx. - Every send has a stable
Idempotency-Key; retries reuse it. window_closed,consent_required,failedandexpiredstatuses are handled as expected outcomes, not as alarms.- Users are keyed by
contact_id;contact.identity_changedandcontact.erasedare handled. - After an outage your system catches up with
GET /v1/events, or reconciles withGET /v1/messages.