Skip to main content
Version: v0.23.1

Use an official OpenWA SDK

The OpenWA SDKs are official client libraries that wrap the REST API in a typed, fluent interface. You instantiate one client, point it at your gateway, and call resource methods like client.messages.sendText(...) instead of hand-building HTTP requests, signing headers, and parsing error envelopes.

This page gets you from npm install to a sent WhatsApp message: pick an SDK, install it, and run a verified hello-world.

Prerequisites
  • A running OpenWA gateway. See Installation — the examples assume the local default http://localhost:2785.
  • An API key sent as the X-API-Key header. See Authentication to obtain one.
  • A WhatsApp account you can scan a QR code with from your phone.

Which SDK to use

There are five first-party SDKs. All five are hand-written against the exact API surface (paths, request bodies, response shapes) and unit-tested against a mocked transport that asserts on the precise request path, method, and body — so drift between an SDK method and the real endpoint breaks a test rather than reaching you.

LanguagePackageInstallRequires
JavaScript / TypeScript@rmyndharis/openwanpm install @rmyndharis/openwaNode 18+
Pythonrmyndharis-openwa (imports as openwa)pip install rmyndharis-openwaPython 3.9+
PHPrmyndharis/openwacomposer require rmyndharis/openwaPHP 8.1+, Guzzle 7
Javacom.rmyndharis:openwaMaven / Gradle (see below)Java 17+
Gogithub.com/rmyndharis/OpenWA/sdk/gogo get github.com/rmyndharis/OpenWA/sdk/goGo 1.22+

They expose the same resource surface; only the language idioms differ — camelCase methods with objects in JS and PHP, snake_case methods with dicts in Python, fluent builders in Java, and exported service fields (client.Messages, client.Sessions) in Go. Examples on this page use the JavaScript/TypeScript SDK; the Python and PHP equivalents are below the hello-world.

Java install (Maven / Gradle):

<dependency>
<groupId>com.rmyndharis</groupId>
<artifactId>openwa</artifactId>
<version>0.5.0</version>
</dependency>
implementation 'com.rmyndharis:openwa:0.5.0'

The Java SDK is synchronous, has a single runtime dependency (Gson), and builds typed request/response records with a fluent Builder. The Go SDK is stdlib-only (zero third-party dependencies), context-first, and groups the API onto exported service fields with typed sentinel errors you match with errors.Is/errors.As.

The SDKs version independently of the gateway

SDK versions follow SemVer but do not track the gateway version (documented here at v0.23.1). All five currently publish 0.5.0. While they are below 1.0, a minor bump can still break you — 0.3.0, 0.4.0, and 0.5.0 all did, and the changes are listed below. Pin the SDK version your code is tested against, and read the release notes before you move off it. Note the one gateway/SDK mismatch that straddles v0.19.0: SDK 0.3.0 still carries the five sendCatalog methods, whose route the v0.19.0 server removed — that call now gets 404 where it used to get 501. SDK 0.4.0 drops the methods entirely.

Since v0.14.2 every SDK release is cut from CI on its own tag rather than by hand: the JavaScript SDK publishes to npm via Trusted Publishing (js-sdk-v* tag), the Python SDK publishes to PyPI via Trusted Publishing (py-sdk-v* tag), the PHP SDK cuts versioned releases that tag the Packagist mirror (php-sdk-v* tag), and the Go SDK requires its tags to carry the sdk/go/ module prefix — a bare v* tag never publishes it. Releases carry build provenance, and no registry token exists anywhere.

Who the SDK is for

Use the SDK if you are building an application or automation against an OpenWA gateway — sending messages, managing sessions and groups, configuring webhooks, or working with the WhatsApp Business surfaces. If you would otherwise write fetch/curl calls against the API reference, the SDK gives you the same capabilities plus type safety, percent-encoded path segments, typed errors, and a 30-second per-request timeout out of the box.

It is a request/response client, not an event SDK

