npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wuapidev/sdk

v0.14.0

Published

TypeScript SDK for wuapi, a WhatsApp API for developers. Link numbers by QR or pairing code, send and receive messages, manage chats, contacts, groups and channels, split them into projects and verify webhooks.

Readme

wuapi TypeScript SDK

npm version CI license

TypeScript SDK for wuapi, a secure, fast and scalable WhatsApp API for developers. Link your own WhatsApp numbers by QR code or pairing code, send and receive messages, manage chats, contacts, groups, communities and channels, split them into projects, and receive events over Webhooks or Streams.

  • Zero runtime dependencies. Uses the global fetch and WebCrypto.
  • ESM with TypeScript types that match the OpenAPI spec.
  • Retries, timeouts and idempotency keys built in.
  • Streams: your events over one connection, with reconnect and resume built in.

Docs: wuapi.dev/docs. OpenAPI: wuapi.dev/openapi.json.

How it works. wuapi does not use the WhatsApp Business Platform (Cloud API). Numbers are linked as devices, the same way WhatsApp Web works. WhatsApp can restrict or ban numbers that behave like spam. You are responsible for your recipients' consent and for following WhatsApp's terms.

Install

npm install @wuapidev/sdk
# or: pnpm add @wuapidev/sdk / yarn add @wuapidev/sdk / bun add @wuapidev/sdk

Using a coding agent? Paste wuapi.dev/llms-full.txt, the whole documentation as one Markdown file. To let Claude, Cursor or VS Code act on your account directly, use the MCP server, @wuapidev/mcp, built on this SDK.

Requirements

The SDK needs a global fetch and WebCrypto (crypto.subtle, for webhook signatures). It runs on:

  • Node 18 or later
  • Bun
  • Deno: import { Wuapi } from "npm:@wuapidev/sdk";
  • Edge runtimes with fetch and WebCrypto, such as Cloudflare Workers and Vercel Edge Functions

Elsewhere, pass your own implementation as new Wuapi({ fetch }). The package is ESM only.

Authentication

Create an API key in the dashboard at wuapi.dev/app/api-keys. Keys look like wu_live_... and are sent as Authorization: Bearer <key>. Keep them on your server.

import { Wuapi } from "@wuapidev/sdk";

const wuapi = new Wuapi({ apiKey: process.env.WUAPI_API_KEY });

apiKey falls back to the WUAPI_API_KEY environment variable, so new Wuapi() works when it is set. On runtimes without process.env, such as most edge runtimes, pass apiKey yourself. A missing key throws when the client is created.

An organization key reaches every project. A project key, created with projects.apiKeys.create, reaches only its project: see Projects.

Quickstart: link a number and send a message

import { Wuapi } from "@wuapidev/sdk";

const wuapi = new Wuapi({ apiKey: process.env.WUAPI_API_KEY });

// 1. Pick where the number's traffic exits: use the phone number's country.
const [location] = await wuapi.proxyLocations.list({ country: "VE" }).toArray(1);

// 2. Create an account. The Free plan includes 1 number, 2,000 messages and 0.5 GB of proxy traffic a month, no card.
const account = await wuapi.accounts.create({
  name: "Support line",
  proxyLocation: { country: location!.country, city: location!.city },
});

// 3. Wait for the QR code and show it to the phone owner.
//    On the phone: WhatsApp > Linked devices > Link a device.
const withQr = await wuapi.accounts.waitForQrCode(account.id);
console.log("Scan this QR code:", withQr.qrCodeUrl); // PNG data URL, e.g. <img src={qrCodeUrl} />

// 4. Wait until the phone finishes linking. The QR code rotates while it
//    waits; onQrCode is called with each new one.
const ready = await wuapi.accounts.waitUntilReady(account.id, {
  onQrCode: (qrCodeUrl) => console.log("New QR code:", qrCodeUrl),
});
console.log("Linked", ready.phone);

// 5. Send a message.
const message = await wuapi.messages.send({
  accountId: ready.id,
  to: "+584241112233",
  text: "Your order has shipped.",
});
console.log(message.id, message.status); // "queued"

proxyLocation is required: every account connects through its own residential proxy, and { country, city } says where it exits. proxyLocations.list() returns every supported pair (country is ISO 3166-1 alpha-2, city a lowercase slug); anything else answers 400 unsupported_proxy_location. Search it with q, which ignores case and accents and returns the best match first: proxyLocations.list({ q: "sao" }) starts with São Paulo.

