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.
Three steps from an empty terminal to a delivered message.
Base URL
https://api.dev.unosap.com/v1Auth
Bearer sk_live_...Content-Type
application/jsonVersion
v1- 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
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
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!"}'
Two kinds of key, and picking the wrong one is the most common cause of a 401.
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_...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/messagesAuthorization: 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.
A session is one connected WhatsApp number. These routes need a master key.
/v1/sessionsMaster keyCreate 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"}'/v1/sessionsMaster keyList every session on the account.
curl https://api.dev.unosap.com/v1/sessions \
-H "Authorization: Bearer sk_live_master_..."/v1/sessions/:idMaster keyDetails for a single session.
curl https://api.dev.unosap.com/v1/sessions/f496492d-... \
-H "Authorization: Bearer sk_live_master_..."/v1/sessions/:id/pairMaster keyGet 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"}'/v1/sessions/:id/reconnectMaster keyReconnect a dropped session. Returns 204.
curl -X POST https://api.dev.unosap.com/v1/sessions/f496492d-.../reconnect \
-H "Authorization: Bearer sk_live_master_..."/v1/sessions/:id/healthMaster keyHealth score and connectivity for a session.
curl https://api.dev.unosap.com/v1/sessions/f496492d-.../health \
-H "Authorization: Bearer sk_live_master_..."/v1/sessions/:idMaster keyDisconnect and remove a session. Returns 204.
curl -X DELETE https://api.dev.unosap.com/v1/sessions/f496492d-... \
-H "Authorization: Bearer sk_live_master_..."One endpoint, one body shape, six message types. The only difference between the two routes is which key you hold.
/v1/sendSession keySend 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"}'/v1/sessions/:id/sendMaster keyThe 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"}'Upload a file once, then reference the returned URL in any message.
/v1/media/uploadEither keymultipart/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 MBimage/jpeg, image/png, image/webp
Video
16 MBvideo/mp4, video/3gpp
Audio
10 MBaudio/mpeg, audio/ogg, audio/mp4
Documents
100 MBPDF, DOCX, XLSX and more
Read-only routes that use a session-scoped key. No ID in the URL — the key decides.
/v1/sessionSession keyYour session's status, number and health.
curl https://api.dev.unosap.com/v1/session \
-H "Authorization: Bearer sk_live_ses_..."/v1/healthSession keyHealth score on its own — cheap enough to poll.
curl https://api.dev.unosap.com/v1/health \
-H "Authorization: Bearer sk_live_ses_..."/v1/messagesSession keyOutbound 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_..."Events pushed to your server as they happen. Each session can have several, with different event filters.
/v1/webhooksSession keyCreate 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"}'/v1/webhooksList your webhooks
/v1/webhooks/:idChange URL, events or enabled
/v1/webhooks/:idRemove a webhook
/v1/webhooks/:id/testSend a test ping
/v1/webhooks/:id/deliveriesRecent 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.receivedInbound message from a user.
message.sentYour outbound message was acknowledged.
message.deliveredYour message reached the recipient's device.
message.readThe recipient opened your message.
message.updatedA message was edited.
message.deletedA message was deleted or revoked.
message.reactionA reaction emoji was added or removed.
session.connectedYour session connected.
session.disconnectedYour session disconnected.
session.bannedSession was banned by WhatsApp.
session.qr_updatedNew QR code generated.
chats.upsertChat created or updated.
chats.updateChat settings changed.
chats.deleteChat deleted.
groups.upsertGroup created or number added.
groups.updateGroup metadata changed.
group-participants.updateParticipant joined, left or was promoted.
call.receivedWhatsApp 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.
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.