The SDK does not open WebSockets, emit events, or expose client.on(...). To receive inbound messages and delivery acks, register a webhook with the webhooks resource and host your own HTTP receiver. See the Webhooks guide.

The resource surface

A single client exposes each user-facing resource as a property — everything the gateway publishes except the administrative and operational modules listed below. Each maps to one API resource.

ResourceWhat it does
sessionsList, create, start, stop, force-kill, and inspect sessions; read and change a running session's config; fetch QR / pairing codes
messagesSend text, media, location, contacts, templates; reply, forward, react, bulk-send
contactsLook up contacts, check numbers, resolve phone numbers, block / unblock
groupsCreate groups, manage participants, set subject / description, invite codes
webhooksConfigure event delivery to your own HTTP endpoints; list webhooks across every session, and read the delivery-failure log
chatsList chats, mark read / unread, send typing state, pin / unpin, mute / unmute (v0.16.0+)
templatesManage reusable message templates
labelsWhatsApp Business labels
channelsWhatsApp Channels / newsletters, demote an admin and transfer ownership (v0.16.0+, Baileys only)
catalogWhatsApp Business product catalog
statusWhatsApp Status / stories
searchSearch messages and chats
profileSet the linked account's display name, status text, and profile picture
callsReject an incoming call, create a shareable call link (v0.16.0+)
mediaServer-side conversion into the formats WhatsApp plays, plus a check for whether it is available
healthLiveness / readiness checks

The client also carries two top-level methods: auth() validates the configured key and resolves its role, and request() is a raw escape hatch for any endpoint the resources don't yet wrap.

What the SDKs deliberately leave out

The SDKs cover the user-facing resources above and stop there. The administrative and operational surfaces are not exposed, by design: API-key management under auth, audit, settings, the standalone stats module, automation, infra, plugins, and the integration management routes are predominantly ADMIN-gated; metrics is a Prometheus scrape gated by a token rather than by role; mcp is a Streamable-HTTP transport mounted straight onto the HTTP adapter; ingress is the public receiver integration providers post into; and docker has no HTTP surface at all. Everything else the gateway publishes is reachable through a resource method — request() is for a route newer than your SDK, not for a whole module.

Individual methods that still need an OPERATOR-level key (for example creating a session or a webhook) are annotated in the SDK's inline docs; a non-operator key receives 403.

Migrating to SDK 0.5.0

Breaking (Go, Java, and typed Python callers only).

  • markRead and subscribePresence each take their own request type rather than the shared MarkChatRequest, which now serves markUnread alone. Go and Java swap in MarkChatReadRequest / SubscribePresenceRequest at both call sites (MarkChatRequest stays on markUnread); typed Python swaps at markRead only, its subscribePresence body being structurally identical. The wire body is unchanged; JavaScript and PHP are unaffected.

Additive.

  • markRead accepts an optional messageIds array (up to 100) naming which messages a read receipt covers; without it a burst on the Baileys engine leaves its earlier messages unread. Available in all five clients; ignored by whatsapp-web.js, whose read receipt is chat-level. In Go the field is a pointer so absent and empty stay distinct on the wire.
Migrating to SDK 0.4.0

Breaking.

  • catalog.sendCatalog is removed in all five SDKs. The gateway route it called answered 501 on every engine since it shipped and was removed in v0.19.0, so the method could never succeed. The four typed SDKs fail at compile or type-check time if you still call it; PHP fails at runtime with an undefined-method error. Nothing that ever worked stops working — catalog reads, sendProduct, and everything else are unchanged.

Additive.

  • New routes exposed in all five clients: the channel administration routes (demote an admin, transfer ownership), the chat pin and mute routes, the call-link route, the blocked-contacts route, the group membership-request routes, and the account's own presence route.
  • quotedMessageId is exposed on the send request types in all five clients — every quotable send can now quote, including send-audio in Java, whose separate request record had been the one quotable route a Java caller could not quote on.