The city is preferred unless proxyLocation.strictCity is true: if no residential IP is free in that city when the number needs a new exit, it may get one in another city of the same country, and it stays on that exit while it is healthy. accounts.update(id, { proxyLocation }) moves a number or switches strictCity; a connected number changes location at most once every 10 minutes, and an earlier change throws 429 rate_limited with retryAfter (not retried by the client).

A send returns the message with status: "queued". The outcome arrives as the message.sent or message.failed webhook, or by calling wuapi.messages.get(id). A recipient without WhatsApp fails with error.code: "not_on_whatsapp".

While a message is still queued, messages.edit sends it with the new text instead and messages.delete cancels it (it ends failed with error.code: "cancelled"). One being handed to WhatsApp at that moment answers 409 message_sending: retry in a few seconds.

Every resource carries object ("account", "message", ...). Contacts are E.164 (+584241112233), or lid:<digits> when WhatsApp hides the number; groups are …@g.us, channels …@newsletter.

A contact who hides their number may still be known by a WhatsApp username: inbound messages and contacts carry username (lowercase, no @), and to: "@lina.morales" sends to a contact the account already chats with. WhatsApp does not let a linked device look up other usernames, so those answer 400 username_not_supported. Blocked contacts (contacts.listBlocked() and blocklist_change webhook payloads) carry phone and lid when the account knows them.

Link by pairing code instead of QR

When the phone owner cannot scan a screen, link by phone number. They type an 8-character code in WhatsApp > Linked devices > Link a device > Link with phone number instead.

// Either create the account with pairingPhone...
const account = await wuapi.accounts.create({
  name: "Field phone",
  proxyLocation: { country: "VE", city: "caracas" },
  pairingPhone: "+584121234567",
});
const { pairingCode } = await wuapi.accounts.waitForPairingCode(account.id);
console.log("Type this on the phone:", pairingCode); // "ABCD-1234"

// ...or ask for a code for an account that is not linked yet.
const { code, expiresAt } = await wuapi.accounts.createPairingCode(account.id, { phone: "+584121234567" });

// Codes expire after about 160 seconds; waitUntilReady reports each new one.
await wuapi.accounts.waitUntilReady(account.id, { onPairingCode: (c) => console.log("New code:", c) });

createPairingCode answers 409 already_linked for an account that is already ready.

Sending other types

await wuapi.messages.send({
  accountId,
  to: "+584241112233",
  type: "document",
  media: { url: "https://example.com/invoice.pdf", filename: "invoice.pdf" },
  text: "Your invoice",
});

await wuapi.messages.send({
  accountId,
  to: "+584241112233",
  type: "location",
  location: { latitude: 10.4806, longitude: -66.9036, name: "Store" },
});

// Reply to (quote) a message in the same chat.
await wuapi.messages.send({ accountId, to: "+584241112233", text: "Tomorrow.", replyToMessageId: "msg_..." });

// Voice note (send ogg/opus; nothing is transcoded).
await wuapi.messages.send({
  accountId,
  to: "+584241112233",
  type: "voice",
  media: { url: "https://example.com/note.ogg", mimeType: "audio/ogg; codecs=opus" },
});

// Poll, then vote in it and read the tally from the returned message.
const poll = await wuapi.messages.send({
  accountId,
  to: "[email protected]",
  type: "poll",
  poll: { name: "Lunch?", options: ["Pizza", "Sushi"], selectableCount: 1 },
});
await wuapi.messages.vote(poll.id, { options: ["Sushi"] });

// Edit, star, react to and delete what you sent.
await wuapi.messages.edit(message.id, { text: "Your order has shipped. Tracking: 1Z999." });
await wuapi.messages.star(message.id);
await wuapi.messages.react(message.id, { emoji: "\u{1F44D}" });
await wuapi.messages.delete(message.id);

// Forward a message you have (received or sent) to up to 5 chats: one queued message per chat.
// No content and no file to move; the recipients see it as forwarded.
const forwarded = await wuapi.messages.forward(message.id, { to: ["+584241112233", "[email protected]"] });
console.log(forwarded.items.map((m) => [m.to, m.status])); // each is a message like any other

// Send to a group, or post to a channel you administer, by its id.
await wuapi.messages.send({ accountId, to: "[email protected]", text: "Hello group" });
await wuapi.messages.send({ accountId, to: "120363198765432101@newsletter", text: "Version 2.4 is out." });

Sending a file you have

media.url needs the file at a public URL. A local file, a pasted image or a recorded voice note is uploaded first and sent by its id:

