---
title: "send()"
description: "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.

```ts
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](/sending/send-your-first-sms).

### 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 [Senders and E.164 numbers](/sending/senders-and-e164).

### `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.

### `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 [Idempotency](/sending/idempotency).

### `mediaUrls`

- Type: `readonly string[]`, absolute `http` or `https` URLs
- 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 `http` or `https` URL
- 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 `ShortenUrls` or Telnyx's `auto_detect`.

```ts
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.

```ts ignore="type listing"
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](/providers/twilio#provider-options), [Telnyx](/providers/telnyx#provider-options), [Plivo](/providers/plivo#provider-options), and [Vonage](/providers/vonage#provider-options).

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](/reference/adapter-contract#provideroptionsspec).

## 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.

```ts ignore="type listing"
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](/reference/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.
