Kalqore
All posts

WhatsApp API tutorial

Send WhatsApp messages from your app, without Wati or Interakt

Run OpenWA, a free and open-source WhatsApp gateway, on your own machine. Link your WhatsApp with a QR code, send your first message from a few lines of Python, then wire it into your app, backend or AI agent.

10 min readBy @thebrokenapp (opens in a new tab)

This is the written version of our 60-second Instagram reel. Same steps, plus what a reel has no room for: where the API key comes from, how to write the phone number, and what to do when a message doesn’t arrive.

By the end, OpenWA will be running on your computer, your WhatsApp will be linked to it, and a short Python script will message any number you choose. Plan on 15 minutes, most of it spent waiting for npm install.

OpenWA is a free, open-source WhatsApp gateway. It connects to WhatsApp as a linked device, the way WhatsApp Web does, and puts an HTTP API on top: send a request, and a message goes out. That is also what separates it from Wati or Interakt, which are built on Meta’s official API. Section 06 covers what that means before you put it in front of customers.

Step 1

Install and start OpenWA

Four commands. Only the first install is slow.

OpenWA on GitHub (opens in a new tab)Free · MIT licence

Open a terminal in the folder where you keep your projects and run these. They’re the same commands as the Local Dev tab of the quick start on www.open-wa.org.

Terminal
git clone https://github.com/rmyndharis/OpenWA.gitcd OpenWAnpm installnpm run dev

npm install also downloads a copy of Chrome. OpenWA runs WhatsApp Web inside it, out of sight, and that is how it talks to WhatsApp. It’s why the first install takes a few minutes.

When the logs settle, three addresses are live:

AddressWhat it’s for
localhost:2886What it’s for:The dashboard. You’ll click around here.
localhost:2785/apiWhat it’s for:The API. Your code sends its requests here.
localhost:2785/api/docsWhat it’s for:Interactive docs for every endpoint, with a “Try it out” button.
Step 2

Sign in to the dashboard

OpenWA made an API key the first time it started. You’ll need it twice.

Open http://localhost:2886 in your browser. The dashboard asks for an API Key.

OpenWA saved that key in a file inside the project. In your second terminal, from the OpenWA folder, print it:

Terminal · inside the OpenWA folder
cat data/.api-key

In Windows Command Prompt, it’s type data\.api-key. Copy the whole line, paste it into the dashboard and press Connect.

Step 4

Send your first message

One HTTP request. Here it is in Python, using only the standard library.

Create a file called send_message.py in the folder that holds OpenWA (next to it, not inside it) and paste this in:

send_message.py
import jsonimport urllib.errorimport urllib.request API_URL = "http://localhost:2785/api"API_KEY = open("OpenWA/data/.api-key").read().strip() SESSION_ID = "paste-your-session-id-here"PHONE = "91XXXXXXXXXX"  # country code + number, digits onlyTEXT = "Hello from my app 👋" request = urllib.request.Request(    f"{API_URL}/sessions/{SESSION_ID}/messages/send-text",    data=json.dumps({"chatId": f"{PHONE}@c.us", "text": TEXT}).encode(),    headers={"Content-Type": "application/json", "X-API-Key": API_KEY},) try:    with urllib.request.urlopen(request) as response:        print("Sent:", json.load(response))except urllib.error.HTTPError as error:    print("Failed:", error.code, error.read().decode())

Then change two lines:

  • SESSION_ID: the full ID you copied in step 3.
  • PHONE: who you’re messaging. Country code first, then the number, digits only. No +, no spaces, no leading zero.
Number as you’d write itWhat goes in PHONE
+91 98765 43210What goes in PHONE:919876543210
+1 (415) 555-0132What goes in PHONE:14155550132
07700 900123 (UK)What goes in PHONE:447700900123

Save the file and run it from that same folder:

Terminal
python3 send_message.py

On Windows, the command is python send_message.py. OpenWA shows “typing…” in the chat for a second or two before it sends, so the message looks less automated. Then the script prints something like this:

Output
Sent: {'messageId': 'true_919876543210@c.us_3EB0A1B2C3D4E5F6', 'timestamp': 1791190000}

And the message lands on the other phone. That’s the whole trick: a URL, a key, a chat ID and some text.

What the script does

  • Reads your API key straight from OpenWA/data/.api-key, so the key never sits in your code.
  • Sends a POST request to your session’s send-text endpoint, with the key in the X-API-Key header.
  • Turns the phone number into a WhatsApp chat ID by adding @c.us. Group chats have IDs ending in @g.us.
  • Prints OpenWA’s reply: the message ID if it worked, the error if it didn’t.
Step 5

Wire it into your app

The script is the whole idea. Everything else is the same request, sent from somewhere useful.

From your backend

Anything that can make an HTTP request can now send WhatsApp messages. In Node.js, a reusable helper looks like this:

Node.js 18+ · no packages needed
export async function sendWhatsApp(phone, text) {  const url = `${process.env.OPENWA_URL}/sessions/${process.env.OPENWA_SESSION_ID}/messages/send-text`;  const res = await fetch(url, {    method: "POST",    headers: {      "Content-Type": "application/json",      "X-API-Key": process.env.OPENWA_API_KEY,    },    body: JSON.stringify({ chatId: `${phone}@c.us`, text }),  });  if (!res.ok) throw new Error(`OpenWA ${res.status}: ${await res.text()}`);  return res.json();} // After an order is placedawait sendWhatsApp(order.phone, `Order #${order.id} is confirmed.`);