Fixes.

  • The SDKs now recognise the error envelope a production gateway sends, so typed errors decode correctly in production and not only in development.
  • Python list returns are annotated with typing.List, and the chat-history fields the endpoint actually returns are declared — both are typing-accuracy corrections that can surface at type-check time.
  • Go: an empty participant list no longer acts on every join request, and the client can send the values that clear a field.
Migrating to SDK 0.3.0

Nothing on the wire changed in any of the items below — the gateway is untouched. What moved is what the SDKs declare they get back, and for four response shapes that declaration was simply wrong, so correcting it breaks source that read the old fields. Only the four typed SDKs — JavaScript, Python, Java, and Go — break at compile or type-check time; the PHP SDK hands back associative arrays, so it keeps running, but the field names below are still what you read out of them.

Breaking.

  • groups.create returns the group summary, { id, name, participantsCount?, isAdmin?, linkedParentJID? }. The SDKs declared the detail type groups.get returns, so participants, description, owner, and createdAt were typed as present on a response that never carries them. Call groups.get when you need the detail shape.
  • The four membership writes return ParticipantsResult, not { success, message }addParticipants, removeParticipants, promoteParticipants, and demoteParticipants. It is a superset, so existing reads of success and message keep compiling, and it adds the per-participant results array. A partial refusal does not fail the batch: the request answers 200 and the rejected member is visible only in results, which the old type gave you no way to see.
  • ContactRecord.pushname is now pushName — the spelling both engines actually emit. Java's case-sensitive binder returned null for every contact under the old one.
  • ContactRecord.isBusiness is gone. The API never sent it, so it was always empty. The same type gained isBlocked and profilePicUrl.
  • catalog.sendProduct returns { id, timestamp }, not { messageId, … }. This one route answers id where every other send answers messageId, and the SDKs decoded the neighbouring field.

Additive.

  • The per-participant group result now carries message — the engine's reason for a participant it refused — alongside id, success, and status.
  • The send-product response now carries timestamp alongside id.
  • sessions.list now accepts limit and offset in the JavaScript, Python, PHP, and Java SDKs. GET /api/sessions has always taken them, but only the Go SDK exposed them; the other four sent a bare path and pinned you to the server default.

Install

npm install @rmyndharis/openwa

The JavaScript SDK requires Node 18+ because it uses the global fetch. It ships both ESM and CommonJS builds with bundled type definitions, so it works in TypeScript and plain JavaScript without extra setup. On an older runtime, pass your own fetch to the constructor.

Hello world

The flow is: instantiate the client → start a session → authenticate by scanning a QR code → send a message. Starting a session brings the WhatsApp connection up but does not log you in; the session reaches ready only after you scan its QR code from your phone.

import { OpenWAClient } from '@rmyndharis/openwa';

const client = new OpenWAClient({
baseUrl: 'http://localhost:2785',
apiKey: 'YOUR_API_KEY',
});

// 1. Create the session. Keep the generated id — every later call takes the id,
// never the name.
const session = await client.sessions.create({ name: 'my-session' });

// 2. Start it and bring the WhatsApp connection up.
await client.sessions.start(session.id);

// 3. Fetch the QR code and scan it from WhatsApp on your phone
// (Settings → Linked devices → Link a device).
const { qrCode, status } = await client.sessions.getQrCode(session.id);
console.log(status, qrCode); // qrCode is a data:image/png;base64,… URL

// 4. Once the session status is 'ready', send a text message.
const result = await client.messages.sendText(session.id, {
chatId: '628123456789@c.us',
text: 'Hello from the OpenWA SDK!',
});

console.log(result.messageId);

sendText resolves to { messageId, timestamp }, where timestamp is a Unix epoch value in seconds:

{ "messageId": "3EB0XXXXXXXXXXXXXXXX", "timestamp": 1740000000 }

chatId is a WhatsApp JID: 628123456789@c.us for an individual contact and 123456789-123456789@g.us for a group. See the Glossary for the JID format.

The session must be ready before you send

