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

@wabery/sdk

v0.12.1

Published

Official TypeScript server-side SDK for the Wabery messaging infrastructure API.

Readme

Wabery TypeScript SDK

Official server-side TypeScript SDK for the Wabery REST API.

Wabery covers the full messaging workflow for WhatsApp, Instagram, and Messenger: channels and routing, contacts and registration intents, conversations and message history, outbound text/templates/media/interactive messages, reactions, locations, stickers, inbound media files with signed URLs, WhatsApp Flows, hosted functions, submissions, dispatches, config-as-code, signed webhooks, and Meta Business Agent API types.

Read the full Wabery API documentation at www.wabery.com/docs.

npm install @wabery/sdk
import { Wabery } from "@wabery/sdk";

const wabery = new Wabery();

const identity = await wabery.identity.retrieve();
console.log(identity.project.id);

const message = await wabery.messages.send({
	channelId: "channel_...",
	conversationId: "conversation_...",
	text: "Thanks for your message",
});

const status = await wabery.messages.get(message.id);
console.log(status.delivery_status);

The SDK reads WABERY_API_KEY, WABERY_BASE_URL, and WABERY_API_VERSION from the environment. It ships a dual build, so both ESM (import) and CommonJS (require("@wabery/sdk")) work.

First sandbox reply

For a new project, create or select the project in the Wabery dashboard, then run wabery login and paste the one-time key shown in the browser back into the terminal. The CLI stores the key locally; for your app process, export it as WABERY_API_KEY.

Use the dashboard's WhatsApp button or manually text the project join code (wab-ABC123 style) to the shared Wabery sandbox number. That join-code message only registers your phone. From the CLI, wabery projects get "project_id" returns join_code and enrollment_channel.phone_number. Send one normal WhatsApp message next, then find the ids:

wabery projects get "project_id"
wabery channels list
wabery conversations list
wabery conversations messages "conversation_..."

Use the returned channel_id and conversation_id with the SDK:

await wabery.messages.send({
	channelId: "channel_...",
	conversationId: "conversation_...",
	text: "Thanks for your message",
});

Use conversationId when replying in an existing conversation. Sending directly to to: "+14155550100" is supported only on dedicated WhatsApp channels after you have recorded an active opt-in for that contact. Phone numbers are expected in E.164 format (a leading + and country code); Wabery normalizes and dedupes them per organization server-side.

Outbound content can be text, templates, media, interactive buttons/lists, reactions, locations, or stickers. Provide exactly one content field:

await wabery.messages.send({
	channelId: "channel_...",
	conversationId: "conversation_...",
	reaction: { messageId: "wamid....", emoji: "👍" },
});

await wabery.messages.send({
	channelId: "channel_...",
	conversationId: "conversation_...",
	location: {
		latitude: 37.7749,
		longitude: -122.4194,
		name: "San Francisco office",
	},
});

await wabery.messages.send({
	channelId: "channel_...",
	conversationId: "conversation_...",
	sticker: { link: "https://example.com/sticker.webp" },
});

Registration intents

Create single-use WhatsApp onboarding links from a browser-safe publishable key. When the key starts with wab_pub_, the SDK sends it as x-wabery-publishable-key instead of a Bearer token:

const wabery = new Wabery({ apiKey: "wab_pub_..." });

const intent = await wabery.registrationIntents.create({
	customerReference: "user_42",
	metadata: { plan: "starter" },
});

// Send the customer to intent.wa_link, then poll status:
const status = await wabery.registrationIntents.get(intent.id);

Server-side code may also create and poll intents with a wab_live_ key that has the registrations:create scope.

Webhooks

Verify and parse a delivery into a typed, discriminated event with constructEvent (Stripe-style). Pass the raw, unparsed request body and the full x-wabery-signature header (formatted sha256=<hex>):

import { Wabery, type WaberyEvent } from "@wabery/sdk";

const wabery = new Wabery();

// rawBody must be the exact bytes received — do not JSON.parse then re-stringify.
const event: WaberyEvent = wabery.webhooks.constructEvent(
	rawBody,
	request.headers["x-wabery-signature"],
	process.env.WABERY_WEBHOOK_SECRET,
);

switch (event.event) {
	case "message.received":
		await reply(event.payload.from, event.payload.text);
		break;
	case "flow.completed":
		await save(event.payload.submission);
		break;
	case "message.status":
	case "participant.joined":
		break;
}

constructEvent throws WaberySignatureVerificationError on a bad signature. If you only need a boolean, wabery.webhooks.verifySignature(rawBody, header, secret) verifies without parsing.

Inbound message.received events include messages, assets, media, and location. Cached WhatsApp media uses short-lived Wabery signed URLs; download it promptly. expires_at is the retention deadline: 24 hours on free plans and 7 days on paid plans. assets is a flattened list of inbound files, media is the latest file, and messages[n].media is the file attached to that specific message. Supported inbound file payloads include WhatsApp images, voice notes, audio, video, documents, and stickers.

