Sending SMS

Numbers, segments, statuses, scheduling, failures and suppressions.

const { : ,  } = await ..(
  {
    : "+14155550123",
    : "+14155550199",
    : "Your Acme order #1042 has shipped.",
    : [{ : "order_id", : "1042" }],
  },
  { : "order-1042-shipped" },
);
if () .(., ., ., ., .);

Sends return HTTP 202 with status: "queued", and a background worker hands the message to the carrier. Retrying with the same idempotency key returns the first message instead of sending a second one (the SDK sets a key on every send).

Numbers

from and to are phone numbers in E.164 form (+14155550199). With a live key, from must be one of your organization's numbers whose carrier registration is approved (numbers and registration), and to must be in a country your organization allows (countries). Toll-free numbers only send to the US and Canada. Sandbox keys accept any from and simulate every message (testing).

Segments

Carriers deliver SMS in segments, and each segment is billed. A message made only of GSM-7 characters (Latin letters, digits and common punctuation) is sent as GSM-7; anything else, an emoji or a curly quote included, makes the whole message UCS-2 (Unicode):

Longer messages from a toll-free number take one character less per segment (152 and 66), as carriers split them; the 10-segment limit is counted that way for every sender.

GSM-7's extended characters (€ [ ] { } \ ^ ~ | and the form feed) take two characters' room. Characters are never split across segments. A body of more than 10 segments is refused with a 422 validation_error, as is one with control characters other than line breaks. The message's segments and encoding say how it was counted; coss.com and the carrier count the same way.

Statuses

Not every carrier reports delivery: a message can stay sent. Each message's events are listed by coss.sms.listEvents() and sent to your webhooks; their payloads never include the text.

const { :  } = await ..();
if (?. === "undelivered" || ?. === "failed") {
  .(.?., .?.);
}

const { :  } = await ..();
for (const  of ?. ?? []) .(., .);

// The last day's failed messages.
const { :  } = await ..({
  : ["failed", "undelivered"],
  : new (.() - 24 * 60 * 60 * 1000).(),
});

Failures

failure.code says why a message failed or wasn't delivered:

When the carrier gives its own error code, the event's data.provider_error_code has it (for example 30007).

Scheduling and cancellation

Pass scheduled_at as an ISO 8601 timestamp with a time zone offset, up to 30 days ahead. The message is scheduled (with an sms.scheduled event) until then; a time that isn't in the future sends it right away. At the scheduled time the sender, the country and the suppression list are checked again, so a number released or suppressed in the meantime fails the message instead. Scheduled messages wait while your organization is suspended or on a billing hold.

const { :  } = await ..({
  : "+14155550123",
  : "+14155550199",
  : "Reminder: your Acme appointment is tomorrow at 10:00.",
  : "2026-11-11T09:00:00Z",
});
if () await ..(.);

Canceling makes it canceled (sms.canceled); canceling it again returns the same message. Any other message can't be canceled (422 sms_not_cancelable).

Suppressions

Each environment has its own SMS suppression list, separate from email's. A send to a number on it fails at once with failure.code suppressed and an sms.suppressed event: nothing is sent or billed. Entries have a reason:

  • opt_out: the number replied STOP (or a synonym) to one of your numbers. Opt-outs are added to every live environment of the organization, since your numbers are shared, and START removes them (receiving). A send the carrier refuses because of an opt-out adds one too.
  • invalid_number: a send failed because the number can't receive SMS.
  • manual: added by you.
await ...({ : "+14155550199" });

const { :  } = await ...({ : "+14155550199" });
if (?. && .?. === "manual") {
  await ...(..);
}

Remove an opt_out entry only when the person asked for your messages again (they can also reply START). Suppression entries are kept until you remove them.

Limits

  • Your plan's SMS rate: segments a second and a day per organization (see the plans). Over it, sends get 429 quota_exceeded with Retry-After.
  • 10 messages an hour to the same number per organization, for live keys: a form that loops one-time codes to one number, or traffic pumped through it, stops there (429 quota_exceeded).
  • Sandbox environments: 200 segments a day each.

Content screening

A sample of live messages is screened for spam, phishing, malware and fraud before it is sent, like email. A message judged highly likely to be abusive fails with content_rejected and live sending is suspended pending review (see the Acceptable Use Policy).

Billing

A live message is billed per segment, at its destination country's rate, once it reaches a carrier (sent, delivered or undelivered). Failed, suppressed and canceled messages cost nothing, and sandbox messages are never billed. See countries and pricing.