TypeScript SDK

TypeScript SDK

The official TypeScript SDK for Sent LogoSent provides type-safe access to the entire Sent API. Built for modern Node.js applications with native ESM and CommonJS support, automatic retries, and comprehensive error handling.

Installation

npm install @sentdm/sentdm
yarn add @sentdm/sentdm
pnpm add @sentdm/sentdm
bun add @sentdm/sentdm

Quick Start

Initialize the client

import SentDm from '@sentdm/sentdm';

const client = new SentDm();  // Uses SENT_DM_API_KEY env var by default

Send your first message

const response = await client.messages.send({
  to: ['+1234567890'],
  template: {
    id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
    name: 'welcome',
    parameters: {
      name: 'John Doe'
    }
  }
});

console.log('Message sent:', response.data.messages[0].id);
console.log('Status:', response.data.messages[0].status);

Authentication

The client can be configured using environment variables or explicitly:

import SentDm from '@sentdm/sentdm';

// Using environment variables (recommended)
// SENT_DM_API_KEY=your_api_key
const client = new SentDm();

// Or explicit configuration
const client = new SentDm({
  apiKey: 'your_api_key',
});

Send Messages

Send a message

const response = await client.messages.send({
  to: ['+1234567890'],
  template: {
    id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
    name: 'welcome',
    parameters: {
      name: 'John Doe',
      order_id: '12345'
    }
  },
  channel: ['whatsapp', 'sms']  // Optional: defaults to template channels
});

console.log('Message ID:', response.data.messages[0].id);
console.log('Status:', response.data.messages[0].status);

Test mode

Use testMode to validate requests without sending real messages:

const response = await client.messages.send({
  to: ['+1234567890'],
  template: {
    id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
    name: 'welcome'
  },
  testMode: true  // Validates but doesn't send
});

// Response will have test data
console.log('Validation passed:', response.data.messages[0].id);

Handle errors

When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of APIError will be thrown:

try {
  const response = await client.messages.send({
    to: ['+1234567890'],
    template: {
      id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
      name: 'welcome'
    }
  });
} catch (err) {
  if (err instanceof SentDm.APIError) {
    console.log(err.status); // 400
    console.log(err.name); // BadRequestError
    console.log(err.headers); // {server: 'nginx', ...}
  } else {
    throw err;
  }
}

Error codes are as follows:

Status CodeError Type
400BadRequestError
401AuthenticationError
403PermissionDeniedError
404NotFoundError
422UnprocessableEntityError
429RateLimitError
>=500InternalServerError
N/AAPIConnectionError

Retries

Certain errors will be automatically retried 2 times by default, with a short exponential backoff. Connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default.

// Configure the default for all requests:
const client = new SentDm({
  maxRetries: 0, // default is 2
});

// Or, configure per-request:
await client.messages.send({
  to: ['+1234567890'],
  template: { id: 'welcome-template', name: 'welcome' }
}, {
  maxRetries: 5,
});

Timeouts

Requests time out after 1 minute by default. You can configure this with a timeout option:

// Configure the default for all requests:
const client = new SentDm({
  timeout: 20 * 1000, // 20 seconds (default is 1 minute)
});

// Override per-request:
await client.messages.send({
  to: ['+1234567890'],
  template: { id: 'welcome-template', name: 'welcome' }
}, {
  timeout: 5 * 1000,
});

Contacts

Create and manage contacts:

// Create a contact
const contact = await client.contacts.create({
  phoneNumber: '+1234567890',
});

console.log('Contact ID:', contact.data.id);

// List contacts
const contacts = await client.contacts.list({
  limit: 100,
});

console.log('Total:', contacts.data.length);

// Get a contact
const contact = await client.contacts.get('contact-uuid');

// Update a contact
const updated = await client.contacts.update('contact-uuid', {
  phoneNumber: '+1987654321',
});

// Delete a contact
await client.contacts.delete('contact-uuid');

Templates

List and retrieve templates:

// List all templates
const templates = await client.templates.list();

for (const template of templates.data) {
  console.log(`${template.name} (${template.status}): ${template.id}`);
}

// Get a specific template
const template = await client.templates.get('template-uuid');

console.log('Template name:', template.data.name);
console.log('Status:', template.data.status);

Framework Integration

NestJS

See the NestJS Integration guide for complete dependency injection, module setup, and testing examples.

// messages/messages.service.ts
import { Injectable, Inject } from '@nestjs/common';
import SentDm from '@sentdm/sentdm';
import { SENT_CLIENT } from '../sent/sent.module';

