Your first Sent integration with the TypeScript SDK
In this tutorial, you build a small TypeScript project that exercises the full Sent messaging loop: send a free-form text message, send a template message, receive message.delivered and message.failed webhooks on your own machine, and look up a message with the SDK. It takes about 30 minutes.
Every send in this tutorial runs in sandbox mode: the API validates and simulates each request but delivers nothing and consumes no credits. You can complete every step on a brand-new Sent account.
Prerequisites
Before you begin, make sure you have:
- Node.js 20 or later: check with
node --version - A Sent API key: copy an existing key, or generate one, on the API Keys page in your Sent Dashboard
- ngrok: install it with
npm install -g @ngrok/ngrok; you use it to receive webhooks locally, and the webhook local development guide explains why you need a tunnel - curl: preinstalled on macOS, Linux, and Windows 10+
Set up the project
First, create a new project and install the SDK, Express (for the webhook receiver), and tsx (to run TypeScript files directly):
mkdir sent-first-integration
cd sent-first-integration
npm init -y
npm install @sentdm/sentdm express
npm install -D tsxNow export your API key so the SDK can find it. The client reads the SENT_DM_API_KEY environment variable by default:
export SENT_DM_API_KEY="your-api-key"Let's check the install worked:
npm ls @sentdm/sentdmThe output should look something like:
sent-first-integration@1.0.0
└── @sentdm/sentdm@0.32.0Keep API keys out of version control. Environment variables are the pattern we use throughout this tutorial.
Send a free-form text message
Now we send our first message. Create a file named send-text.ts:
import SentDm from '@sentdm/sentdm';
const client = new SentDm(); // reads SENT_DM_API_KEY
async function main() {
const response = await client.messages.send({
to: ['+14155551234'], // replace with your own number in E.164 format
text: 'Hello from my first Sent integration!',
sandbox: true, // validate and simulate: nothing is delivered
});
console.log(JSON.stringify(response, null, 2));
}
main();Replace +14155551234 with your own mobile number in E.164 format, then run it:
npx tsx send-text.tsThe output should look something like:
{
"success": true,
"data": {
"status": "QUEUED",
"template_id": "00000000-0000-0000-0000-000000000000",
"template_name": "",
"recipients": [
{
"message_id": "3f2b8c41-6a0e-4c8f-9b7d-2a1e5c9d0f36",
"to": "+14155551234",
"channel": null,
"body": null
}
]
},
"error": null,
"meta": {
"request_id": "req_7X9zKp2jDw",
"timestamp": "2026-07-25T09:30:00.0000000+00:00",
"version": "v3"
}
}Copy the message_id value from your output. We use it again in the final step.
Notice three things in the response:
statusisQUEUED: the API accepts sends asynchronously (HTTP 202) and reports delivery later, through webhooks.channelisnullbecause we did not pick one. Sent auto-detects the best channel per recipient at send time.template_idis all zeros andtemplate_nameis empty because this was a free-formtextsend. Every request must carry exactly one oftextortemplate. Validation runs on sandbox requests too, so a malformed payload returns a real 400 error.
Send a template message
Channels like WhatsApp and RCS require approved templates for production messaging, so let's send one. Create send-template.ts:
import SentDm from '@sentdm/sentdm';
const client = new SentDm();
async function main() {
const response = await client.messages.send({
to: ['+14155551234'], // your number again
channel: ['sms'],
template: {
name: 'welcome',
parameters: { name: 'Ada' },
},
sandbox: true,
});
console.log(JSON.stringify(response.data, null, 2));
}
main();Run it:
npx tsx send-template.tsThe output should look something like:
{
"status": "QUEUED",
"template_id": "00000000-0000-0000-0000-000000000000",
"template_name": "welcome",
"recipients": [
{
"message_id": "9c41d2aa-7b3f-4e58-8f06-5d2e91c7ab10",
"to": "+14155551234",
"channel": "sms",
"body": null
}
]
}Notice that this time each recipient carries channel: "sms". When you list several channels, Sent creates a separate message (with its own message_id) for every recipient-and-channel pair.
We reference the template by name here; a template reference takes either id or name, never both. In sandbox mode the template is not looked up, so any name passes. A live send requires a template that exists on your account, and returns a 404 RESOURCE_002 (template not found) error otherwise.
You have now sent messages both ways the API supports. Next, let's find out what happens to a message after it is queued.
Start a local webhook receiver
Sent reports delivery progress by POSTing events to your server. We start with a minimal receiver. Create webhook-server.ts:
import express from 'express';
const app = express();
// Raw body: signature verification in production needs the exact bytes
app.post('/webhooks/sent', express.raw({ type: 'application/json' }), (req, res) => {
const eventType = req.header('x-webhook-event-type');
const event = JSON.parse(req.body.toString('utf8'));
console.log(`--- ${eventType} ---`);
console.log(JSON.stringify(event, null, 2));
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook receiver listening on http://localhost:3000');
});Run it in its own terminal and leave it running:
npx tsx webhook-server.tsThe output should look something like:
Webhook receiver listening on http://localhost:3000We parse the raw body instead of using express.json() because production handlers must verify the X-Webhook-Signature header against the exact request bytes, as shown in webhook signature verification. We skip verification here to stay focused on the flow.
Open a tunnel with ngrok
Sent cannot reach localhost, and it rejects webhook URLs that resolve to private or loopback addresses. A tunnel gives your local server a public HTTPS URL.
In a second terminal, run:
ngrok http 3000The output should include a forwarding line like:
https://abc123.ngrok.io -> http://localhost:3000Copy your https:// forwarding URL. We register it with Sent in the next step. Keep this terminal running too.
Register the webhook
Now we tell Sent where to deliver events. In a third terminal, call the webhooks API with your forwarding URL:
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": "First integration tutorial",
"endpoint_url": "https://abc123.ngrok.io/webhooks/sent",
"event_types": ["message"]
}'The output should look something like:
{
"success": true,
"data": {
"id": "d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
"display_name": "First integration tutorial",
"endpoint_url": "https://abc123.ngrok.io/webhooks/sent",
"signing_secret": "whsec_a1b2c3d4e5f6g7h8i9j0",
"is_active": true,
"event_types": ["message"],
"retry_count": 3,
"timeout_seconds": 30
},
"error": null
}Copy the id from your response. We need it in the next step.
Notice that event_types takes parent categories: message covers every message event, from message.queued through message.delivered, message.failed, and inbound message.received (the webhook event types page has the full list). The signing_secret is what production code uses to verify the X-Webhook-Signature header; treat it like a password.
Deliver message.delivered and message.failed events
With the receiver, the tunnel, and the registration in place, we can ask Sent to deliver a test event. Replace the webhook ID in the URL with the id you copied:
curl -X POST https://api.sent.dm/v3/webhooks/d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8/test \
-H "x-api-key: $SENT_DM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event_type": "message.delivered"}'The curl output should look something like:
{
"success": true,
"data": {
"success": true,
"message": "Test event delivered successfully"
}
}Now look at the terminal running webhook-server.ts. The output should look something like:
--- message.delivered ---
{
"field": "message",
"event": "message.delivered",
"timestamp": "2026-07-25T09:30:00Z",
"payload": {
"updated_at": "2026-07-25T09:30:00Z",
"account_id": "00000000-0000-0000-0000-000000000000",
"message_id": "00000000-0000-0000-0000-000000000001",
"template_id": "00000000-0000-0000-0000-000000000002",
"template_name": "Test Template",
"outbound_number": "+0987654321",
"message_status": "DELIVERED",
"channel": "sms"
}
}Sent just made a round trip to your laptop. Run the same command again with {"event_type": "message.failed"} in the body. Your receiver prints a second event, identical in shape but with "message_status": "FAILED".
Notice that test events carry placeholder IDs (the zero-padded values above). Events for real sends carry the same message_id your send call returned; that ID is how your application matches a delivery report back to the message it sent. Sent attempts a test event exactly once, and retries real events up to the webhook's retry_count on failure.
Look up a message with the SDK
Finally, let's read a message back. Create check-status.ts:
import SentDm from '@sentdm/sentdm';
const client = new SentDm();
async function main() {
const messageId = process.argv[2];
try {
const status = await client.messages.retrieveStatus(messageId);
console.log('Status:', status.data.status);
console.log('Channel:', status.data.channel);
console.log('Direction:', status.data.direction);
} catch (err) {
if (err instanceof SentDm.APIError) {
console.log(`${err.constructor.name} (${err.status})`);
} else {
throw err;
}
}
}
main();Run it with the message_id you copied in the free-form text step:
npx tsx check-status.ts 3f2b8c41-6a0e-4c8f-9b7d-2a1e5c9d0f36The output should be:
NotFoundError (404)That 404 is expected, and it is the last lesson of this tutorial. Sandbox messages are simulated, never stored, so there is nothing to look up. The catch block you just wrote is the same APIError handling your production code needs for every SDK call.
For a live (non-sandbox) message, the identical script prints the real delivery state:
Status: DELIVERED
Channel: sms
Direction: OUTBOUNDRemember that webhooks, not lookups, are the recommended way to track delivery: store the message_id when you send, then update your records as events arrive instead of polling this endpoint.
What you have built
You have built a working end-to-end Sent integration: a project that sends free-form and template messages through the TypeScript SDK, a webhook receiver that accepts signed delivery events from Sent's servers on your own machine, and a status lookup with real error handling. You have also learned the ideas that carry into production (asynchronous sends, per-recipient message IDs, parent event types, and webhook-first delivery tracking) without spending a credit.
Next steps
Take the loop you built here to production:
- Swap
sandbox: truefor real sends once your account setup is complete; the code doesn't change - Work through the webhooks production checklist and add signature verification to your receiver
- Wire the client into your framework with the Express, NestJS, or Next.js integration guides
- Read the error handling reference and the sandbox mode reference for what you used today
TypeScript SDK
Official TypeScript SDK for Sent. Send SMS, WhatsApp, and RCS messages with full type safety and intelligent autocomplete.
Sending messages from Next.js with the Sent TypeScript SDK
Wire the Sent TypeScript SDK into a Next.js app: install, configure a server client, send from a route handler, verify webhooks, and test sandbox sends.