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

How sending works

The three outcomes of sms.send(), the order a send runs in, and every default with the option that changes it.

Every send ends one of three ways, and SMS SDK treats each one differently. Learn these and the rest of the docs follow.

Three outcomes

Outcome What happened What send() does
Accepted The provider took the message and queued it. The phone may not have it yet. Returns a result with the provider’s message ID (handoff: "accepted").
Rejected The provider said no and did not create the message: bad credentials, an unregistered sender, an invalid number, an opted-out recipient. Throws ProviderRejectedError (or a subclass). Sending again cannot cause a duplicate, so retrySafe is true.
Unknown The request may have reached the provider, but the answer does not prove what happened: a timeout, a network error, a 5xx, a garbled success response. The provider may have sent it, so the SDK stops and tells you (HandoffUnknownError, retrySafe: false). It never retries or tries another provider.

There is no { ok: false } result: send() returns only when the message was accepted. Whether the phone received it is a separate question, answered later by a delivery webhook.

The order a send runs in

  1. Local checks. The recipient must be an E.164 number, the sender must suit the first adapter, and the adapter must support every field you set (media, scheduling, expiry, callback URL). A failure throws before any request, so nothing is silently dropped. See Message length and cost.
  2. Duplicate check. If the same idempotencyKey was already accepted, you get the stored result back with replayed: true instead of a second text. See Prevent duplicate sends.
  3. Your policy. A beforeSend callback can block the message, for example for a suppressed recipient. See Block and log sends.
  4. The request. The first adapter sends. A documented rate limit is retried on the same adapter. If you turned fallback on and the provider clearly rejected the message, the next adapter tries. See Fallback and retries.
  5. The result. The first accepted response resolves the call. Anything else throws.

Defaults

SMS SDK picks the safe option when you pass nothing, even when that means an error instead of an automatic retry.

Behavior Default Change it in
Fallback Off. With several adapters, only the first is used. Turn on with fallback: "on-known-rejection". Fallback and retries
Retries Rate limits only, at most 2 requests per adapter (retry.maxAttempts). Backoff starts at 500 ms and caps at 10 s (retry.baseDelayMs, retry.maxDelayMs). A Retry-After longer than the cap stops retrying. A 429 without the provider’s documented rate-limit error counts as unknown. Fallback and retries
Timeout 10 seconds per request (timeoutMs). A timeout after the request starts is an unknown outcome. createSmsClient
Duplicate protection Without a store, an idempotencyKey only joins identical calls running at the same time in one process. With idempotency.store, accepted results are kept 24 hours (ttlSec), unknown outcomes until you delete them, and a send stuck for 5 minutes (staleReservationSec) becomes unknown, never a resend. Prevent duplicate sends
Duplicate store memoryIdempotencyStore() protects one process. It is lost on restart and not shared across instances or serverless invocations. Multiple instances
Sender A message without from uses the adapter’s from. With neither, send() throws InvalidSenderError. Send a message
Segment count result.segments and validate() estimate GSM-7 and UCS-2 segments. The provider decides what it bills. Length and cost
Hooks Phone numbers are masked, bodies are never included, and a hook that throws is ignored. Block and log sends
Webhook verification On. An unsigned or altered request throws WebhookSignatureError. unsafeSkipVerification: true is for tests only. Webhook security
Proxy headers Ignored. Pass publicUrl, or trustProxy: true only if a proxy you control sets X-Forwarded-Proto and X-Forwarded-Host. Webhook security
Replay window Signed timestamps must be within 5 minutes (toleranceSec: 300) for Telnyx, Vonage, and Sinch HMAC. Twilio, Plivo, and Bandwidth sign no timestamp, so deduplicate with dedupeKey. Webhook security
Keyword detection On. An inbound message that is only STOP, UNSUBSCRIBE, HELP, or a similar keyword becomes recipient.opted_out or recipient.help. Turn off with detectKeywords: false. SMS SDK never replies itself. Opt-outs
Unknown webhook statuses A verified event the SDK does not map becomes { type: "unrecognized" } with the payload in raw. It does not throw. Webhook events

Next steps