# Send SMS with Next.js

Source: https://jointextkit.com/docs/send-with-nextjs
Index: https://jointextkit.com/llms.txt

Send a text from a Next.js App Router route handler with the TextKit SDK. The key stays on the server.

## Prompt for your AI agent

Paste this into Claude Code, Cursor or any coding agent to have it do the
integration:

```text
Add SMS sending to this Next.js project with TextKit.

- Docs for this stack: https://jointextkit.com/docs/send-with-nextjs.md
- API: POST https://api.jointextkit.com/v1/messages with `Authorization: Bearer
  <key>` and a JSON body `{"to": "+1...", "body": "..."}`.
- Use the official SDK on the server only (route handler or server action): `npm
  install jointextkit`, then `new TextKit().messages.send({ to, body })`.
- Read the key from the `TEXTKIT_API_KEY` environment variable. Never hardcode
  it or send it to the browser.
- Phone numbers are E.164 and US or Canada only (`+1` followed by 10 digits).
- A test key (`ssms_sk_test_`) simulates: `+15005550006` always ends `delivered`
  and no phone gets a text.
- Errors are JSON: `{"error": {"code", "message", "param"}}` with a 4xx status.
  On `429`, wait `Retry-After` seconds and retry.
- Send only from server code that runs after a real event (a signup, an order).
  Never expose an endpoint that texts any number a visitor types.
```

## Before you start

- **An API key.** [Create a free account](/signup) and copy the test key
  (`ssms_sk_test_...`) from Home in the console. It works at once, no card.
- **Next.js 13.4 or later with the App Router, on the Node.js runtime.**
- Put the key in an environment variable:

```bash
export TEXTKIT_API_KEY=ssms_sk_test_...
```

## 1. Install the SDK

```bash
npm install jointextkit
```

Add the key to `.env.local` (and to your host's environment variables when you deploy):

```bash
TEXTKIT_API_KEY=ssms_sk_test_...
```

Never prefix it with `NEXT_PUBLIC_`: that would ship it to the browser.

## 2. Create a route handler

Create `app/api/send-sms/route.ts`:

```typescript
import { TextKit, TextKitError } from 'jointextkit';

export async function POST(request: Request) {
  const { to, body } = await request.json();
  const sms = new TextKit(); // reads TEXTKIT_API_KEY

  try {
    const message = await sms.messages.send({ to, body });
    return Response.json({ id: message.id, status: message.status });
  } catch (err) {
    if (err instanceof TextKitError) {
      return Response.json({ error: err.code, message: err.message }, { status: err.status });
    }
    throw err;
  }
}
```

The client is created inside the handler so `next build` never needs the key.

## 3. Try it

```bash
npm run dev

curl -X POST http://localhost:3000/api/send-sms \
  -H "Content-Type: application/json" \
  -d '{"to": "+15005550006", "body": "Hello from TextKit"}'
```

It returns `{"id": "msg_...", "status": "delivered"}`. A server action works the
same way: call `new TextKit().messages.send(...)` inside a `'use server'` function.

> This endpoint texts whatever number it receives. Put it behind your own
> sign-in before you deploy it, or call `send` from code that runs after a real
> event (a signup, an order). An open endpoint lets anyone text through your
> account.

## What the test key did

`+15005550006` is a sandbox number: the send comes back `delivered` and no
phone gets a text (the response says `"simulated": true`). Put your own
verified phone in `to` and the test key texts it for real. See
[Sandbox & test numbers](/docs/sandbox).

## Handle errors

The SDK throws a `TextKitError` with the API's `code`, `status` and
`message` (plus `param` for a bad field and `retryAfter` on a rate limit). It
retries network failures and `429`s for you, with an idempotency key, so a
retry never sends twice.

| Status | Code | What to do |
| --- | --- | --- |
| 400 | `invalid_request` | A field is missing or malformed; `param` names it. |
| 401 | `invalid_api_key` | The key is missing, revoked or mistyped. |
| 402 | `insufficient_credits` | Add credits; nothing was sent or charged. |
| 403 | `recipient_opted_out` | They texted STOP. Do not retry. |
| 429 | `rate_limited` | Wait `Retry-After` seconds, then retry. |

Every code is in [Errors](/docs/errors).

## Go live

1. **Add a number** in the [console](/console).
2. **Register your use case.** We file your 10DLC registration; toll-free
   numbers use a guided verification form. Until carriers approve it, the number
   texts only your verified numbers. See [Compliance & registration](/docs/compliance).
3. **Swap in a live key** (`ssms_sk_live_...`). The code stays the same.

## Next steps

- [Webhooks](/docs/webhooks): delivery receipts and replies, pushed to your server.
- [Verify](/docs/verify): one-time codes in two calls, billed only when verified.
- [Send an SMS](/docs/api/messages/send): every field the endpoint takes.
- [All quickstarts](/docs/quickstarts): the same guide for other languages and frameworks.
