TypeScript SDK

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 tsx

Now 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/sentdm

The output should look something like:

sent-first-integration@1.0.0
└── @sentdm/sentdm@0.32.0

Keep 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:

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.ts

The 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:

  • status is QUEUED: the API accepts sends asynchronously (HTTP 202) and reports delivery later, through webhooks.
  • channel is null because we did not pick one. Sent auto-detects the best channel per recipient at send time.
  • template_id is all zeros and template_name is empty because this was a free-form text send. Every request must carry exactly one of text or template. 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:

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.ts

The 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:

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.ts

The output should look something like:

Webhook receiver listening on http://localhost:3000

We 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 3000

The output should include a forwarding line like:

https://abc123.ngrok.io -> http://localhost:3000

Copy 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:

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-2a1e5c9d0f36

The 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: OUTBOUND

Remember 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:

On this page