start returns as soon as the engine is initializing — its status will be initializing or qr_ready, not ready. If you call sendText before scanning the QR code, the API returns 409 Conflict (the SDK throws OpenWAConflictError). Poll client.sessions.get(session.id) until status === 'ready', or wait for the webhook session event. See Sessions for the full lifecycle.

create needs an OPERATOR-level key. If the session already exists, read its id from client.sessions.list() or from the dashboard and skip straight to start — the name is only used to create the session, and no route accepts it in place of the id.

Python

from openwa import OpenWAClient

with OpenWAClient(base_url="http://localhost:2785", api_key="YOUR_API_KEY") as client:
session = client.sessions.create({"name": "my-session"})

client.sessions.start(session["id"])

qr = client.sessions.get_qr_code(session["id"])
print(qr["status"], qr["qrCode"]) # scan from your phone

# After the session reaches 'ready':
result = client.messages.send_text(session["id"], {
"chatId": "628123456789@c.us",
"text": "Hello from the OpenWA Python SDK!",
})
print(result["messageId"])

The Python client is synchronous and returns plain dict/list values. It is also a context manager, so the connection pool is closed for you.

PHP

<?php
require 'vendor/autoload.php';

use OpenWA\Client;

$client = new Client([
'baseUrl' => 'http://localhost:2785',
'apiKey' => 'YOUR_API_KEY',
]);

// The session's id, not its name — from POST /sessions, or the dashboard.
$sessionId = '8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a';

$client->sessions->start($sessionId);

$qr = $client->sessions->getQrCode($sessionId);
echo $qr['status'], PHP_EOL; // scan $qr['qrCode'] from your phone

// After the session reaches 'ready':
$result = $client->messages->sendText($sessionId, [
'chatId' => '628123456789@c.us',
'text' => 'Hello from the OpenWA PHP SDK!',
]);

echo $result['messageId'];

The PHP entry class is OpenWA\Client; payloads are associative arrays.

Errors you will hit

Every non-2xx response throws a typed error you can branch on. In JavaScript, all error classes extend OpenWAError and are exported, so they are instanceof-checkable; a timeout throws OpenWATimeoutError.

Error class (JS)StatusWhen it happens
OpenWAAuthError401Missing or invalid API key
OpenWAForbiddenError403The key's role is insufficient for an OPERATOR-only route
OpenWANotFoundError404Session, chat, or resource does not exist
OpenWAConflictError409Engine not ready — start the session and scan its QR code first
OpenWARateLimitError429Rate limited — back off and retry
OpenWANotImplementedError501The active engine does not support this operation
OpenWAServiceUnavailableError503The engine did not confirm the operation in time — the one error here that is worth retrying
OpenWATimeoutErrorThe request exceeded the 30-second timeout
import { OpenWAConflictError, OpenWAAuthError } from '@rmyndharis/openwa';

const sessionId = '8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a'; // the session's id, not its name

try {
await client.messages.sendText(sessionId, {
chatId: '628123456789@c.us',
text: 'Hi!',
});
} catch (err) {
if (err instanceof OpenWAConflictError) {
// 409 — engine not ready; start the session and scan its QR code first.
} else if (err instanceof OpenWAAuthError) {
// 401 — check your API key.
} else {
throw err;
}
}

The Python (OpenWAConflictError, …) and PHP (OpenWAConflictException, …) hierarchies mirror these one-to-one. Every Python error class is importable from the package root, including the newest one — from openwa import OpenWAServiceUnavailableError.

Send the API key over HTTPS in production

X-API-Key is bearer-equivalent — anyone holding it can act as you. Send it only over https:// outside local development. The SDK never follows redirects, so the key is never re-sent to a redirect target. The SDK also does no automatic retries; wrap calls in your own backoff if you need them, especially for 429.

Next steps

  • SDK usage — client configuration, sending media, custom transports, and request patterns
  • Sessions — the full session lifecycle and how to wait for ready
  • Sending messages — every message type and formatting
  • Webhooks — receive inbound messages and delivery events
  • API reference — the underlying REST endpoints the SDK wraps