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:
- A Sent API key from the API Keys page in your Sent Dashboard
- A public HTTPS URL for webhook delivery. For local work, open a tunnel as described in the webhook local development guide
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
textinstead oftemplate. 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
- Review the webhook event types reference for every payload field
- Work through the webhook production checklist before going live
- Explore the TypeScript SDK reference for retries, timeouts, and error types
- New to Sent? The first integration tutorial walks the same loop from scratch
Sending messages from NestJS with the Sent TypeScript SDK
Wire the Sent TypeScript SDK into a NestJS app: install, register a client provider, send from a controller, verify webhooks, and test with sandbox mode.
Python SDK
Official Python SDK for Sent. Send SMS, WhatsApp, and RCS messages with Pythonic elegance and full async support.