Get started
TypeScript SDK
Two npm packages: the library to call the API and receive its webhooks, and the nodium command to develop locally.
The SDK wraps this API for TypeScript and JavaScript. It describes only routes that exist, with their types; from any other language, call the API directly — every route of the Reference carries a curl, Node.js and SDK sample.
@nodium.io/whatsapp— the library: every resource of the API (messages,templates,flows,broadcasts,tenants,webhooks,events…). No runtime dependency: the nativefetchof Node 20+, Bun, Deno and browsers. ESM and CommonJS, types included. MIT licensed.@nodium.io/whatsapp/server— what needsnode:cryptoand runs on your server only: verifying webhooks (verifyWebhook) and answering dynamic Flows (receiveFlowRequest,respondToFlow,flowError,completeFlow). Kept apart so the main entry also runs in browsers and edge functions.@nodium.io/cli— thenodiumcommand: replays your events to your local server, signed like production, and sends a message from the terminal.
Install
npm install @nodium.io/whatsapp # the librarynpm install -g @nodium.io/cli # the nodium command (optional)export NODIUM_KEY=nod_… # Settings › API keys in the consoleA first message
import { Nodium } from '@nodium.io/whatsapp'const nodium = new Nodium(process.env.NODIUM_KEY!)const [channel] = await nodium.channels.list()const { message } = await nodium.messages.send({ channelId: channel.id, to: '+33612345678', type: 'template', // a first message is always an approved template templateName: 'order_ready', variables: { 1: 'Karim' }})if (message?.status === 'failed') console.error(message.errorInfo?.title)messages.sendis the one way to send:typepicks the kind (text,template,interactive,location,contacts, a file bymediaId) and its fields;tois a number in E.164 form, orrecipientawa:…id for a contact who shows no number.- It always carries an idempotency key: pass your own
idempotencyKey, or the SDK draws one, fixed for the call, so a retry can never send twice. - A refused send is not an exception: the call resolves with
message.status === "failed", andmessage.errorInfosays what happened, what to do and who acts.
Your clients
const client = await nodium.tenants.create( { name: 'Agence Horizon', externalId: 'crm-4187' }, { connectLink: { redirectUrl: 'https://app.example.com/whatsapp' } })console.log(client.connectLink.url) // send it to your customer: shown onceconst horizon = nodium.as('crm-4187') // adds Nodium-Tenant to every callawait horizon.messages.send({ channelId, to: '+33612345678', type: 'text', text: 'Hello' })Without as(), calls act on My company — or on the client a key is pinned to. as() takes Nodium's id or your own externalId. See Your clients.
Receive webhooks
import express from 'express'import { NodiumSignatureError, verifyWebhook } from '@nodium.io/whatsapp/server'app.post('/webhooks/nodium', express.raw({ type: 'application/json' }), (req, res) => { try { const event = verifyWebhook(req.body, req.headers, process.env.NODIUM_WEBHOOK_SECRET!) if (event.event === 'message.received') handle(event.external_id, event.data) res.sendStatus(200) // event.id is the same on every redelivery } catch (error) { if (error instanceof NodiumSignatureError) return res.sendStatus(400) throw error }})Verify the raw body, never a re-serialised one. During a secret rotation, pass both secrets as an array. Calls older than 5 minutes are refused. Events are typed: NodiumEvent is a union on event. See Webhooks.
Answer a dynamic Flow
import express from 'express'import { completeFlow, flowError, receiveFlowRequest, respondToFlow } from '@nodium.io/whatsapp/server'app.post('/whatsapp/flow', express.raw({ type: 'application/json' }), async (req, res) => { const call = receiveFlowRequest(req.body, req.headers, process.env.NODIUM_FLOW_SECRET!) // call.action: 'INIT' | 'data_exchange' | 'BACK' · call.screen · call.data · call.flowToken if (call.action === 'INIT') return res.json(respondToFlow({ screen: 'SLOTS', data: { days: await nextDays() } })) if (call.screen === 'SLOTS') { const slots = await freeSlots(call.data.date as string) if (!slots.length) return res.json(flowError('SLOTS', 'No slot left that day.')) return res.json(respondToFlow({ screen: 'CONFIRM', data: { slots } })) } res.json(completeFlow(call.flowToken, { booked: true })) // closes the Flow})Nodium decrypts Meta's calls and relays them signed with the secret of the Flow's address (flows.setDataEndpoint); your JSON answer is encrypted back for Meta. Answer within about 9 seconds. completeFlow closes the form; the answer then reaches your webhook as message.received with reply.kind: "flow". See Flows.
Pages, retries and errors
- A list that takes
limitandcursorreturns{ items, nextCursor, hasMore }and has aneach()that walks every page:for await (const tenant of nodium.tenants.each()). - 429 and 5xx are retried (
maxRetries, default 2) afterRetry-Afteror an exponential backoff. A write without an idempotency key is never retried. - Every refusal is a
NodiumError:status,code,detail,requestId,retryAfter,data, andretry—retry,retry_after,fix_and_retry,upgradeordo_not_retry.isAuthError(),isRateLimit(),isNotFound(),isNetworkError()andisPlanLimitanswer the usual questions. - Options:
new Nodium(key, { baseUrl, fetch, timeoutMs, maxRetries })— defaultshttps://nodium.io/api/v1, the globalfetch, 30 seconds, 2 retries.
import { NodiumError } from '@nodium.io/whatsapp'try { await nodium.tenants.create({ name: 'Horizon', externalId: 'crm-4187' })} catch (error) { if (!(error instanceof NodiumError)) throw error if (error.code === 'tenant_exists') return error.data.tenantId // branch on code, never on the text if (error.retry.action === 'retry_after') await sleep(error.retry.retryAfterMs!) if (error.isPlanLimit) notifyBilling(error.requestId)}A route the SDK does not wrap yet
// A route the SDK does not wrap yet: same key, same client header, same retries.const encryption = await nodium.as('crm-4187').request({ method: 'GET', path: '/flows/encryption' })request() returns the content of data. A write is retried only when you give it an idempotencyKey.
Develop locally
npx @nodium.io/cli listen --forward http://localhost:3000/webhook --secret whsec_… reads your event log and calls your local server with real, signed deliveries — no tunnel, no public address. See Webhooks.
Versions
The packages follow semantic versioning: a patch fixes, a minor adds, a major changes what exists. Their README on npm lists every method: npmjs.com/package/@nodium.io/whatsapp and npmjs.com/package/@nodium.io/cli.