import { readFile } from "node:fs/promises";

const upload = await wuapi.uploads.upload(await readFile("photo.jpg"), { mimeType: "image/jpeg" });
await wuapi.messages.send({ accountId, to: "+584241112233", type: "image", media: { uploadId: upload.id }, text: "From my camera roll" });

// In a browser, a File or Blob carries its own type and name.
const note = await wuapi.uploads.upload(recordedBlob); // audio/ogg; codecs=opus
await wuapi.messages.send({ accountId, to: "+584241112233", type: "voice", media: { uploadId: note.id } });

uploads.upload(file, { mimeType?, filename? }) takes a Blob, File, Buffer, Uint8Array, ArrayBuffer or a stream (read into memory first) and returns the ready upload. A file up to 1 MiB goes in one request; a larger one, up to 100 MB, is posted straight to storage through an upload URL and then completed, and a connection that drops while posting is retried. Pass { idempotencyKey } to name the whole upload, so calling again with the same key and file answers the same upload.

A ready upload can be sent any number of times for 24 hours (upload.expiresAt), to any account the key reaches; a message sent with it keeps the file after that. The steps are also there one by one: uploads.create, uploads.complete and uploads.get.

Send types: text, image, video, audio, voice (an audio sent as a voice note), document, sticker, location, contact, contacts, poll and calendar_event. Any send also takes mentions, mentionAll (groups), forwarded (labels a new message as forwarded; messages.forward passes on one you have), viewOnce, disappearingSeconds, linkPreview (text) and media.gifPlayback. A channel takes text, image, video and document.

Everything else works on the account: chats, contacts, the profile, privacy, stories, groups and communities, channels, labels and calls. Each method takes the accountId first, and the account must be ready. Opposites are two methods: archive / unarchive, pin / unpin, mute / unmute, block / unblock, follow / unfollow, star / unstar.

for await (const chat of wuapi.chats.list(accountId, { unread: true })) {
  console.log(chat.name ?? chat.id, chat.unreadCount, chat.lastMessage?.text); // null: a state wuapi has not observed yet
}
await wuapi.chats.sendReadReceipts(accountId, "+584241112233");      // blue ticks
await wuapi.chats.archive(accountId, "+584241112233");
for await (const contact of wuapi.contacts.list(accountId)) {       // the phone's address book, by saved name
  console.log(contact.savedName, contact.phone ?? contact.lid);
}
const { items: [check] } = await wuapi.contacts.check(accountId, { phones: ["+584241112233"] });
await wuapi.stories.create(accountId, { text: "Open until 18:00", backgroundColor: "#0F766E" });
for await (const group of wuapi.stories.list(accountId)) {           // contacts' stories of the last 24 hours, by contact
  for (const story of group.stories) console.log(group.contactId, story.type, story.text, story.viewedAt);
}
await wuapi.groups.create(accountId, { name: "Customers", community: true });
await wuapi.accounts.update(accountId, { rejectCalls: true, rejectCallsMessage: "Please write to us." });

Pacing

Each number sends one message at a time. The anti-ban protections are off by default: no per-minute cap, no first-contact cap, no typing. Turn them on per account before bulk, cold or marketing sends; the recommended values are 12 messages a minute, 5 a minute to people who never wrote to it, and "typing..." for 800 to 6000 ms first. With a cap on, a paced message waits in the queue up to 60 minutes.

const acc = await wuapi.accounts.update(accountId, {
  pacing: { messagesPerMinute: 12, firstContactPerMinute: 5, typing: { enabled: true } },
});
acc.pacing; // effective values, with custom: true
await wuapi.accounts.update(accountId, { pacing: { messagesPerMinute: 0 } }); // 0 turns a cap off
await wuapi.accounts.update(accountId, { pacing: null }); // back to the defaults: every protection off

Ranges: messagesPerMinute 0 to 30, firstContactPerMinute 0 to messagesPerMinute (0 to 30 while that is 0), typing.minMs 0 to 10000, typing.maxMs minMs to 20000, typing.charsPerSecond 5 to 100, queueTimeoutMinutes 1 to 1440. Guide: https://wuapi.dev/docs#sending-safely

Lists and pagination

List methods return a Paginator. Iterate it to walk every item across pages, or ask for one page:

for await (const message of wuapi.messages.list({ accountId, direction: "inbound" })) {
  console.log(message.from, message.text);
}