@Injectable()
export class MessagesService {
  constructor(
    @Inject(SENT_CLIENT) private readonly sentClient: SentDm,
  ) {}

  async sendWelcomeMessage(phoneNumber: string, name: string) {
    const response = await this.sentClient.messages.send({
      to: [phoneNumber],
      template: {
        id: 'welcome-template',
        name: 'welcome',
        parameters: { name }
      }
    });
    return response.data.messages[0];
  }
}

Next.js (App Router)

See the Next.js Integration guide for complete Server Actions, Route Handlers, and Edge Runtime examples.

// app/api/send-message/route.ts
import SentDm from '@sentdm/sentdm';
import { NextResponse } from 'next/server';

const client = new SentDm();

export async function POST(request: Request) {
  const { phoneNumber, templateId, variables } = await request.json();

  try {
    const response = await client.messages.send({
      to: [phoneNumber],
      template: {
        id: templateId,
        name: 'welcome',
        parameters: variables
      }
    });

    return NextResponse.json({
      messageId: response.data.messages[0].id,
      status: response.data.messages[0].status,
    });
  } catch (error) {
    if (error instanceof SentDm.APIError) {
      return NextResponse.json(
        { error: error.message },
        { status: error.status }
      );
    }
    throw error;
  }
}

Express.js

See the Express.js Integration guide for complete dependency injection, validation, and structured logging examples.

import express from 'express';
import SentDm from '@sentdm/sentdm';

const app = express();
const client = new SentDm();

app.use(express.json());

app.post('/send-message', async (req, res) => {
  const { phoneNumber, templateId, variables } = req.body;

  try {
    const response = await client.messages.send({
      to: [phoneNumber],
      template: {
        id: templateId,
        name: 'welcome',
        parameters: variables
      }
    });

    res.json({
      success: true,
      message: response.data,
    });
  } catch (error) {
    if (error instanceof SentDm.APIError) {
      res.status(error.status).json({ error: error.message });
    } else {
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});

app.listen(3000);

Webhooks

Recommended pattern: Webhooks are the primary way to track message delivery — don't poll the API. Save the message ID when you send, then update your database as webhook events arrive.

Sent delivers signed POST requests to your endpoint for every status change. Two event types exist:

  • messages — Message status changes (SENT, DELIVERED, READ, FAILED, …)
  • templates — WhatsApp template approval/rejection

Every request includes these headers:

HeaderDescription
X-Webhook-IDUUID of the webhook configuration
X-Webhook-TimestampUnix timestamp (seconds) when the event was created
X-Webhook-Signaturev1,{base64} — HMAC-SHA256 of {webhookId}.{timestamp}.{rawBody}
X-Webhook-Event-Typemessages or templates

The signing secret (from the Sent Dashboard) has a whsec_ prefix. Strip it and base64-decode the remainder to obtain the raw HMAC key.

import express from 'express';
import { createHmac, timingSafeEqual } from 'crypto';

const app = express();

// Must use raw body — do NOT use express.json() for this route
app.post('/webhooks/sent', express.raw({ type: 'application/json' }), (req, res) => {
  const payload    = req.body as Buffer;
  const webhookId  = req.headers['x-webhook-id'] as string;
  const timestamp  = req.headers['x-webhook-timestamp'] as string;
  const signature  = req.headers['x-webhook-signature'] as string;

  // 1. Verify: signed content = "{webhookId}.{timestamp}.{rawBody}"
  const secret   = process.env.SENT_WEBHOOK_SECRET!;          // "whsec_abc123..."
  const keyBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const signed   = `${webhookId}.${timestamp}.${payload.toString('utf8')}`;
  const expected = 'v1,' + createHmac('sha256', keyBytes).update(signed).digest('base64');

  if (!signature || !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 2. Optional: reject replayed events older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return res.status(401).json({ error: 'Timestamp too old' });
  }

  const event = JSON.parse(payload.toString());

  // 3. Handle events — update message status in your own database
  if (event.field === 'messages') {
    const { message_id, message_status, channel } = event.payload;
    // await db.messages.update({ where: { sentId: message_id }, data: { status: message_status } })
    console.log(`Message ${message_id} → ${message_status} (${channel})`);
  }

  // 4. Always return 200 quickly
  res.json({ received: true });
});

See the Webhooks reference for the full payload schema and all status values.

Logging

The log level can be configured via the SENT_DM_LOG environment variable or using the logLevel client option:

import SentDm from '@sentdm/sentdm';

const client = new SentDm({
  logLevel: 'debug', // Show all log messages
});

Available log levels: 'debug', 'info', 'warn' (default), 'error', 'off'

Source & Issues

Getting Help

On this page