Sendable Docs
Node.js

TypeScript SDK

Use the official Sendable TypeScript SDK with session-scoped and account-scoped API keys.

Installation

npm install @sendable-dev/sdk

Create a Client

The SDK uses one Sendable client with resource namespaces like messages, sessions, and webhooks.

import { Sendable } from '@sendable-dev/sdk'

export const sendable = new Sendable({
  apiKey: process.env.SENDABLE_API_KEY!,
  apiKeyScope: 'session',
  baseUrl: process.env.SENDABLE_API_URL,
  webhookSecret: process.env.SENDABLE_WEBHOOK_SECRET,
})

Configuration

OptionRequiredDescription
apiKeyYesYour Sendable API key
apiKeyScopeNo'session' or 'account'; recommended so the SDK can fail fast on invalid operations
baseUrlNoAPI base URL, defaults to https://api.sendable.dev
webhookSecretNoSvix whsec_... signing secret used by webhooks verification helpers
fetchNoCustom fetch implementation
headersNoDefault headers added to every request

Session-Scoped Usage

Use a session-scoped API key for messages, contacts, groups, session runtime operations, and webhooks.

Send a Message

const result = await sendable.messages.send({
  chatId: '[email protected]',
  text: { content: 'Hello from Sendable' },
})

console.log(result.messageId, result.status)

Mention Contacts In a Group

await sendable.messages.send({
  chatId: '[email protected]',
  text: { content: 'asd @6285176702468' },
  mentions: ['[email protected]'],
})

Format WhatsApp Text

Use the SDK formatter helpers to build text.content strings without changing the send API:

import { Sendable, waText } from '@sendable-dev/sdk'

const content = waText()
  .bold('Release update')
  .newline()
  .quote('Deployment completed')
  .newline()
  .bulletedList(['API healthy', 'Workers healthy'])
  .build()

await sendable.messages.send({
  chatId: '[email protected]',
  text: { content },
})

Send a Message Asynchronously

const queued = await sendable.messages.sendAsync(
  {
    chatId: '[email protected]',
    text: { content: 'Queued message' },
  },
  {
    idempotencyKey: 'order-123-notify',
  },
)

const delivery = await sendable.messages.getDeliveryStatus(queued.runId)

List Messages

const messages = await sendable.messages.list({
  chatId: '[email protected]',
  limit: 25,
})

Decrypt Media Locally

Use the standalone SDK utility when you already have a WhatsApp media url and mediaKey. This runs in Node/Bun and does not proxy decrypted bytes through the Sendable API.

import { Sendable, decryptWhatsAppMedia } from '@sendable-dev/sdk'

const message = await sendable.messages.get('wamid.HBgLN...')

if (!message.image?.url || !message.image.key) {
  throw new Error('Message does not include decryptable image metadata')
}

const media = await decryptWhatsAppMedia({
  url: message.image.url,
  mediaKey: message.image.key,
  mediaType: 'image',
  mimeType: message.image.mimetype,
  messageId: message.id,
})

await Bun.write(media.fileName, media.data)

Decrypt Webhook Media

If you store Sendable webhook events, you can pass the full event or event.data directly into the SDK wrapper without manually mapping url and mediaKey.

import { decryptWebhookMedia } from '@sendable-dev/sdk'

const media = await decryptWebhookMedia(event)
await Bun.write(media.fileName, media.data)

Session Runtime Actions

const status = await sendable.sessions.status()
await sendable.sessions.start()
await sendable.sessions.restart()
await sendable.sessions.end()

Contacts, Groups, and Webhooks

const contacts = await sendable.contacts.list({ limit: 20 })
const groups = await sendable.groups.list({ limit: 20 })
const webhooks = await sendable.webhooks.list({ page: 1, limit: 25 })

Account-Scoped Usage

Use an account-scoped API key for account-level session management.

import { Sendable } from '@sendable-dev/sdk'

const accountClient = new Sendable({
  apiKey: process.env.SENDABLE_ACCOUNT_API_KEY!,
  apiKeyScope: 'account',
  baseUrl: process.env.SENDABLE_API_URL,
})

const sessions = await accountClient.sessions.list()

const created = await accountClient.sessions.create({
  state: 'ID',
  name: 'Primary session',
})

You can also fetch, update, and delete sessions:

const session = await accountClient.sessions.get(created.id)

await accountClient.sessions.update(created.id, {
  name: 'Production session',
  isActive: true,
})

await accountClient.sessions.delete(created.id)

Webhook Verification

If you pass webhookSecret into the client, you can verify and parse Svix-signed webhooks with the SDK:

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

const app = express()

app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const payload = req.body.toString()

  try {
    const event = await sendable.webhooks.verifyAndParse(payload, req.headers)
    console.log(event.type, event.data)
    return res.json({ received: true })
  } catch {
    return res.status(401).json({ error: 'Invalid signature' })
  }
})

Error Handling

The SDK throws typed errors you can branch on:

import {
  SendableApiError,
  SendableScopeError,
} from '@sendable-dev/sdk'

try {
  await sendable.messages.send({
    chatId: '[email protected]',
    text: { content: 'Hello' },
  })
} catch (error) {
  if (error instanceof SendableScopeError) {
    console.error('Wrong API key scope for this operation')
  } else if (error instanceof SendableApiError) {
    console.error(error.status, error.message, error.body)
  } else {
    throw error
  }
}

Next Steps

On this page