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 your first SMS.
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 Senders and E.164 numbers.
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.
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 Idempotency.
mediaUrls
- Type:
readonly string[], absolutehttporhttpsURLs - Optional
- MMS media. Needs
capabilities.mms.
sendAt
- Type:
Date - Optional
- Provider-side scheduled send. Needs
capabilities.scheduling. Twilio also needs a Messaging Service sender.
validityPeriodSec
- Type: positive integer
- Optional
- How long the provider may keep trying. Needs
capabilities.validityPeriod, and must be within its range.
webhookUrl
- Type: absolute
httporhttpsURL - Optional
- Per-message status callback. Needs
capabilities.webhookUrlOverride.
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. |
handoff |
"accepted" |
Always "accepted". |
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. |
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.