By language

WhatsApp API in Node.js: a practical guide with the official SDK

In Node.js, the shortest path to the WhatsApp API is the d-api-sdk package: one npm install, one client instance, and you can already create the connection, read the QR code and send your first message. This guide covers the whole flow in TypeScript, including the Express webhook and error handling.

By D-API engineering team6 min read

Why use the SDK instead of building the requests yourself

The whole D-API WhatsApp API is REST, so nothing stops you from writing the calls with fetch or axios. The SDK pays off day to day: methods are named after actions (sessions.create, messages.sendText, interactive.sendList), parameters are typed, and the editor warns you about a missing required field before the code ships to production.

Node.js is the only language with an official D-API library. In other stacks you integrate with the language's own HTTP client, and the contract is the same. The Node.js SDK integration page summarizes the available modules.

Installation and a configured client

Install the package and create a single client for the whole application. Keep the API key in an environment variable; it goes in the Authorization header without the Bearer prefix, and the SDK handles that for you.

npm install d-api-sdk
// src/dapi.ts
import { DApi } from 'd-api-sdk'

export const dapi = new DApi({
  apiKey: process.env.DAPI_API_KEY!,
  timeout: 10_000, // default is 30000 ms; inside an HTTP route, go lower
})

baseUrl is optional and points to https://api.d-api.cloud by default. The timeout parameter applies to every call: when it expires, the SDK aborts the fetch and throws an error with status 408. Setting this value explicitly keeps a slow request from holding one of your app's routes for half a minute.

Create the connection and show the QR code

Each WhatsApp number is a session, identified by a sessionId you choose. You pass the webhook URL at creation time, so events start flowing as soon as the number connects.

import { dapi } from './dapi'

await dapi.sessions.create({
  sessionId: 'downtown-store',
  connectionMode: 'qr',
  webhookUrl: 'https://your-app.com/webhooks/whatsapp/' + process.env.WEBHOOK_TOKEN,
})

const { qrCodeImage } = await dapi.sessions.getQRCode('downtown-store')
// qrCodeImage is a data URI (data:image/png;base64,...)
// just use it as the src of an <img> in your customer's dashboard

The QR code expires within seconds and the API renews it. On the connection screen, call getQRCode again at short intervals until the number connects, or listen for the connection.status event on the webhook and switch screens when data.status comes in as connected. Pairing details, including pairing by code instead of QR, are in WhatsApp API with QR code.

Send text and a list in a few lines

Once the number is connected, sending text takes three fields: the session, the recipient in international format without the plus sign, and the content. The interactive list adds the button label and the sections with the options.

await dapi.messages.sendText({
  sessionId: 'downtown-store',
  to: '14155550123',
  text: 'Hi Anna! Your order 4812 has been approved.',
})

await dapi.interactive.sendList({
  sessionId: 'downtown-store',
  to: '14155550123',
  description: 'When would you like it delivered?',
  buttonText: 'See time slots',
  sections: [
    {
      title: 'Delivery',
      rows: [
        { rowId: 'morning', title: 'Morning', description: '8am to 12pm' },
        { rowId: 'afternoon', title: 'Afternoon', description: '1pm to 6pm' },
      ],
    },
  ],
})

The rowId is what comes back to your system when the person picks an option, so use values your code understands without an extra lookup. Limits on sections and rows, and what WhatsApp allows in buttons, are covered in WhatsApp buttons and lists.

Handle errors and timeouts

When the API responds with an error status, it returns a body shaped like { success: false, error, statusCode }. The SDK turns that into a DApiError exception, with message taken from the error field, the HTTP status, and the original body in response.

import type { DApiError } from 'd-api-sdk'

try {
  await dapi.messages.sendText({ sessionId: 'downtown-store', to, text })
} catch (err) {
  const e = err as DApiError
  if (e.status === 408) {
    // client timeout: the message may or may not have gone out
    logger.warn({ to, status: e.status }, 'send got no response in time')
  } else if (e.status >= 400 && e.status < 500) {
    // invalid payload or unknown session: retrying won't help
    logger.error({ status: e.status, error: e.message }, 'send rejected')
  }
  throw err
}

Two decisions are worth making early. A 4xx error means something is wrong with the request, such as a missing field or a disconnected session, and repeating the call only adds noise. Timeouts and 5xx errors, on the other hand, may be temporary. Teams sending high volume usually put sends on a queue (BullMQ, for example), so your product's request responds fast and the send happens in the background.

Receive the webhook with Express

Incoming messages, read receipts and connection changes arrive by POST at the URL you registered. Every event has the same envelope: event, sessionId, data, timestamp and traceId. The route below validates the secret token in the URL, responds immediately, and only then handles the list reply.

import express from 'express'
import { dapi } from './dapi'

const app = express()

app.post('/webhooks/whatsapp/:token', express.json({ limit: '1mb' }), (req, res) => {
  if (req.params.token !== process.env.WEBHOOK_TOKEN) {
    res.sendStatus(401)
    return
  }
  res.sendStatus(200) // respond before any heavy work

  const { event, sessionId, data } = req.body
  if (event !== 'messages.received' || data.fromMe) return

  if (data.type === 'list_response') {
    const choice = data.data.selected_row_id // 'morning' or 'afternoon'
    const number = data.from.jid.split('@')[0]
    dapi.messages
      .sendText({ sessionId, to: number, text: 'Great, your delivery is scheduled.' })
      .catch((err) => logger.error({ err, traceId: req.body.traceId }, 'reply failed'))
  }
})

app.listen(3000)

The WhatsApp webhook does not carry an HMAC signature, so the token in the URL path acts as the secret. You can also check the User-Agent header, which arrives as Deliverify-Webhook/1.0, and whether the sessionId belongs to one of your customers. Available events and the retry policy are in WhatsApp webhooks.

Checklist before going to production

  • API key and webhook token in environment variables, never in the repository.
  • Client timeout tuned to the context: shorter inside an HTTP route, longer in a worker.
  • Deduplication by the message's data.id, because the same delivery can arrive more than once if your server is slow to respond.
  • Logging with the event's traceId, which helps support trace a specific delivery.
  • One sessionId per number. If your product will have one connection per customer, see how that is organized on the WhatsApp API for SaaS page.

Frequently asked questions

Do I need the SDK, or can I call the API directly with fetch?
Both work. The API is REST and accepts any HTTP client. d-api-sdk just saves you work: it builds the URL, sends the Authorization header, applies a timeout and ships ready-made TypeScript types.
Does d-api-sdk work with plain JavaScript, without TypeScript?
Yes. The package is published as JavaScript with type definitions bundled. In a project without TypeScript you import and call the same methods; you only lose typed autocomplete in the editor.
Which Node.js version do I need?
The SDK uses native fetch and AbortController, so it needs a Node.js version with global fetch, such as 18 or later. On older versions the import works, but the calls fail.
How do I know which option the customer picked from the list?
The reply reaches your webhook as a messages.received event with type set to list_response. Inside data.data you get selected_row_id, which is the rowId you defined when sending the list.

Try D-API's WhatsApp API

3-day trial with full access. No credit card, no lock-in.