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

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, throws SendAbortedError. Aborting while a request is in flight throws HandoffUnknownError with reason: "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 when from is 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 mediaUrls is 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 has idempotency.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[], absolute http or https URLs
  • Optional
  • MMS media. Needs capabilities.mms: Twilio (up to 10 URLs), Telnyx, and Plivo. Each URL must be reachable by the provider, and body may 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 at sendAt; 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 throws InvalidMessageError.

webhookUrl

  • Type: absolute http or https URL
  • 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 (applicationId and privateKey); 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 ShortenUrls or Telnyx’s auto_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 in providerOptionsFor.
  • Checked before sending. For every configured adapter, before any request: an unknown option key throws UnsupportedFieldError, a wrong type or value throws InvalidMessageError, and an extra key that names a parameter SMS SDK sets (such as Twilio’s To or StatusCallback, in any letter case) or a typed option’s parameter throws UnsupportedFieldError. Each error has field: "providerOptions".
  • extra is 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 idempotencyKey with different options throws IdempotencyConflictError. 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 SendAttempt carry no options. beforeSend receives the whole input, options included, as it does body.

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

  1. Local validation: recipient, body, field formats, providerOptions for every configured adapter, then the primary adapter’s sender and capabilities. Failures throw before any request.
  2. Abort check: an already-aborted signal throws SendAbortedError.
  3. Idempotency: replay, conflict, in-progress, or unknown, when a key and store are set.
  4. beforeSend.
  5. 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.