Skip to content
SMS SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Send a message

Call sms.send(), read the result, handle errors by whether a retry is safe, choose a sender, and use MMS, scheduling, and provider options.

Send a text, read the result, and handle failures without texting anyone twice. No client yet? Start with the Quick start.

Send and read the result

import { db } from "./db";
import { sms } from "./sms";

const result = await sms.send({
  to: "+14155550123",
  body: "Your order #123 has shipped.",
  idempotencyKey: "order:123:shipped:v1",
});

await db.messages.create({
  id: result.id,
  provider: result.provider,
  providerId: result.providerId,
  to: "+14155550123",
  status: result.delivery,
});

send() resolves only when a provider accepts the message. The fields you will use most:

Field What it holds
id The SDK’s ID for this message. Stable across idempotent replays.
provider The adapter that accepted it.
providerId The provider’s message ID. Store it so status webhooks can find the message.
delivery Status from the send response, usually "queued". Delivery to the phone arrives later by webhook.

The result also has segment counts, every attempt made, and whether it was replayed. All fields are in the send() reference.

Handle errors

Every error extends SmsError and has a stable code and a retrySafe flag. retrySafe: true means the provider did not take the message, so sending again cannot create a duplicate. It does not promise the retry will work.

import {
  HandoffUnknownError,
  InvalidRecipientError,
  isSmsError,
  PolicyRejectedError,
  ProviderRejectedError,
} from "@opencoredev/sms-sdk";
import { db } from "./db";
import { sms } from "./sms";

export async function notifyShipped(orderId: string, to: `+${string}`): Promise<void> {
  const idempotencyKey = `order:${orderId}:shipped:v1`;
  try {
    await sms.send({ to, body: `Order ${orderId} has shipped.`, idempotencyKey });
  } catch (error) {
    if (error instanceof HandoffUnknownError) {
      // The provider may have the message. Do not resend automatically.
      await db.messages.markNeedsReview(idempotencyKey, error.reason);
      return;
    }
    if (error instanceof InvalidRecipientError || error instanceof PolicyRejectedError) {
      return; // Fix the data or respect the policy. A retry would fail the same way.
    }
    if (error instanceof ProviderRejectedError && error.category === "compliance") {
      await db.suppressions.add(to); // The recipient opted out with the provider.
      return;
    }
    if (isSmsError(error) && !error.retrySafe) {
      await db.messages.markNeedsReview(idempotencyKey, error.code);
      return;
    }
    throw error; // Retry safe: let your job queue try again later.
  }
}

Errors lists every error class, code, and what to do about it. To block sends yourself, for example to people who opted out, see Block and log sends.

Choose a sender

to must be E.164: a +, a country code, and up to 15 digits, with no spaces. +14155550123 is valid; (415) 555-0123 is not. The SDK checks this before any request. Narrow user input with isE164:

import { isE164 } from "@opencoredev/sms-sdk";
import { sms } from "./sms";

const input: string = "+14155550123"; // for example, from a form field

if (!isE164(input)) {
  throw new Error("Enter the number with its country code, such as +14155550123.");
}

await sms.send({ to: input, body: "Thanks for signing up." });

isE164 checks format only. To convert national formats, use a phone number library.

from is optional when the adapter has a default sender. It takes one of four shapes:

You send from Write
A long code or toll-free number "+15550100001"
A short code { shortCode: "12345" }
An alphanumeric sender ID { senderId: "Acme" }
A provider sender pool, such as a Twilio Messaging Service { messagingService: "MG…" }

Which providers support each shape is in Sender types, and the formats the SDK checks are in the send() reference.

Set a default with the adapter’s from option, such as twilio({ ..., from: { messagingService: "MG…" } }), and override it for one message:

import { sms } from "./sms";

await sms.send({ to: "+447700900123", from: { senderId: "Acme" }, body: "Your parcel is out for delivery." });

A sender the first adapter cannot use throws UnsupportedFieldError before any request. Whether your account may use a sender (ownership, 10DLC or toll-free registration, country rules) is checked by the provider; see Registration and consent.

More options

Field What it does Details
mediaUrls Sends MMS with images or files at public http/https URLs. mediaUrls
sendAt The provider holds the message and sends it at this Date. sendAt
validityPeriodSec How long the provider keeps trying before the message expires. validityPeriodSec
webhookUrl Sends this message’s status updates to a different URL. webhookUrl
import { sms } from "./sms";

await sms.send({
  to: "+14155550123",
  body: "Here is your boarding pass. It is valid for 10 minutes.",
  mediaUrls: ["https://example.com/passes/abc123.png"],
  validityPeriodSec: 600,
});

Each of these works only on some providers; see Capabilities, or call sms.capabilities() at runtime. If the adapter cannot send a field, the SDK throws UnsupportedFieldError before any request instead of dropping it.

Provider options

providerOptions passes provider parameters the shared fields do not cover, keyed by adapter name. An entry applies only when that adapter sends.

import { sms } from "./sms";

await sms.send({ to: "+14155550123", body: "Your order “123” shipped.", providerOptions: { twilio: { smartEncoded: true } } });

Here Twilio replaces the curly quotes with plain ones. Typed options, the extra escape hatch, and the validation rules are in the send() reference.

Cancel a send

Pass an AbortSignal to stop waiting. If no request had started, send() throws SendAbortedError and it is safe to send later. If the request was in flight, it throws HandoffUnknownError with reason: "aborted", because the provider may already have the message.

import { sms } from "./sms";

await sms.send({ to: "+14155550123", body: "Reminder" }, { signal: AbortSignal.timeout(2_000) });

Next steps