Send SMS with Supabase Edge Functions

Send a text from a Supabase Edge Function with the TextKit SDK, imported from npm.

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 Supabase Edge Functions project with TextKit.

- Docs for this stack: https://jointextkit.com/docs/send-with-supabase.md
- API: POST https://api.jointextkit.com/v1/messages with `Authorization: Bearer
  <key>` and a JSON body `{"to": "+1...", "body": "..."}`.
- Use the official SDK: `import { TextKit } from 'npm:jointextkit'`, key from
  `Deno.env.get('TEXTKIT_API_KEY')` (a function secret), then `.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 and copy the test key (ssms_sk_test_...) from Home in the console. It works at once, no card.
  • The Supabase CLI and a linked project.
  • Put the key in an environment variable:
bash
export TEXTKIT_API_KEY=ssms_sk_test_...

1. Create the function and add the key

bash
supabase functions new send-sms
supabase secrets set TEXTKIT_API_KEY=ssms_sk_test_...

2. Send an SMS

Replace supabase/functions/send-sms/index.ts:

typescript
import { TextKit, TextKitError } from 'npm:jointextkit';

Deno.serve(async (req) => {
  const { to, body } = await req.json();
  const sms = new TextKit(Deno.env.get('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;
  }
});

3. Deploy it

bash
supabase functions deploy send-sms

Edge Functions check the caller's Supabase JWT by default, so only signed-in users of your app can call it. Call it from your app with supabase.functions.invoke('send-sms', { body: { to, body } }).

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.

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 429s for you, with an idempotency key, so a retry never sends twice.

StatusCodeWhat to do
400invalid_requestA field is missing or malformed; param names it.
401invalid_api_keyThe key is missing, revoked or mistyped.
402insufficient_creditsAdd credits; nothing was sent or charged.
403recipient_opted_outThey texted STOP. Do not retry.
429rate_limitedWait Retry-After seconds, then retry.

Every code is in Errors.

Go live

  1. Add a number in the 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.
  3. Swap in a live key (ssms_sk_live_...). The code stays the same.

Next steps

  • Webhooks: delivery receipts and replies, pushed to your server.
  • Verify: one-time codes in two calls, billed only when verified.
  • Send an SMS: every field the endpoint takes.
  • All quickstarts: the same guide for other languages and frameworks.