if (event.event === "message.received") {
	for (const asset of event.payload.assets ?? []) {
		if (asset.status !== "available" || !asset.url) continue;

		const response = await fetch(asset.url);
		if (!response.ok) throw new Error(`Failed to download ${asset.id}`);

		await saveInboundFile({
			id: asset.id,
			messageId: asset.message_id,
			fileName: asset.file_name,
			contentType: asset.mime_type,
			bytes: await response.arrayBuffer(),
			retainedUntil: asset.expires_at,
		});
	}
}

Deliveries are at-least-once. Deduplicate on a stable id (message_id for message.received, flow_token for flow.completed):

import { createDedupeStore } from "@wabery/sdk";

const seen = createDedupeStore({ ttlMs: 24 * 60 * 60 * 1000 });
if (seen.add(event.payload.message_id)) return; // already processed

createDedupeStore is single-instance. For multiple instances, implement the DedupeStore interface over a shared store (Redis SET … NX PX, a unique DB column) — add(key) returns true when the key was already seen.

The flow.completed submission is a Record<string, unknown>; coerce values with asNumber / asString / asBoolean / asDate / asDateRange / asStringArray, or coerceSubmission(submission, { budget: "number", … }) for the whole record at once.

Contacts

Enroll a contact (records opt-in, returns a stable contact_id), optionally with a preferredLanguage used to resolve locale-specific flows, and update it later:

const contact = await wabery.contacts.enroll({
	phone: "+14155550100",
	preferredLanguage: "es", // BCP-47; resolves locale flows on sendByConfigKey
	referenceId: "user_42", // your id, echoed back on every event
	optIn: { source: "checkout" },
});

// Set the language later, or mark enrollment state read by gated automations.
// `metadata` is shallow-merged; `preferredLanguage: null` clears it.
await wabery.contacts.update(contact.id, {
	preferredLanguage: "id",
	metadata: { linked: true },
});

Channels & routing

A channel's routing_mode decides what happens to inbound on it — EXTERNAL forwards to your project webhook, FLOWS runs native automations, NONE records only. For a dedicated number this overrides the project's routing_mode, so a channel left in FLOWS under an EXTERNAL project silently never reaches your webhook. New channels inherit the project routing at connect time.

list() / get() return a routing_warning when a channel would drop inbound, and update() lets you fix it (or set it in CI):

const { data } = await wabery.channels.list();
for (const channel of data) {
	if (channel.routing_warning) console.warn(channel.id, channel.routing_warning);
}

// Forward this dedicated number's inbound to your webhook.
await wabery.channels.update("channel_...", { routingMode: "EXTERNAL" });

// Or set the project-wide default that new channels inherit.
await wabery.projects.update("project_...", { routingMode: "EXTERNAL" });

WhatsApp channels and projects also expose publish_readiness. The sandbox can publish Flows but cannot submit templates; dedicated WhatsApp channels must have a Meta-verified business account before Flows or templates are submitted. Template submission and proactive template sends also require a Meta payment method; if NO_PAYMENT_METHOD is present, add one in Meta Business Settings before sending templates. If readiness blockers such as BUSINESS_NOT_VERIFIED or NO_PAYMENT_METHOD are present, publish/template calls return 412 with the same publish_readiness object and do not queue work.

WhatsApp Flows

Author a flow's draft with the typed builders, apply it, then publish:

import { defineFlow, screen, textField, numberField, choiceField } from "@wabery/sdk";

const draft = defineFlow([
	screen("intake", "Tell us about your project", [
		textField("name", { label: "Your name", required: true }),
		numberField("budget", { label: "Budget (USD)" }),
		choiceField("timeline", {
			label: "Timeline",
			options: [
				{ id: "asap", title: "ASAP" },
				{ id: "this_month", title: "This month" },
			],
		}),
	]),
]);

await wabery.config.apply({
	flows: [{ key: "lead_intake", name: "Lead intake", draft }],
	automations: [],
});

// config.apply works in flow keys; publish/send work in flow_ids. Bridge them:
const flow = await wabery.flows.findByConfigKey("lead_intake");
await wabery.flows.publish(flow.id); // queues the publish job
// Meta reviews asynchronously; poll until it is sendable:
await wabery.flows.waitUntilPublishable(flow.id, { timeoutMs: 120_000 });
// Or force a one-off live status refresh:
await wabery.flows.status(flow.id);

const { flow_token } = await wabery.flows.send(flow.id, {
	channelId: "channel_...",
	contactId: "contact_...", // exactly one recipient: contactId | conversationId | to
});

