TypeScript SDK

Sending messages from Express with the Sent TypeScript SDK

This guide shows you how to wire Sent messaging into an existing Express app: install the TypeScript SDK, configure a shared client, send a template message from a route, receive delivery webhooks, and verify the whole loop in sandbox mode.

Prerequisites

This guide assumes a working Express 5 app with TypeScript. You also need:

Install the SDK

Add the SDK to your existing project:

npm install @sentdm/sentdm

Configure the client

Set your credentials as environment variables so they stay out of code; the webhook secret arrives in step 4:

export SENT_DM_API_KEY="your-api-key"
export SENT_DM_WEBHOOK_SECRET="whsec_your_signing_secret"

Create one shared client for the whole process; new SentDm() reads SENT_DM_API_KEY by default:

// src/sent.ts
import SentDm from '@sentdm/sentdm';

export const sent = new SentDm();

Send a template message from a route

Add a router that calls messages.send; the pass-through sandbox flag lets callers exercise the route without delivering anything:

// src/routes/messages.ts
import { Router } from 'express';
import { sent } from '../sent';

export const messagesRouter = Router();

messagesRouter.post('/send', async (req, res, next) => {
  try {
    const { phoneNumber, templateName, parameters, channels, sandbox } = req.body;
    const response = await sent.messages.send({
      to: [phoneNumber],                              // E.164 format, for example +14155551234
      template: { name: templateName, parameters },   // reference by name or id, never both
      channel: channels,                              // omit to let Sent pick per recipient
      sandbox: sandbox ?? false,                      // true = validate and simulate only
    });
    const recipient = response.data.recipients[0];
    res.status(202).json({ messageId: recipient.message_id, status: response.data.status });
  } catch (err) {
    next(err);
  }
});

Sent accepts sends asynchronously: the API responds with status QUEUED and one message_id per recipient-and-channel pair. Store the message_id, because delivery outcomes arrive on your webhook endpoint instead of in this response.

Receive delivery webhooks

Add a verification helper that checks the X-Webhook-Signature header against the raw request body before your handler trusts any event. The scheme is HMAC-SHA256 over {X-Webhook-ID}.{X-Webhook-Timestamp}.{rawBody}, keyed with the base64-decoded secret after stripping its whsec_ prefix. Refer to webhook signature verification for the full scheme:

// src/webhook-signature.ts
import { createHmac, timingSafeEqual } from 'crypto';

// Signature format: "v1,{base64(hmac)}" over "{webhookId}.{timestamp}.{rawBody}"
export function verifyWebhookSignature(
  payload: Buffer,
  webhookId: string,
  timestamp: string,
  signature: string,
  secret: string,
): boolean {
  const keyBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const signedContent = `${webhookId}.${timestamp}.${payload.toString('utf8')}`;
  const expected = 'v1,' + createHmac('sha256', keyBytes).update(signedContent).digest('base64');
  const signatureBuffer = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);
  return signatureBuffer.length === expectedBuffer.length && timingSafeEqual(signatureBuffer, expectedBuffer);
}

Add the endpoint itself. Every event arrives in the same envelope (field, event, timestamp, payload), so one handler routes all of them; return 200 quickly and do slow work elsewhere:

// src/routes/webhooks.ts
import { Router } from 'express';
import { verifyWebhookSignature } from '../webhook-signature';

export const webhooksRouter = Router();

webhooksRouter.post('/sent', (req, res) => {
  const webhookId = req.header('x-webhook-id') ?? '';
  const timestamp = req.header('x-webhook-timestamp') ?? '';
  const signature = req.header('x-webhook-signature') ?? '';
  const secret = process.env.SENT_DM_WEBHOOK_SECRET ?? '';

  if (!signature || !secret || !verifyWebhookSignature(req.body, webhookId, timestamp, signature, secret)) {
    return res.status(401).json({ error: 'Invalid webhook signature' });
  }
  // Reject replayed events older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return res.status(401).json({ error: 'Webhook timestamp too old' });
  }

  const event = JSON.parse(req.body.toString('utf8'));
  const payload = event.payload ?? {};

  if (event.field === 'message') {
    switch (event.event) {
      case 'message.delivered':
        console.log(`Message ${payload.message_id} delivered`);
        break;
      case 'message.failed':
        console.error(`Message ${payload.message_id} failed (status ${payload.message_status})`);
        break;
      case 'message.received':
        console.log(`Inbound ${payload.channel} from ${payload.inbound_number}: ${payload.text}`);
        break;
      default:
        console.log(`Message ${payload.message_id} status: ${payload.message_status}`);
    }
  }
  res.json({ received: true });
});

Mount both routers. The webhook path must receive the raw body. Signature verification needs the exact bytes, so register express.raw for it, not express.json:

// src/app.ts (excerpt)
app.use('/api/messages', express.json(), messagesRouter);
app.use('/webhooks', express.raw({ type: 'application/json' }), webhooksRouter);

Then tell Sent where to deliver events. If you prefer a UI, use the webhooks getting started guide; otherwise register over the API:

curl -X POST https://api.sent.dm/v3/webhooks \
  -H "x-api-key: $SENT_DM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "Express integration",
    "endpoint_url": "https://your-domain.example/webhooks/sent",
    "event_types": ["message"]
  }'

Copy two values from the response: the webhook id (used to test delivery in the next step) and signing_secret. Put the secret in SENT_DM_WEBHOOK_SECRET. The message event type covers every message event; the webhook event types reference lists all payload fields.

Verify the integration

Start the app with your credentials loaded (for example, npm run dev). Then send a sandbox message through your new route. Full validation runs, but nothing is delivered and no credits are consumed:

curl -X POST http://localhost:3000/api/messages/send \
  -H "Content-Type: application/json" \
  -d '{"phoneNumber": "+14155551234", "templateName": "welcome",
       "parameters": {"name": "Ada"}, "sandbox": true}'

The response should contain a messageId and "status": "QUEUED". A 400 here means the request shape is wrong; sandbox requests return real validation errors.

Now confirm webhook delivery end to end. Ask Sent to deliver a signed test event, replacing the ID with the webhook id you copied:

curl -X POST https://api.sent.dm/v3/webhooks/YOUR_WEBHOOK_ID/test \
  -H "x-api-key: $SENT_DM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event_type": "message.delivered"}'

Your server log should show a Message ... delivered line, and the endpoint should have answered 200 {"received": true}. Test events travel the same signed delivery pipeline as real events, so a 401 in your log means the signing secret or verification code is wrong. Sent attempts a test event exactly once, so re-run the command after each fix.

Adapt this to your app

  • If your app already parses JSON globally with app.use(express.json()), scope it away from the webhook path, because verification fails on re-serialized bodies.
  • If webhook processing does slow work (database writes, downstream calls), acknowledge with 200 first and process asynchronously so retries do not pile up; see handling webhook retries.
  • To send free-form text instead of a template, pass text instead of template. Each send carries exactly one of the two.
  • If you need input validation, rate limiting, or structured logging around these routes, the appendix below has the scaffolding.

Appendix: production scaffolding

The numbered steps stay on the core messaging tasks. The blocks below are optional scaffolding for a production Express stack. Adapt them to your own conventions rather than adopting them wholesale.

Next steps

On this page