const { items, nextCursor } = await wuapi.messages.list({ limit: 20 }).page();
const next = await wuapi.messages.list({ limit: 20 }).page(nextCursor ?? undefined);

Every list has the same shape, { object: "list", items, nextCursor }, including lists read live from WhatsApp (groups.list, channels.list, contacts.listBlocked, channels.listMessages, ...). Batch actions (contacts.check, groups.addParticipants, ...) return arrays.

Webhooks

Create an endpoint once and store the secret; it is returned only on creation.

const endpoint = await wuapi.webhookEndpoints.create({
  url: "https://example.com/webhooks/wuapi",
  events: ["message.received", "message.failed", "account.disconnected"],
});
console.log(endpoint.secret); // whsec_...

Verify each request with the raw body, before parsing it as JSON:

import { verifyWebhook, WebhookVerificationError } from "@wuapidev/sdk";

export async function POST(request: Request) {
  const rawBody = await request.text();
  try {
    const event = await verifyWebhook(
      rawBody,
      request.headers.get("wuapi-signature"),
      process.env.WUAPI_WEBHOOK_SECRET!,
    );
    switch (event.type) {
      case "message.received":
        console.log(event.data.object.from, event.data.object.text);
        break;
      case "account.disconnected":
        console.log(event.data.object.id, event.data.object.disconnectReason);
        break;
    }
    return new Response(null, { status: 204 });
  } catch (err) {
    if (err instanceof WebhookVerificationError) return new Response("invalid signature", { status: 400 });
    throw err;
  }
}

