@yaotoshi/openclaw-wa-sdk
v0.2.1
Published
TypeScript SDK for sending WhatsApp messages & reactions through the OpenClaw CAMIS gateway.
Maintainers
Readme
@yaotoshi/openclaw-wa-sdk
A tiny, fully-typed TypeScript SDK for sending WhatsApp messages and reactions through the OpenClaw CAMIS gateway.
- Zero runtime dependencies — uses the global
fetch(Node 18+, browsers, Bun, Deno). - Dual ESM / CommonJS — works with
importandrequire. - Strict types + fail-fast validation — invalid payloads never reach the network.
- Typed, actionable errors — discriminate on
WaSdkError.code.
Designed to be easy for a coding LLM (or human) to use correctly: obvious method names, rich JSDoc, copy-paste examples, errors that say how to fix the problem.
Install
npm install @yaotoshi/openclaw-wa-sdkQuick start
import { fromEnv, createWaClient } from "@yaotoshi/openclaw-wa-sdk";
// (a) from environment — reads OPENCLAW_WA_SDK_BASE_URL + OPENCLAW_WA_SDK_TOKEN
const wa = fromEnv();
// (b) explicit — testable, no env magic
const wa = createWaClient({
baseUrl: "https://example.com",
apiToken: process.env.OPENCLAW_WA_SDK_TOKEN!,
});Works in CommonJS too:
const { fromEnv } = require("@yaotoshi/openclaw-wa-sdk");
const wa = fromEnv();Environment variables (for fromEnv)
# .env
OPENCLAW_WA_SDK_BASE_URL=https://example.com # gateway base URL (no trailing slash)
OPENCLAW_WA_SDK_TOKEN=xxxxx # must match the gateway's API_TOKEN_WAMethods
| Method | Args | Returns | Notes |
| --- | --- | --- | --- |
| wa.sendMessage(args) | { to, message, mediaUrl? } | { messageId, toJid } | plain send |
| wa.reply(args) | { to, messageId, message, participant?, self?, mediaUrl?, quotedText? } | { messageId, toJid } | group requires participant unless self:true |
| wa.sendReaction(args) | { to, messageId, emoji, participant?, self? } | void | any emoji; "" removes |
| wa.reactSuccess(args) | { to, messageId, participant?, self? } | void | sends ✅ |
| wa.reactFailed(args) | { to, messageId, participant?, self? } | void | sends ❌ |
| wa.reactRemove(args) | { to, messageId, participant?, self? } | void | removes the reaction |
to is an E.164 phone number for a personal chat (e.g. "+6281234567890") or a group JID (e.g. "120363…@g.us").
participant is the sender of the target message — a phone number (e.g. "+6281287657411") or JID. It's required for groups unless self: true.
Examples
Send a text message
const { messageId } = await wa.sendMessage({
to: "+6281234567890",
message: "Pesanan Anda dalam pengiriman 🚚",
});Send media
await wa.sendMessage({
to: "+6281234567890",
message: "Surat jalan terlampir",
mediaUrl: "https://example.com/surat-jalan.pdf",
});Reply to a message
Reply to someone else's (inbound) message — pass the sender's participant for groups:
await wa.reply({
to: "[email protected]",
messageId: "<inbound-msg-id>",
message: "Noted 👍",
participant: "+6281287657411", // sender's phone (or JID); required for groups
});Reply to your own message — set self: true (no participant needed):
await wa.reply({
to: "[email protected]",
messageId: "<my-msg-id>",
message: "updated",
self: true,
});The
selfflag.self: truemeans "the target message is my own" → setsfromMe: trueand auto-fillsparticipant(withto) for groups. Omit it for someone else's message →fromMe: false, andparticipantis required for groups. Applies toreplyand every reaction method.
React / remove a reaction
React with any emoji, or use the presets:
await wa.sendReaction({ to: "+6281234567890", messageId, emoji: "👍" }); // reacts with 👍
await wa.reactSuccess({ to: "+6281234567890", messageId }); // reacts with ✅
await wa.reactFailed({ to: "+6281234567890", messageId }); // reacts with ❌
await wa.reactRemove({ to: "+6281234567890", messageId }); // removes the reactionReact to a message in a group (someone else's message → pass participant):
await wa.reactSuccess({
to: "[email protected]",
messageId: "<inbound-msg-id>",
participant: "+6281287657411", // required for groups
});React to your own message in a group (no participant needed):
await wa.reactSuccess({
to: "[email protected]",
messageId: "<my-msg-id>",
self: true,
});Error handling
All failures throw a single {@link WaSdkError} class. Discriminate on code:
import { WaSdkError } from "@yaotoshi/openclaw-wa-sdk";
try {
await wa.sendMessage({ to: "+62…", message: "hi" });
} catch (e) {
if (e instanceof WaSdkError) {
switch (e.code) {
case "AUTH_ERROR": console.error("bad token"); break;
case "TIMEOUT": console.error("gateway slow"); break;
case "NETWORK_ERROR": console.error("cannot reach gateway"); break;
case "API_ERROR": console.error("gateway:", e.message, e.status); break;
case "INVALID_REQUEST": console.error("bad args:", e.message); break;
case "MISSING_CONFIG": console.error("config:", e.message); break;
}
}
}| code | meaning |
| --- | --- |
| MISSING_CONFIG | baseUrl/apiToken missing or malformed, or env vars unset |
| INVALID_REQUEST | client-side validation failed (missing field, group without participant) |
| TIMEOUT | request exceeded timeoutMs |
| AUTH_ERROR | gateway returned 401 (bad token) |
| API_ERROR | gateway returned a non-2xx with { success:false, error } |
| NETWORK_ERROR | request could not reach the gateway (DNS, connection) |
Configuration
createWaClient({ baseUrl, apiToken, timeoutMs: 30_000 }); // timeoutMs defaults to 30000Requirements
- A runtime with global
fetch(Node ≥ 18, modern browsers, Bun, Deno). fromEnv()is Node-only (readsprocess.env); the core client works anywhere.
License
MIT © onchainyaotoshi
