A practical guide

How to send a WhatsApp message from your code

One POST sends a text, an image, a video or a PDF to a WhatsApp number in any country. This page is the whole of it: the request, the number formats, files, and what comes back when somebody replies.

The request

Everything goes to the same endpoint. What you are sending is decided by which key you put in the body, so there is one shape to learn rather than six.

Send a message
curl -X POST https://njiwa.upeo.ai/v1/messages \
  -H "Authorization: Bearer sk_live_your_key" \
  -d '{"to": "0712345678", "text": "Your order has shipped"}'

You get a message id back straight away, and the delivery updates arrive on your webhook as WhatsApp reports them.

The response
{
  "id": "msg_01J9ZK4Q7M2X8P0R3V6T5W1YHD",
  "status": "sent",
  "to": "254712345678",
  "type": "text",
  "created_at": "2026-08-31T09:14:02Z"
}

Phone numbers, in any format

Njiwa takes numbers the way they actually appear in a database rather than the way a specification wishes they did. All of these reach the same person:

  • +254 712 345 678, spaced and with a plus
  • 254712345678, bare international
  • 0712-345-678, local, punctuated
  • 0712345678, local, resolved against the country code of the number you are sending from

That last one is the useful part. A business in Lagos sending to 08012345678 reaches a Nigerian number, and a business in London sending to 07700900123 reaches a British one, without either of them normalising a database first. When Njiwa genuinely cannot tell which country a local number belongs to, it says so and tells you what to send instead, rather than guessing and delivering to a stranger.

Files, three ways

Send a PDF straight off your disk
curl -X POST https://njiwa.upeo.ai/v1/messages \
  -H "Authorization: Bearer sk_live_your_key" \
  -F to=0712345678 \
  -F document=@invoice-8841.pdf \
  -F caption="Your invoice for August"

An invoice, a receipt, a ticket, a photograph. Attach it however suits what you already have:

  • By URL. Point Njiwa at a file your system already serves and it fetches it.
  • By upload. POST the bytes to /v1/media and use the med_ id you get back.
  • By stored id. Reuse a med_ id you uploaded earlier, so the same price list is uploaded once and sent a thousand times.

In every case Njiwa reads the real type from the file's own bytes rather than trusting its name or its declared content type, so a PDF somebody renamed .jpg still arrives as a PDF.

Getting replies back

Incoming messages and status changes are posted to an endpoint you nominate, signed with HMAC SHA-256 and timestamped, so your handler can prove a request came from us before it acts on it. Every attempt is logged with what we sent and what came back, which is what turns a failing integration into something you can read rather than guess at.

Retries that cannot double send

Send an Idempotency-Key header and a repeat of the same request returns the original message id instead of sending a second message. A timeout on your side stops being a decision about whether to risk messaging a customer twice.

Works with what you already run

It is a plain HTTP API, so it fits a WooCommerce store, a Laravel application, an ERPNext instance, a Python script on a cron, or a Node service. Thin SDKs exist for PHP, Node and Python, and none of them are compulsory.

What it costs to try

Nothing. Every account gets a test key that validates every request, stores every message and fires real webhooks, and delivers nothing to WhatsApp. Build the whole integration before a single message leaves the building.

Start a 7 day trial Read the quickstart