Skip to content

Developers

One API for WhatsApp, Instagram, Messenger and live chat.

REST API v1 with a public OpenAPI document, a required Idempotency-Key on sends and events signed with Standard Webhooks. Examples in curl, PHP, Node.js and Python.

  • OpenAPI 3.1 document
  • Signed, ordered webhooks
  • Examples in four languages
POST/v1/messages

Request

curl -sS -X POST "https://messaging.uppu.net/v1/messages" \
  -H "Authorization: Bearer $UPPU_API_KEY" \
  -H "Idempotency-Key: order-1001-shipped" \
  -H "Content-Type: application/json" \
  -d '{"channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
       "to": {"contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v"},
       "content": {"type": "text",
                   "text": {"body": "Your order is on its way."}},
       "client_reference": "order-1001"}'
202 Acceptedthen message.status to your webhook

Start here

Environments

UPPU runs two separate environments, each with its own panel, keys and data. Staging is for testing integrations; nothing in it reaches your live workspace.

Environments
EnvironmentPanelAPI base URLKey prefix
Livehttps://app.uppu.nethttps://messaging.uppu.net/v1uppu_live_
Staginghttps://staging.uppu.nethttps://messaging-staging.uppu.net/v1uppu_test_

A key used in the other environment gets 401 wrong_environment. The API answers only on these two hosts, and the panel is never served there.

Access and API keys

  1. Ask for API access

    The API is off for every workspace until the UPPU team turns it on. Tell us what you want to build and we enable it for your workspace.

  2. Create a key in the panel

    The workspace owner creates keys under team settings, «Geliştiriciler» (Developers). Creating a key asks for the owner's password, and the key is shown only once.

  3. Call from your server

    Send the key as Authorization: Bearer … on every request. A request that carries an Origin header gets 403 browser_request_refused and no CORS headers are sent, so keep keys out of browsers and mobile apps.

Keys

  • Format: uppu_live_, 12 characters, _, 43 characters (uppu_test_ on staging). The prefix lets secret scanners spot a leaked key.
  • UPPU stores only a SHA-256 hash of each key. A workspace can have up to 25 active keys, and the owner gets an email about a key that hasn't been used for 90 days. Keys are never revoked automatically.
  • Options per key: scopes, one channel or all channels, an IP allowlist (CIDR ranges) and an expiry of 30, 90, 180 or 365 days. Revocation takes effect immediately.
  • Rotation («Yenile» in the panel) creates a twin key with the same scopes, channel and IP ranges. The old key keeps working for a grace period: 24 hours by default, at most 7 days.
  • The owner can change the scopes and the channel of an active key without changing its secret; the change applies to the next request.
Scopes
ScopeAllows
any keyGET /v1/channels and GET /v1/channels/{id}, limited to the key's channels
messages:sendPOST /v1/messages and POST /v1/messages/{id}/read
messages:readGET /v1/messages and GET /v1/messages/{id}
templates:readGET /v1/templates and GET /v1/templates/{id}
contacts:readPOST /v1/contacts/lookup
media:writePOST /v1/media
media:readGET /v1/media/{id} and GET /v1/media/{id}/content
events:readGET /v1/events
consents:writeAn inline consent on a template send
conversations:writeCan be granted; no endpoint uses it today
Scope presets in the panel
PresetScopes
botmessages:send, messages:read, conversations:write, media:read, media:write, templates:read, contacts:read
«Bildirim gönderici» (notifier)messages:send, messages:read, media:write, templates:read, contacts:read, consents:write, events:read
«Salt okuma» (read_only)messages:read, media:read, templates:read, contacts:read, events:read

A preset is copied into the key when you create it. If a bot key also reads the event backlog, add events:read.

Quickstart

From a key to a delivered message. The examples use the staging base URL and a uppu_test_ key; for live, change the two variables. The IDs are made up: take yours from step 1.

Set up: base URL, key and a small helper

cURL

# Staging; for live use https://messaging.uppu.net/v1 and a uppu_live_ key.
# Never commit or log the key.
export UPPU_API_BASE="https://messaging-staging.uppu.net/v1"
export UPPU_API_KEY="uppu_test_XXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

Keep the key in your secret store or environment. Never put it in code, logs, tickets or a browser.

1. List your channels

GET/v1/channels

cURL

curl -sS "$UPPU_API_BASE/channels" \
  -H "Authorization: Bearer $UPPU_API_KEY" \
  -H "Accept: application/json"
Response (abridged)
{"object": "list",
 "data": [{"object": "channel",
           "id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
           "type": "whatsapp",
           "handling": "inbox",
           "sending": "active",
           "capabilities": {"content_types": ["text", "interactive", "image", "video", "audio",
                                              "document", "template", "reaction", "location"],
                            "limits": {"text_body": 4000, "buttons": 3},
                            "window_hours": 24,
                            "phone_addressing": true}}],
 "next_cursor": null}

capabilities tells you what each channel can send: content types, field limits and the reply window. A key bound to one channel sees only that channel.

2. Find a sendable template

GET/v1/templates

