Developers

Send WhatsApp messages
from your own code

A REST API over a real WhatsApp number — no Business API approval, no template review. This page is the guide; the complete endpoint reference is generated from the API itself and always current.

Quickstart

Three steps from an empty terminal to a delivered message.

Base URL

https://api.dev.unosap.com/v1

Auth

Bearer sk_live_...

Content-Type

application/json

Version

v1
  1. 1

    Connect a number

    Create a session and scan the QR code from WhatsApp → Linked devices. You can do this in the dashboard or over the API.

    curl -X POST https://api.dev.unosap.com/v1/sessions \
      -H "Authorization: Bearer sk_live_master_..." \
      -H "Content-Type: application/json" \
      -d '{"displayName": "Support Line"}'
  2. 2

    Take the session key

    Creating a session returns a key scoped to it. Store it — it is shown once.

    {
      "id": "f496492d-...",
      "displayName": "Support Line",
      "status": "PENDING",
      "apiKey": "sk_live_ses_..."
    }
  3. 3

    Send a message

    Once the session reports CONNECTED, it can send.

    curl -X POST https://api.dev.unosap.com/v1/send \
      -H "Authorization: Bearer sk_live_ses_..." \
      -H "Content-Type: application/json" \
      -d '{"to": "2348012345678", "type": "text", "text": "Hello!"}'

Keys & authentication

Two kinds of key, and picking the wrong one is the most common cause of a 401.

Master key

Every session and account-level operation. Use it from your server to create sessions, send on behalf of any of them, and manage webhooks across the account. Found in Settings.

Authorization: Bearer sk_live_master_...
Session-scoped key

Tied to one WhatsApp number. The key itself identifies the session, so no ID appears in the URL — these paths only exist with this key type:

/send/webhooks/session/health/messages
Authorization: Bearer sk_live_ses_...

Both use the same header — the API detects which it is. Generate a session key from the Sessions page or with POST /v1/sessions/:id/keys. Keys are shown once.

Sessions

A session is one connected WhatsApp number. These routes need a master key.

POST/v1/sessionsMaster key

Create a session. Returns its ID and a session-scoped key.

curl -X POST https://api.dev.unosap.com/v1/sessions \
  -H "Authorization: Bearer sk_live_master_..." \
  -H "Content-Type: application/json" \
  -d '{"displayName": "Support Line"}'
GET/v1/sessionsMaster key

List every session on the account.

curl https://api.dev.unosap.com/v1/sessions \
  -H "Authorization: Bearer sk_live_master_..."
GET/v1/sessions/:idMaster key

Details for a single session.

curl https://api.dev.unosap.com/v1/sessions/f496492d-... \
  -H "Authorization: Bearer sk_live_master_..."
POST/v1/sessions/:id/pairMaster key

Get a pairing code instead of a QR — useful when the number is not next to a screen. Phone in international format.

curl -X POST https://api.dev.unosap.com/v1/sessions/f496492d-.../pair \
  -H "Authorization: Bearer sk_live_master_..." \
  -H "Content-Type: application/json" \
  -d '{"phone": "2348012345678"}'
POST/v1/sessions/:id/reconnectMaster key

Reconnect a dropped session. Returns 204.

curl -X POST https://api.dev.unosap.com/v1/sessions/f496492d-.../reconnect \
  -H "Authorization: Bearer sk_live_master_..."
GET/v1/sessions/:id/healthMaster key

Health score and connectivity for a session.

curl https://api.dev.unosap.com/v1/sessions/f496492d-.../health \
  -H "Authorization: Bearer sk_live_master_..."
DELETE/v1/sessions/:idMaster key

Disconnect and remove a session. Returns 204.

curl -X DELETE https://api.dev.unosap.com/v1/sessions/f496492d-... \
  -H "Authorization: Bearer sk_live_master_..."

Sending messages

One endpoint, one body shape, six message types. The only difference between the two routes is which key you hold.

POST/v1/sendSession key

Send from the session the key belongs to. Every message type below uses this same route — only the body changes.

Text

curl -X POST https://api.dev.unosap.com/v1/send \
  -H "Authorization: Bearer sk_live_ses_..." \
  -H "Content-Type: application/json" \
  -d '{"to": "2348012345678", "type": "text", "text": "Hello!"}'

Image with caption

curl -X POST https://api.dev.unosap.com/v1/send \
  -H "Authorization: Bearer sk_live_ses_..." \
  -H "Content-Type: application/json" \
  -d '{"to": "2348012345678", "type": "image", "mediaUrl": "https://example.com/photo.jpg", "caption": "Check this"}'

Document

curl -X POST https://api.dev.unosap.com/v1/send \
  -H "Authorization: Bearer sk_live_ses_..." \
  -H "Content-Type: application/json" \
  -d '{"to": "2348012345678", "type": "document", "mediaUrl": "https://example.com/file.pdf", "fileName": "report.pdf"}'

Buttons

curl -X POST https://api.dev.unosap.com/v1/send \
  -H "Authorization: Bearer sk_live_ses_..." \
  -H "Content-Type: application/json" \
  -d '{"to": "2348012345678", "type": "buttons", "text": "Confirm?", "buttons": [{"id": "yes", "label": "Yes"}, {"id": "no", "label": "No"}]}'