verifyWebhook checks the Wuapi-Signature header (t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<rawBody>">) in constant time and rejects timestamps more than 300 seconds away (pass a fourth argument to change it). Deliveries can repeat: deduplicate on event.id.

Every event has the same envelope: { id, object: "event", type, createdAt, organizationId, projectId, data: { object } }. data.object is the resource in its REST shape (an account, a message, a group, ...) or the event's own object (poll_vote, call, chat_change, ...). message.edited and invitation.status_changed add data.previousAttributes. webhook.test (the endpoint as data.object) is sent only when you press Send test event in the dashboard. WEBHOOK_EVENT_TYPES lists every type.

Streams

Streams deliver the same events as Webhooks over one connection your code opens: no public endpoint, no signature to verify. Use Streams from a script, a worker behind a firewall, a desktop app, or while you build.

for await (const event of wuapi.events.stream({ types: ["message.received"] })) {
  console.log(event.data.object.from, event.data.object.text); // typed by the filter
}

The loop runs until you stop it. The stream connects, and when the connection ends (a deploy, a network drop, an hour of age) it waits and connects again with its cursor, so nothing in between is missed; an event a replay repeats is delivered once. It never gives up by itself: only what waiting cannot fix ends the loop, as a StreamError.

import { StreamError } from "@wuapidev/sdk";

const controller = new AbortController();
const stream = wuapi.events.stream(
  { accounts: ["k57a8m2x9d3f0q1wjh6ypc4n2d7s0vbr"], lastEventId: saved },
  {
    signal: controller.signal, // abort() ends the loop without an error
    onStatus: (status) => {
      if (status.type === "reconnecting") console.warn(`stream ${status.reason}, back in ${status.delayMs} ms`);
      if (status.type === "reset") console.warn(`missed events (${status.reason}): resync over REST`);
    },
  },
);
try {
  for await (const event of stream) {
    await handle(event);
    saved = stream.lastEventId; // resume from here after a restart
  }
} catch (err) {
  if (err instanceof StreamError) console.error(err.kind, err.code, err.message); // unauthorized, forbidden, invalid_request, ...
  else throw err;
}
  • Filters. types (typed: the loop's event is narrowed to them) and accounts, up to 50 values each. Presence events and webhook.test are not on Streams.
  • Resume. lastEventId starts after a cursor; within 28 minutes wuapi replays what happened since (events are kept 30 minutes, but only 28 are guaranteed). An older cursor can get a reset status: events may have been missed, so read them over REST (messages.list), and the stream goes on live. A stream without a cursor starts now.
  • What ends it. signal, stream.close() or break. A StreamError for unauthorized (the key), forbidden (the organization is suspended), invalid_request (a filter), not_found (the project), refused and unexpected_response. A 429 (the Free plan allows 3 open stream connections per organization), a 5xx and a network error are waited out, honoring Retry-After, with jitter and at most 6 connects a minute.
  • Everything it reports. stream.items() yields the events with their cursor, id and raw data, and open, reset, skipped and reconnecting in order, instead of onStatus. giveUpAfterMs turns a long outage into a gave_up error.

It uses fetch and streams, so it runs on Node 18+, Bun, Deno and edge runtimes. In a browser the key would be public: stream from your backend. The key is sent in the Authorization header only.

Projects

Three levels: your organization pays; a project is one of your customers, or an environment, with its own accounts, API keys, webhooks, limits and usage, isolated from every other project; an account is a linked WhatsApp number with a name, like "Sales" or "Support".

// Organization key: create the project with your own id for the customer.
const project = await wuapi.projects.create({ name: "Northwind Dental", externalId: "customer_8812", maxAccounts: 3 });

// Act inside it: every request carries the Wuapi-Project header.
const northwind = wuapi.withProject("ext:customer_8812"); // or project.id
await northwind.accounts.list().page();                    // only Northwind's accounts
await northwind.webhookEndpoints.create({ url: "https://example.com/hooks/northwind", events: ["message.received"] });

// Or hand the customer a key that can only reach their project.
const { key } = await wuapi.projects.apiKeys.create(project.id, { name: "Northwind production" });
const theirs = new Wuapi({ apiKey: key! });

new Wuapi({ project }) does the same as withProject from the start. A resource outside the scope answers 404 not_found; a project key naming another project answers 403 forbidden. Every webhook payload carries organizationId and projectId. An organization endpoint receives every project's events; a project endpoint receives only its own.

Invitations

Instead of building a QR screen, send someone else, your customer, a store manager or a sales rep, an invitation: a page on wuapi with your name, logo and color where they link their own number. They need no account or key.

await wuapi.branding.update({ displayName: "Northwind Cloud", accentColor: "#0F766E" });
// hideWuapiBranding: true also needs the White label add-on; without it: 402 addon_required.

const invitation = await wuapi.invitations.create({
  projectId: "ext:customer_8812",
  inviteeName: "Maria Perez",
  inviteeEmail: "[email protected]",
  accountName: "Front desk",
  suggestedCountry: "MX",
  returnUrl: "https://app.example.com/settings/whatsapp",
  metadata: { store: "cdmx-2" },
});
if (!invitation.emailSentAt) console.log(invitation.url); // send it yourself; it is returned only by create and resend

Pass proxyLocation to preset where the account's proxy exits; without it the invitee picks the country and city on the page (invitation.proxyLocation is null until then). They then scans a live QR or types a pairing code. The account is created in the project when they start and named accountName; metadata is copied onto it when the number is linked. Each step fires invitation.status_changed, with the invitation as data.object and data.previousAttributes.status: pending → in_progress → completed, or failed (the link keeps working and a retry reuses the account). expired is computed on read, from expiresInDays (1 to 30, default 7).

for await (const inv of wuapi.invitations.list({ status: "failed" })) console.log(inv.id, inv.failureReason);
await wuapi.invitations.resend(invitation.id); // new url, the old one stops working, emailed again
await wuapi.invitations.cancel(invitation.id); // 409 already_completed once completed

Suspension, limits and usage

await wuapi.projects.update("ext:customer_8812", { status: "suspended" }); // sends and writes answer 403 project_suspended; inbound keeps arriving
await wuapi.projects.update("ext:customer_8812", { status: "active" });

const report = await wuapi.usage.byProject({ month: "2026-09" }); // one line per project, for rebilling
for (const line of report.projects) console.log(line.externalId, line.billableAccountCount, line.sentMessageCount);

wuapi bills the organization across all its projects; usage.byProject is what you rebill from. usage.get() is the organization's own bill this month: every billable account includes 0.5 GB of proxy, pooled (traffic used on the Free plan is never billed), so proxyBytes is everything used, includedProxyBytes the pool, and proxyFeeCents bills only billableProxyBytes, the traffic past it, at $0.99 per GB.

Errors

Every non-2xx response throws a WuapiError with status, code, message, details and requestId (when the server sends x-request-id).

import { WuapiError } from "@wuapidev/sdk";

try {
  await wuapi.messages.send({ accountId, to: "+584241112233", text: "hi" });
} catch (err) {
  if (err instanceof WuapiError && err.code === "account_not_ready") {
    await wuapi.accounts.reconnect(accountId);
  }
}

A request that never got a response throws a WuapiError with status: 0 and code set to timeout, network_error or aborted (your signal fired). The docs list every API error code.

Retries and timeouts

The client retries a failed request up to maxRetries times (default 2):

  • 429 rate_limited, waiting for Retry-After (at most 60 seconds).
  • 5xx responses, network errors and timeouts, with exponential backoff and jitter, starting under 0.5 s and capped at 8 s.

Other 4xx responses throw right away. timeoutMs (default 30_000) applies to each attempt. Cancel a call with an AbortSignal:

const wuapi = new Wuapi({ timeoutMs: 10_000, maxRetries: 4 });

const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
await wuapi.messages.get("msg_...", { signal: controller.signal });

Idempotency

Every POST carries an Idempotency-Key header, generated per call when you do not pass one, so a retried send or create is answered with the first response (Idempotent-Replayed: true) instead of running twice. Pass your own key to make retries across processes safe too:

await wuapi.messages.send({ accountId, to: "+584241112233", text: "Shipped." }, { idempotencyKey: "order-A-1042-shipped" });

Every method takes this options object last: { idempotencyKey?, signal? }.

Calling convention

Path ids come first as strings (accountId, groupId, ...), then one params object with the query and body fields, then the options object. Methods whose body is a single field, like messages.react, also still accept that field on its own (react(id, "\u{1F44D}")), and the batch methods (contacts.check, contacts.lookup, groups.addParticipants and the other participant and join-request methods) then resolve to a bare array. That form is deprecated and goes away in 1.0: pass the params object and read items.

await wuapi.groups.addParticipants(accountId, groupId, { contactIds: ["+584241112233"] }); // { object: "list", items: [...] }

Options

new Wuapi({
  apiKey: "wu_live_...",           // or WUAPI_API_KEY
  baseUrl: "https://api.wuapi.dev", // default
  timeoutMs: 30_000,               // per attempt
  maxRetries: 2,
  fetch: customFetch,              // optional
  project: "ext:customer_8812",     // optional: sends Wuapi-Project on every request
  streamBaseUrl: "https://stream.wuapi.dev", // default, for Streams
});

Reference

| Resource | Methods | |---|---| | proxyLocations | list | | accounts | list, create, get, update, delete, reconnect, logout, createPairingCode, setPresence, setDefaultDisappearingTimer, waitForQrCode, waitForPairingCode, waitUntilReady | | messages | send, list, get, edit, delete, react, vote, forward, star, unstar, addLabel, removeLabel | | uploads | upload, create, get, complete | | chats | list, get, sendPresence, sendReadReceipts, markRead, markUnread, archive, unarchive, pin, unpin, mute, unmute, delete, setDisappearingTimer, addLabel, removeLabel | | stories | create, list, listOwn, get, getMedia, view, react, listViewers, delete | | contacts | check, lookup, getPicture, getBusinessProfile, subscribePresence, block, unblock, listBlocked, getLink, resetLink, resolveLink | | bots | list | | profile | update, setPicture, deletePicture | | privacy | get, update, getStoryPrivacy | | labels | upsert, delete | | calls | reject | | stickerPacks, orders | get | | favoriteStickers | list, add, remove, getMedia | | groups | list, create, get, update, leave, addParticipants, removeParticipants, promoteParticipants, demoteParticipants, getInviteLink, resetInviteLink, join, getInvite, setPicture, deletePicture, listJoinRequests, approveJoinRequests, rejectJoinRequests, listSubgroups, linkSubgroup, unlinkSubgroup, listCommunityParticipants | | channels | list, create, get, getInvite, follow, unfollow, mute, unmute, listMessages, react, markViewed | | webhookEndpoints | list, create, get, update, delete, rotateSecret | | projects | list, create, get, update, delete, getUsage, apiKeys.list, apiKeys.create, apiKeys.revoke | | invitations | create, list, get, cancel, resend | | branding | get, update | | usage | get, byProject | | client | me(), withProject(project), project |

Account-level resources take the accountId first.

Links

How this SDK is made

The client, its types and its tests are generated from the OpenAPI spec, so every endpoint and field the API documents is here, typed, on release day. A few helpers the spec cannot describe (the account wait helpers, WEBHOOK_EVENT_TYPES and the names earlier versions exported) are written by hand next to the generated code. The changes of each version are in CHANGELOG.md.

Contributing

This repository mirrors the SDK from the wuapi monorepo, where it is developed. Issues are welcome here, and a maintainer ports pull requests: see CONTRIBUTING.md. To report a vulnerability, see SECURITY.md.

License

MIT

wuapi is an independent service. It is not affiliated with, endorsed or sponsored by WhatsApp or Meta. WhatsApp is a trademark of Meta Platforms, Inc.