cURL

curl -sS "$UPPU_API_BASE/templates?channel_id=01j9z0ch5k2m4n6p8q0r2s4t6v&status=APPROVED" \
  -H "Authorization: Bearer $UPPU_API_KEY"

Templates belong to the WhatsApp Business account behind a channel; they're synced with Meta every 6 hours and when you ask in the panel. Each template carries sendable, unsendable_reason, a parameter schema and a send_example: a ready content object built from the template's own example values.

3. Send an approved template

On WhatsApp an approved template can go out at any time. Everything else needs the 24-hour customer service window, which is counted by Meta's clock from the person's last message.

POST/v1/messages

cURL

curl -sS -X POST "$UPPU_API_BASE/messages" \
  -H "Authorization: Bearer $UPPU_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1001-shipped" \
  -d '{"channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
       "to": {"contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v"},
       "content": {"type": "template",
                   "template": {"name": "order_update", "language": "en",
                                "body": {"parameters": ["Ayşe", "1001"]}}},
       "client_reference": "order-1001"}'
  • A utility template («Hizmet» in the panel) needs a recorded utility consent, or a message from the person on this number after their latest withdrawal.
  • A marketing template needs a recorded marketing consent. A person who writes DUR, STOP or İPTAL withdraws it.
  • Authentication templates are not sent: 422 template_category_not_allowed.
  • Positional templates take a list (["Ayşe", "1001"], the first value is {{1}}), named templates an object of names and values. Values are text only.

For a WhatsApp number that has never written to you, address it with "to": {"phone": "+905550000001"} and add an inline consent (scope consents:write):

JSON
"consent": {"purpose": "utility",
            "state": "granted",
            "source_text": "The exact text the person agreed to (at most 2000 characters)",
            "source_ref": "web-form:7781",
            "collected_at": "2026-10-15T12:00:00Z"}

4. Reply with free text inside the 24-hour window

POST/v1/messages

cURL

curl -sS -X POST "$UPPU_API_BASE/messages" \
  -H "Authorization: Bearer $UPPU_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reply-01jab3t6ms2m4n6p8q0r2s4t6v" \
  -d '{"channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
       "to": {"contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v"},
       "content": {"type": "text", "text": {"body": "Your order is on its way."}},
       "client_reference": "order-1001"}'
Response 202 (abridged)
{"object": "message",
 "id": "01jab3a1k9f2m4n6p8q0r2s4t6",
 "channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
 "conversation_id": "01jab39zcv2m4n6p8q0r2s4t6v",
 "direction": "out",
 "author": {"type": "api", "label": "Order bot", "api_key_prefix": "k7Hq2mP0aZ1x"},
 "content": {"type": "text", "text": {"body": "Your order is on its way.", "preview_url": false}},
 "status": "queued",
 "error": null,
 "client_reference": "order-1001",
 "provider_message_id": null,
 "created_at": "2026-10-20T09:20:00.120Z"}

Store the message id. Statuses follow as message.status webhooks (accepted, sent, delivered, read or failed) carrying your client_reference. A send to a contact without an open conversation is recorded in a new, already resolved conversation; their reply opens a conversation as usual.

5. Retry safely

Idempotency-Key outcomes
SituationWhat UPPU doesWhat you do
Same key, same body200 with the current state of the first message and Idempotent-Replayed: trueNothing: no duplicate was sent
Same key, different body409 idempotency_key_reusedFix the code: one logical send gets one key and one body
Network error, 429 or 5xxThe first attempt may or may not have been storedRetry with the same key and body, after Retry-After
A policy refusal (for example window_closed)Nothing is sent and the key isn't keptFix the cause; the same key is evaluated again
Status failed arrives laterThe send is finalA new send needs a new key
  • Derive every body value, including client_reference and consent.collected_at, from the business event, never from the attempt or the current time.
  • window_closed (422) is an expected outcome, not an alarm: on WhatsApp send a template, on Messenger and Instagram wait for the person to write again.
  • Keys are unique per channel for good. A UUID or ULID per logical send is a safe choice.

Endpoints

These are all the endpoints available today, relative to the base URL. The API reference has every parameter and schema.

API v1 endpoints
Method and pathScopePurpose
GET /v1/channels
GET /v1/channels/{id}
any keyChannels, their status and capabilities
POST /v1/messagesmessages:sendSend a message; Idempotency-Key required
GET /v1/messages
GET /v1/messages/{id}
messages:readReconcile after a webhook gap; read one message
POST /v1/messages/{id}/readmessages:sendRead receipt for an inbound WhatsApp message
POST /v1/contacts/lookupcontacts:readFind a contact by phone or identity; personal data stays out of URLs
GET /v1/templates
GET /v1/templates/{id}
templates:readWhatsApp templates with sendable and send_example
GET /v1/eventsevents:readThe last 7 days of webhook events
POST /v1/mediamedia:writeUpload a file to send (multipart, at most 25 MB)
GET /v1/media/{id}
GET /v1/media/{id}/content
media:readFile metadata; download a file
GET /v1/openapi.jsonno keyThis API's OpenAPI document