Keep OPENWA_URL (http://localhost:2785/api for now), the session ID and the key in environment variables, never in the code. Here’s why.

And give your app a key of its own. In the dashboard, open API Keys and create an operator key limited to your session. It can send messages but can’t manage keys or settings, and if it ever leaks, you revoke that one key.

Reply to incoming messages

Sending is half a bot. To react when someone writes to you, add a webhook: OpenWA will POST every incoming message to a URL in your app.

Terminal
curl -X POST http://localhost:2785/api/sessions/YOUR_SESSION_ID/webhooks \  -H "Content-Type: application/json" \  -H "X-API-Key: YOUR_API_KEY" \  -d '{"url": "https://your-app.com/webhooks/whatsapp", "events": ["message.received"], "secret": "a-long-random-string"}'

You can also add it from the Webhooks page in the dashboard. Your endpoint reads the message, decides what to say and answers with the same send-text call. The secret lets you check each delivery’s HMAC signature, so nobody else can feed your bot fake messages.

Hand it to an AI agent

Two ways in. Build it yourself: wrap sendWhatsApp as a tool your agent can call, and feed it the messages your webhook receives. Or use the one that ships with OpenWA: a built-in MCP server, so Claude Code, Cursor and other MCP clients can use WhatsApp directly.

To switch it on, add these two lines to a .env file in the OpenWA folder (create the file if it isn’t there), then stop OpenWA with Ctrl+C and run npm run dev again:

.env · in the OpenWA folder
MCP_ENABLED=trueMCP_READONLY=false

MCP_READONLY=false is what lets the agent send. Leave it out and the agent can only read. Then point your client at OpenWA. For Claude Code, that’s a .mcp.json in your project:

.mcp.json
{  "mcpServers": {    "openwa": {      "type": "http",      "url": "http://localhost:2785/mcp",      "headers": { "Authorization": "Bearer YOUR_AGENT_KEY" }    }  }}
Before you go live

The catch with free

Great for prototypes and internal tools. Think twice before customers depend on it.

Wati, Interakt and the other paid tools are built on Meta’s official WhatsApp Business Platform. OpenWA isn’t. It works the way WhatsApp Web does, which is why it’s free and needs nobody’s approval. It’s also why WhatsApp can restrict a number that starts acting like a bot, and no setting in OpenWA can rule that out.

OpenWA’s README lists the habits that keep a number safe. In short:

  • Use a dedicated number. Never your main personal or business one.
  • Warm up a new number. For the first few days, use it like a person would: chat with saved contacts, join a group or two. Then automate.
  • Message people who expect it. Order updates, alerts, replies to people who wrote first. Not cold messages to lists of strangers.
  • Go slow. A few messages a minute is sustainable. Thousands an hour is how numbers get banned.
  • Keep a fallback. Logins, and anything that makes you money, should also work over SMS or email.

OpenWA or the official API?

What mattersOpenWAOfficial API
CostOpenWA:Free. You run it.Official API:Meta’s per-message charges, plus the provider’s plan if you use one
SetupOpenWA:Minutes. Scan a QR code.Official API:A Meta business account, and approved templates for the messages you start
Risk to your numberOpenWA:Real. It’s unofficial.Official API:Low, as long as you follow Meta’s rules
Best forOpenWA:Prototypes, internal alerts, personal tools, small opted-in listsOfficial API:Customer messaging at scale, marketing, regulated industries

A sensible path: prove the idea on OpenWA, then move to the official API once customers rely on it. Keep every send behind one function, like sendWhatsApp above, and that move means rewriting one function instead of hunting through your whole app.

Keeping it running

On your laptop, OpenWA sends only while the laptop is awake and npm run dev is running. For something that’s always on, run it on a server with Docker (docker compose up -d; the README’s production section has the details), and keep the API private: only your backend should be able to reach port 2785.

If something breaks

Troubleshooting

First, find out whether it’s your code or your session.

Open Message Tester in the dashboard, pick your session and send a message from there. If that fails too, the problem is the session, not your script. If it works, find what you’re seeing below.

What you seeWhat to do
npm warn EBADENGINE, or the install failsWhat to do:Your Node.js is too old. OpenWA needs 22.19 or newer; check with node -v.
FileNotFoundErrorWhat to do:Run the script from the folder that holds OpenWA, or put the full path to .api-key in the script.
Connection refused (on Windows, “actively refused it”)What to do:OpenWA isn’t running, or the script points at the wrong port. Code talks to 2785; 2886 is only the dashboard.
Failed: 401What to do:OpenWA rejected the key. If you pasted it by hand, copy the whole line from data/.api-key again.
Failed: 400 and “is not active”What to do:Either the session isn’t Connected (press Start or Reconnect), or SESSION_ID is wrong. Use the full ID from View, not the name or the shortened one on the card.
It says Sent, but nothing arrivesWhat to do:Check the country code. Then test with someone who already has a chat with this number: WhatsApp can drop a first message to a new contact.
Your phone asks you to create a passkey, and linking never finishesWhat to do:WhatsApp has added a step to linking on some accounts, and OpenWA can’t complete it yet on either engine. Use a different number.
The session shows DisconnectedWhat to do:Press Reconnect. If WhatsApp logged the device out (the phone was offline for about 14 days, or someone removed it under Linked devices), scan a fresh QR code.

That’s the whole loop: a running gateway, a linked number and one HTTP request. Order updates, reminders, an AI agent that answers on WhatsApp: everything you build next is ordinary application code on top of it.

And if you need it on the official API, with approved templates and an agent that knows when to hand a chat to a person, that’s the kind of system Kalqore builds.

Launching soon?

Want a second pair of eyes on it before it goes live?

Kalqore builds and runs production software. Tell us what you are shipping and we will tell you where it is likely to break.

Let’s talk (opens WhatsApp in a new tab)
All posts