List / menu

curl -X POST https://api.dev.unosap.com/v1/send \
  -H "Authorization: Bearer sk_live_ses_..." \
  -H "Content-Type: application/json" \
  -d '{"to": "2348012345678", "type": "list", "text": "Choose", "listButtonText": "Select", "sections": [{"title": "Dept", "rows": [{"id": "a", "title": "Support"}]}]}'

Location

curl -X POST https://api.dev.unosap.com/v1/send \
  -H "Authorization: Bearer sk_live_ses_..." \
  -H "Content-Type: application/json" \
  -d '{"to": "2348012345678", "type": "location", "latitude": 6.5244, "longitude": 3.3792, "locationName": "Lagos"}'
POST/v1/sessions/:id/sendMaster key

The same body, but you choose the session in the URL. Use this when one key drives several numbers.

curl -X POST https://api.dev.unosap.com/v1/sessions/f496492d-.../send \
  -H "Authorization: Bearer sk_live_master_..." \
  -H "Content-Type: application/json" \
  -d '{"to": "2348012345678", "type": "text", "text": "Hello from a master key"}'
On buttons and lists: WhatsApp stopped honouring interactive messages from numbers that are not on the official Business API. On a QR-linked number they arrive as plain text, with the options dropped. Send the choices in the message body instead.

Media

Upload a file once, then reference the returned URL in any message.

POST/v1/media/uploadEither key

multipart/form-data. Returns a hosted URL for use in mediaUrl.

curl -X POST https://api.dev.unosap.com/v1/media/upload \
  -H "Authorization: Bearer sk_live_..." \
  -F "file=@/path/to/photo.jpg"

Images

5 MB

image/jpeg, image/png, image/webp

Video

16 MB

video/mp4, video/3gpp

Audio

10 MB

audio/mpeg, audio/ogg, audio/mp4

Documents

100 MB

PDF, DOCX, XLSX and more

Session state & history

Read-only routes that use a session-scoped key. No ID in the URL — the key decides.

GET/v1/sessionSession key

Your session's status, number and health.

curl https://api.dev.unosap.com/v1/session \
  -H "Authorization: Bearer sk_live_ses_..."
GET/v1/healthSession key

Health score on its own — cheap enough to poll.

curl https://api.dev.unosap.com/v1/health \
  -H "Authorization: Bearer sk_live_ses_..."
GET/v1/messagesSession key

Outbound messages to a number, newest first. Paginated, 50 per page.

curl "https://api.dev.unosap.com/v1/messages?contactPhone=2348012345678&page=1&limit=50" \
  -H "Authorization: Bearer sk_live_ses_..."

Webhooks

Events pushed to your server as they happen. Each session can have several, with different event filters.

POST/v1/webhooksSession key

Create a webhook. A secret is generated if you do not supply one.

curl -X POST https://api.dev.unosap.com/v1/webhooks \
  -H "Authorization: Bearer sk_live_ses_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-server.com/hook", "events": ["message.received"], "secret": "my-secret"}'
GET/v1/webhooks

List your webhooks

PATCH/v1/webhooks/:id

Change URL, events or enabled

DELETE/v1/webhooks/:id

Remove a webhook

POST/v1/webhooks/:id/test

Send a test ping

GET/v1/webhooks/:id/deliveries

Recent attempts and responses

What arrives at your endpoint

POST https://your-server.com/hook
X-Uno-Signature: a1b2c3d4e5f6...

{
  "event": "message.received",
  "tenantId": "tenant_abc",
  "sessionId": "f496492d-...",
  "timestamp": 1716894200000,
  "data": { "messages": [{ "key": {...}, "message": {...} }] }
}

Verify the signature

HMAC-SHA256 of the raw body with your webhook secret. Compare in constant time, and read the body before any JSON parser touches it — re-serialising changes the bytes and the signature will never match.

const crypto = require('crypto');

app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-uno-signature'];
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).end();
  }
  res.status(200).end();          // acknowledge first
  processEvent(JSON.parse(req.body));  // then do the work
});

Event types

message.received

Inbound message from a user.

message.sent

Your outbound message was acknowledged.

message.delivered

Your message reached the recipient's device.

message.read

The recipient opened your message.

message.updated

A message was edited.

message.deleted

A message was deleted or revoked.

message.reaction

A reaction emoji was added or removed.

session.connected

Your session connected.

session.disconnected

Your session disconnected.

session.banned

Session was banned by WhatsApp.

session.qr_updated

New QR code generated.

chats.upsert

Chat created or updated.

chats.update

Chat settings changed.

chats.delete

Chat deleted.

groups.upsert

Group created or number added.

groups.update

Group metadata changed.

group-participants.update

Participant joined, left or was promoted.

call.received

WhatsApp call detected.

Four rules that prevent most webhook bugs

  • • Answer 200 within 60 seconds, then process asynchronously.
  • • Always verify the signature, with a constant-time compare.
  • • Be idempotent — an event can arrive more than once.
  • • Ignore events you do not recognise rather than failing on them.

Full API reference

Everything above is the guided tour. The complete reference is generated from the API's own OpenAPI spec, so it cannot drift out of date the way a hand-written page does.

Ready to send your first message?

Connect a number and your key is issued with it.

Connect a number