send()
sms.send(input, options): SmsSendInput and SmsSendResult fields, SendAttempt, the order of checks, and the errors it throws.
sms.send(input, options?)
Sends one logical message. It resolves only when a provider accepts the message (handoff: "accepted"). Every rejection and unknown outcome throws.
import { sms } from "./sms";
const result = await sms.send(
{
to: "+14155550123",
body: "Your order has shipped.",
idempotencyKey: "order:123:shipped:v1",
},
{ signal: AbortSignal.timeout(30_000) },
);
console.log(result.providerId, result.delivery, result.segments);
Guide with error handling: Send a message.
Parameters
input: SmsSendInput, the message. Fields below.options?: { signal?: AbortSignal }. Aborting before any request, or during a rate-limit backoff, throwsSendAbortedError. Aborting while a request is in flight throwsHandoffUnknownErrorwithreason: "aborted".
SmsSendInput
to
- Type:
`+${string}`(E164) - Required
- Recipient. Checked at runtime against
^\+[1-9]\d{1,14}$. See Send a message.
from
- Type:
SmsFrom(E164 | { senderId } | { shortCode } | { messagingService }) - Optional
- Defaults to the adapter’s configured
from. With fallback, each adapter uses its own default whenfromis omitted.
| Shape | Checked locally |
|---|---|
| E164 string, a long code or toll-free number | E.164 |
{ shortCode } |
3 to 8 digits |
{ senderId }, an alphanumeric sender ID |
1 to 11 letters, digits, or spaces, with at least one letter |
{ messagingService }, a provider sender pool |
Not empty. Twilio also checks MG plus 32 hex characters. |
messagingService is a Twilio Messaging Service SID, a Telnyx messaging profile ID, or a Plivo Powerpack UUID. Which providers support each shape is in Sender types. A sender the primary adapter cannot use throws UnsupportedFieldError (field: "from") before any request; with fallback on, later adapters that cannot use it are skipped.
The SDK cannot see whether a number belongs to your account, is registered for 10DLC or toll-free, or whether the country allows alphanumeric sender IDs (the United States and Canada do not). The provider rejects those with category: "sender". A sender belongs to one provider account, so a second provider needs its own. See Registration and consent.
body
- Type:
string - Required
- Message text. May be empty only when
mediaUrlsis non-empty, and never for Vonage. Twilio limits it to 1600 characters.
idempotencyKey
- Type:
string, 1 to 255 characters - Optional
- Names the logical message, such as
order:123:shipped:v1. Deduplicates only when the client hasidempotency.store. Without a store, only concurrent calls with the same key on the same client share one send. See Prevent duplicate sends.
mediaUrls
- Type:
readonly string[], absolutehttporhttpsURLs - Optional
- MMS media. Needs
capabilities.mms: Twilio (up to 10 URLs), Telnyx, and Plivo. Each URL must be reachable by the provider, andbodymay be empty. Many countries outside the US and Canada do not support MMS, and the provider rejects it there.
sendAt
- Type:
Date - Optional
- Provider-side scheduled send. Needs
capabilities.scheduling. The provider holds the message and sends it atsendAt;send()resolves as soon as the provider accepts it. Telnyx and Sinch schedule from any sender they support. Twilio schedules only from a Messaging Service sender and enforces its own lead-time limits. To cancel a scheduled message, use the provider’s API or console.
validityPeriodSec
- Type: positive integer
- Optional
- How long the provider may keep trying before the message expires. Needs
capabilities.validityPeriod. Ranges: Twilio 1 to 36000, Plivo 5 to 10799, Vonage 20 to 604800, Sinch 1 to 2592000. Telnyx and Bandwidth do not support it. Out of range throwsInvalidMessageError.
webhookUrl
- Type: absolute
httporhttpsURL - Optional
- Per-message status callback, overriding the URL on your number or account. Needs
capabilities.webhookUrlOverride: Twilio, Telnyx, Plivo, and Sinch; Vonage only with JWT auth (applicationIdandprivateKey); never Bandwidth. See Where to point webhooks.
providerOptions
- Type:
SmsProviderOptions, an object keyed by adapter name - Optional
- Provider-specific parameters the portable fields do not cover, such as Twilio’s
ShortenUrlsor Telnyx’sauto_detect.
import { createSmsClient } from "@opencoredev/sms-sdk";
import { telnyx } from "@opencoredev/sms-sdk/telnyx";
import { twilio } from "@opencoredev/sms-sdk/twilio";
const sms = createSmsClient({
adapters: [
twilio({
accountSid: process.env.TWILIO_ACCOUNT_SID ?? "",
authToken: process.env.TWILIO_AUTH_TOKEN ?? "",
from: { messagingService: process.env.TWILIO_MESSAGING_SERVICE_SID ?? "" },
}),
telnyx({ apiKey: process.env.TELNYX_API_KEY ?? "", from: "+15550100001" }),
],
fallback: "on-known-rejection",
});
await sms.send({
to: "+14155550123",
body: "Track your order: https://example.com/orders/123",
providerOptions: {
twilio: { shortenUrls: true, smartEncoded: false },
telnyx: { autoDetect: true },
},
});
Each entry has typed options in camelCase, which the adapter sends under the provider’s own parameter names. An optional extra object holds parameters the typed options do not cover yet.
interface SmsProviderOptions {
twilio?: TwilioSendOptions; // added by "@opencoredev/sms-sdk/twilio"
telnyx?: TelnyxSendOptions; // added by "@opencoredev/sms-sdk/telnyx"
plivo?: PlivoSendOptions; // added by "@opencoredev/sms-sdk/plivo"
vonage?: VonageSendOptions; // added by "@opencoredev/sms-sdk/vonage"
}
type TwilioSendOptions = {
shortenUrls?: boolean;
smartEncoded?: boolean;
// ...one optional field per typed option
extra?: Record<string, string | number | boolean>;
};
Each provider page lists its options and wire names: Twilio, Telnyx, Plivo, and Vonage.
Rules:
- Typed by import. Each adapter subpath adds its key to
SmsProviderOptions, so you can only write keys for adapters you import. An unknown option key is a type error. - One adapter each. An entry applies only when that adapter sends. With fallback, each adapter sends its own entry and never another’s. Entries for adapters the client does not have are ignored.
sms.validate()lists the candidates that have an entry inproviderOptionsFor. - Checked before sending. For every configured adapter, before any request: an unknown option key throws
UnsupportedFieldError, a wrong type or value throwsInvalidMessageError, and anextrakey that names a parameter SMS SDK sets (such as Twilio’sToorStatusCallback, in any letter case) or a typed option’s parameter throwsUnsupportedFieldError. Each error hasfield: "providerOptions". extrais not validated. Its values must be strings, finite numbers, or booleans, and are sent as they are. Use it for parameters released after this SDK version, and check the provider’s reference first.- Part of the idempotency key. Reusing an
idempotencyKeywith different options throwsIdempotencyConflictError. Key order does not matter, and empty entries or entries for adapters the client does not have are ignored. - Values stay out of logs. Error messages name option keys, never their values. Hook events and
SendAttemptcarry no options.beforeSendreceives the whole input, options included, as it doesbody.
An adapter you write can accept options too. Set providerOptions on the adapter to a ProviderOptionsSpec and add its key to SmsProviderOptions with declare module "@opencoredev/sms-sdk". See Adapter contract.
Returns: SmsSendResult
| Field | Type | Meaning |
|---|---|---|
id |
string |
sms_ plus 32 hex characters. The SDK’s ID for this logical send. Stable across idempotent replays. |
provider |
string |
Name of the adapter that accepted. |
providerId |
string |
The provider’s message ID: Twilio SM…/MM…, Telnyx data.id, Plivo message_uuid[0], Vonage message_uuid, Bandwidth id, Sinch batch id. Status webhooks carry it as event.providerId. |
handoff |
"accepted" |
Always "accepted": the provider has the message, not yet the phone. |
delivery |
Delivery |
Status from the response, usually "queued". Not handset delivery. |
encoding |
"gsm7" | "ucs2" |
Encoding of the estimate. |
segments |
number |
Estimated segments. Not a billing figure. See Message length and cost. |
attemptedProviders |
readonly string[] |
Adapters tried, in order, without duplicates. |
attempts |
readonly SendAttempt[] |
Every provider request, in order. |
replayed |
boolean |
true when returned from the idempotency store or a concurrent in-flight call with the same key. |
Delivery is "unknown" | "queued" | "sent" | "delivered" | "undelivered" | "filtered". Handoff is "accepted" | "rejected" | "unknown".
SendAttempt
Safe to log. It has no body and no phone numbers.
type SendAttempt =
| { provider: string; attempt: number; outcome: "accepted"; providerId: string; delivery: Delivery; durationMs: number }
| { provider: string; attempt: number; outcome: "rejected"; category: RejectionCategory; httpStatus?: number; providerCode?: string; retryAfterMs?: number; durationMs: number }
| { provider: string; attempt: number; outcome: "unknown"; reason: UnknownReason; httpStatus?: number; durationMs: number };
attempt counts from 1 per adapter within one send.
Order of checks
- Local validation: recipient, body, field formats,
providerOptionsfor every configured adapter, then the primary adapter’s sender and capabilities. Failures throw before any request. - Abort check: an already-aborted
signalthrowsSendAbortedError. - Idempotency: replay, conflict, in-progress, or unknown, when a key and store are set.
beforeSend.- Provider requests, with rate-limit retries and, if enabled, fallback.
Errors
| Error | code |
retrySafe |
When |
|---|---|---|---|
InvalidRecipientError |
invalid_recipient |
true | to is not E.164 |
InvalidSenderError |
invalid_sender |
true | No sender, or a malformed one |
InvalidMessageError |
invalid_message |
true | Empty body without media, bad URL, date, or number field, a provider limit, a providerOptions value of the wrong type |
UnsupportedFieldError |
unsupported_field |
true | The primary adapter cannot send a field or sender type, or a providerOptions key is unknown or names a parameter SMS SDK sets |
PolicyRejectedError |
policy_rejected |
true | beforeSend rejected |
ProviderRejectedError |
provider_rejected |
true | The provider proved non-acceptance |
ProviderAuthError |
provider_auth |
true | Category auth |
ProviderRateLimitedError |
provider_rate_limited |
true | Rate limited after retries |
AllProvidersRejectedError |
all_providers_rejected |
true | Fallback ran and every attempted adapter rejected |
HandoffUnknownError |
handoff_unknown |
false | Acceptance cannot be determined |
SendAbortedError |
aborted |
true | Aborted before any request could have been processed |
IdempotencyConflictError |
idempotency_conflict |
false | Key reused with a different payload or sender |
IdempotencyInProgressError |
idempotency_in_progress |
true | Another process holds a fresh reservation |
Errors has details and the action for each. An error thrown by your beforeSend propagates unchanged.
Types
SmsSendInput, SmsSendOptions, SmsSendResult, SmsProviderOptions, ProviderOptionsSpec, ProviderOptionsOf, ProviderExtra, SmsFrom, Delivery, Handoff, SendAttempt, RejectionCategory, and UnknownReason are exported as types from @opencoredev/sms-sdk. E164 is exported from the same path.