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
- 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.
- Duplicate check. If the same
idempotencyKeywas already accepted, you get the stored result back withreplayed: trueinstead of a second text. See Prevent duplicate sends. - Your policy. A
beforeSendcallback can block the message, for example for a suppressed recipient. See Block and log sends. - 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.
- 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 |