Conventions

  • IDs are lowercase ULIDs of 26 characters (^[0-9a-hjkmnp-tv-z]{26}$), and every object carries object.
  • Times are RFC 3339 in UTC with milliseconds (2026-10-20T09:20:00.120Z). Null fields are always present.
  • Phone numbers are E.164 with a plus sign (+905550000001). Personal data never goes into a URL: look contacts up with POST /v1/contacts/lookup.
  • Unknown request fields get 422 validation_failed with the field code unknown_field.
  • Forward compatibility: within v1, UPPU may add fields, endpoints, event types, enum values, content types and error codes. Ignore unknown fields and event types, accept unknown enum values and treat an unknown content.type as unsupported.
  • Lists take limit (1 to 100, default 25), cursor, order=asc|desc, created_after and created_before, and return {"object":"list","data":[…],"next_cursor":…}. The cursor carries the order; repeat the time filters on every page.
  • Headers: every response has X-Request-Id (yours is echoed if it's at most 64 visible ASCII characters). Look for RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, Retry-After and, on a replay, Idempotent-Replayed: true.
  • Language: Accept-Language: en or tr changes only error.message. error.code is the stable contract.

Rate and size limits

Rate limits per API key
Applies toLimit
All endpoints50 requests per second on average (3,000 per minute), bursts up to 100
Sends and read receipts together20 per second
GET /v1/events2 per second
POST /v1/media2 per second and 1 GB per day
GET /v1/media/{id}/content10 per second and 2 GB per hour
A channel's send queue429 channel_backlog with Retry-After: 30 while 10,000 or more sends wait, or the oldest is older than 15 minutes
  • Request bodies can be up to 1 MB (413 payload_too_large). POST /v1/media takes up to 26 MB with a file of at most 25 MB.
  • More than 30 invalid keys a minute from one network locks that network out for 10 minutes. Don't retry a 401 in a loop.

Errors

Every error uses the same envelope. code is the stable contract; message follows Accept-Language and is meant for people.

Error envelope
{"error": {"type": "policy",
           "code": "window_closed",
           "status": 422,
           "retryable": false,
           "message": "Human-readable text (follows Accept-Language; do not parse)",
           "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}}

retryable is true only for media_not_ready, rate_limited, channel_backlog, internal_error and service_unavailable. Retry those with the same Idempotency-Key and body after Retry-After.

The most common error codes
CodeHTTPWhat to do
missing_api_key401Send Authorization: Bearer … on every request.
invalid_api_key401The key is unknown, revoked or expired, or the workspace is closed. Use an active key; don't retry in a loop.
wrong_environment401A uppu_test_ key on live or a uppu_live_ key on staging. Use the key of that environment.
insufficient_scope403details.required names the missing scope. Add it to the key or use another key.
idempotency_key_required400POST /v1/messages needs an Idempotency-Key of 1 to 64 characters (A-Z a-z 0-9 . _ : -).
idempotency_key_reused409Same key, different body. The first send already exists; rebuild the retry byte for byte, or give a new logical send a new key.
validation_failed422details.fields lists each field and its code (for example unknown_field, too_long). Fix the request.
window_closed422The 24-hour window is closed. details.hint says send_template (WhatsApp) or wait_for_contact (Messenger, Instagram, live chat).
consent_required422The template's category needs a recorded consent. Send the template with an inline consent collected from the person.
template_not_approved422The template isn't APPROVED (details.status). Pick a template with sendable: true.
rate_limited429Wait for Retry-After, then retry with the same key and body.
channel_backlog429The channel's send queue is full. Wait 30 seconds (Retry-After) and retry with the same key.

The full list of error codes also covers the asynchronous codes that arrive later in message.status.

After a message is queued

Meta's answers arrive later as a message.status event with status failed and an error.code, for example window_closed, marketing_opt_out or recipient_unreachable. Only provider_rate_limited and provider_unavailable are retryable, and a resend needs a new Idempotency-Key. UPPU itself retries temporary Meta errors before giving up. Never resend on unknown: after 10 minutes the message shows error.code delivery_unverified.

Who answers: inbox or API mode

Each channel has a handling mode, set by the workspace owner in the panel. In API mode your own bot or system runs the channel, and your team can still step in from the inbox.

Inbox mode and API mode
FeatureInbox modeAPI mode
Inbound messages, contacts and conversationsRecordedRecorded
Webhook eventsIf you subscribeIf you subscribe
Auto-assignment to agentsLive chat only, as set for the channelOff
Email about an unanswered conversationWhen nobody is online in the inboxOff
Agents can reply from the inboxYesYes
An idle open conversationStays openResolved after 24 hours

Messages your system sends appear in the conversation as “API · key label”. Subscribe to message.created if your bot should also see what agents write.

Resources

Tell us what you want to connect.

We turn the API on for your workspace and walk you through it with your own scenario: notifications, your own bot or your CRM.

  • OpenAPI 3.1
  • Standard Webhooks
  • curl, PHP, Node.js, Python