// Inspect, debug, and clean up:
const full = await wabery.flows.get(flow.id); // includes the compiled flow_json
const { data: events } = await wabery.flows.events(flow.id); // publish/submission/reconcile log
await wabery.flows.delete(flow.id); // draft → delete on Meta, published → deprecate

Prefer hand-written Meta Flow JSON? Set flow_json (typed as MetaFlowJson) instead of draft, and check it locally before publishing with validateFlowJson(json) — a structural-only check (missing version, no terminal screen, dangling/unreachable screens). A clean result does not guarantee Meta will accept it (e.g. flow-name uniqueness is only enforced at publish). The completed submission arrives on your flow.completed webhook — there is no onSubmit URL — with valid and, when invalid, validation_errors. Correlate it via the flow_token returned from flows.send, or via contact_reference echoed on every event.

Localization

WhatsApp Flows have no native locale switch. For fixed copy, translate a draft into one draft per language with localizeFlow, publish each as a flow that shares one config_key with a distinct locale (omit locale for the group default), then send by config key — Wabery resolves the variant for the contact's language:

import { localizeFlow } from "@wabery/sdk";

const byLocale = localizeFlow(draft, {
  en: {}, // group default (no locale)
  es: { "Your name": "Tu nombre", Submit: "Enviar" },
});

await wabery.config.apply({
  flows: [
    { key: "lead_intake", name: "Lead intake", draft: byLocale.en },
    { key: "lead_intake", name: "Lead intake (es)", locale: "es", draft: byLocale.es },
  ],
  automations: [],
});
// publish each variant, then let Wabery pick by the contact's preferredLanguage
// (explicit locale -> contact language -> "" default), falling back gracefully:
const { flow_token, locale } = await wabery.flows.sendByConfigKey("lead_intake", {
  channelId: "channel_...",
  contactId: "contact_...", // uses this contact's preferredLanguage
  locale: "es", // optional override
});

For per-user copy, drive translations through the data-exchange endpoint instead.

Proactive flows (outside the 24h window)

An interactive flows.send only works inside the 24-hour customer-service window. To reach a contact proactively, send an approved flow-type message template (a template with a FLOW button) and pass the flow token via a button action:

await wabery.messages.send({
  channelId: "channel_...",
  to: "+14155550100",
  template: {
    name: "daily_reminder",
    language: "en",
    components: [
      {
        type: "button",
        sub_type: "flow",
        index: "0",
        parameters: [{ type: "action", action: { flow_token: "..." } }],
      },
    ],
  },
});

Dynamic flows (data-exchange)

For DATA_EXCHANGE flows, Wabery POSTs to your data_exchange_url signed with the same x-wabery-signature scheme and secret as webhooks. Verify and build responses with the typed helpers:

import { flowOptions, dataExchangeScreen, dataExchangeError } from "@wabery/sdk";

const req = wabery.webhooks.verifyDataExchange(rawBody, signatureHeader, secret);

if (req.action === "ping") return { data: { status: "active" } };

const projects = await projectsFor(req.contact_reference, req.data?.workspace);
return dataExchangeScreen("SELECT", {
	...flowOptions("project", projects), // -> { project_options: [{ id, title }] }
});
// To surface a validation problem instead of advancing:
// return dataExchangeError("SELECT", "No active projects in this workspace.");

Replace, not merge: the data you return becomes the target screen's data model wholesale — resend all <field>_options and prefills the screen needs on every response that lands on it.

Hosted functions

Hosted functions are Wabery-hosted TypeScript handlers for message-triggered logic, Business Agent tools, and external MCP/tool invocation. Create metadata, deploy source, test, then expose as a tool if needed:

const fn = await wabery.functions.create({
	slug: "quote-customer",
	name: "Quote customer",
	triggerType: "ANY_MESSAGE",
});

await wabery.functions.deploy(fn.id, {
	source: `
export default async function handler(event, ctx) {
  ctx.log("received", event.type);
  return { ok: true };
}
`,
});

await wabery.functions.test(fn.id, { text: "I need a quote" });

await wabery.functions.update(fn.id, {
	exposeAsMcpTool: true,
	inputSchema: { type: "object" },
});

await wabery.functions.invoke(fn.id, {
	arguments: { orderId: "ord_123" },
});

const logs = await wabery.functions.logs(fn.id);

Use API scopes functions:read, functions:write, and functions:invoke for least-privilege keys.

Local testing

Exercise your webhook and data-exchange handlers without deploying. signPayload produces the x-wabery-signature header so you can POST fixtures to your local server:

import { signPayload } from "@wabery/sdk";

const body = JSON.stringify(myFakeEvent);
await fetch("http://localhost:3000/webhooks", {
	method: "POST",
	headers: { "x-wabery-signature": signPayload(body, secret) },
	body,
});

Or use the CLI: wabery webhooks send-test --url <url> --secret <s> and wabery flows test --url <url> --secret <s> --action data_exchange.