The TypeScript client
One typed method per endpoint, generated from the same definitions that answer your requests. What your editor says is what the API does. Zero dependencies, ESM, Node 20+.
Install it
npm i @whatsapi.sh/sdkThe client takes your key once and carries it everywhere. After that it is method calls. Every method, argument and field arrives with the documentation you are reading now attached, so hovering in your editor beats a second tab. TypeScript is optional: the package is plain ESM and works from JavaScript, including require() on Node 22.12+.
Send something
import { Whatsapi } from "@whatsapi.sh/sdk";
const wa = new Whatsapi({ apiKey: process.env.WHATSAPI_KEY });
const message = await wa.sendMessage({
to: "+5511988887777",
text: "Your table is ready.",
});
console.log(message.id, message.quotaRemaining);Every method takes one object and returns exactly what the endpoint answers with, quotaRemaining included, so a usage meter needs no second call.
Handling errors
import { WhatsapiError } from "@whatsapi.sh/sdk";
try {
await wa.sendMessage({ to, text });
} catch (error) {
if (!(error instanceof WhatsapiError)) throw error;
if (error.code === "PAYMENT_REQUIRED") return offerAnUpgrade();
if (error.code === "NUMBER_NOT_CONNECTED") return askForARepair();
if (error.code === "GATEWAY_TIMEOUT") return watchFor(error.messageId);
throw error;
}Failures arrive as WhatsapiError, carrying the API's own code, the HTTP status, a retryAfter whenever the wait is knowable, and the request id to quote at support.
When delivery is not confirmed
GATEWAY_TIMEOUT means WhatsApp did not confirm in time. Delivery is not confirmed. If error.messageId is present, watch for it on the message.status webhook to learn whether the message arrived. Check the chat before sending again. Another send with a new key, or with none, may duplicate the message. With the same idempotencyKey, you get the same answer back without a second send.
What retries, and what never does
const message = await wa.sendMessage(
{ to: "+5511988887777", text: "Your table is ready." },
{ idempotencyKey: order.id },
);Give a send an idempotencyKey, an id you choose, like an order id. If the send fails with no answer or a 5xx, the client sends it again with the same key, and the API answers with the first result instead of sending the message twice. If the first send is still running, the API answers 409 with a Retry-After, and the client waits that long and tries again. Keep the key with what you are sending, so a retry after a crash uses it too. Reads are retried after a 5xx or no answer as well. A send without a key is never retried after a 5xx or a lost answer: that retry would be a second real message to a real person. A 429 that clears in 10 seconds or less is retried for any call, because nothing ran.
Verify what arrives
import { verifyWebhook } from "@whatsapi.sh/sdk";
const event = await verifyWebhook({
payload: rawBody, // the raw bytes, not the parsed body
signature: req.header("x-whatsapi-signature"),
secret: process.env.WHATSAPI_WEBHOOK_SECRET,
});
if (event.type === "message.created") {
await reply(event.data.number.id, event.data.message.text);
}verifyWebhook checks the signature, refuses a replayed delivery, and hands back the event narrowed on its type, so the payload's fields are known before you touch them.
Building an agent instead?
Then you want MCP, not this package: the same operations as tools, over the same key. The client is for the apps you write yourself.