createSmsClient
createSmsClient(options): every SmsClientOptions field, its default, and the SmsClient methods it returns.
npm install @opencoredev/sms-sdkpnpm add @opencoredev/sms-sdkyarn add @opencoredev/sms-sdkbun add @opencoredev/sms-sdknub add @opencoredev/sms-sdkaube add @opencoredev/sms-sdkcreateSmsClient(options)
Creates an SMS client over one or more adapters. It makes no network request.
import { createSmsClient, memoryIdempotencyStore } 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: "+15550100001" }),
telnyx({ apiKey: process.env.TELNYX_API_KEY ?? "", from: "+15550100002" }),
],
fallback: "on-known-rejection",
retry: { maxAttempts: 2, baseDelayMs: 500, maxDelayMs: 10_000 },
timeoutMs: 10_000,
idempotency: { store: memoryIdempotencyStore(), ttlSec: 86_400, staleReservationSec: 300 },
beforeSend: () => ({ kind: "allow" }),
hooks: { onFailure: (event) => console.error(event.errorCode) },
});
Throws ConfigurationError when adapters is empty, two adapters share a name, or timeoutMs is not a positive number.
Options
adapters
- Type:
readonly [SmsAdapter, ...SmsAdapter[]] - Required
- Adapters in priority order. The first is the primary: it must support every field of a message, or
send()throwsUnsupportedFieldError. Names must be unique. See Providers.
fallback
- Type:
"none" | "on-known-rejection" - Optional
- Defaults to
"none" - With
"none", only the primary adapter sends. With"on-known-rejection", a fallback-eligible rejection (auth,rate_limited,sender,account) moves to the next adapter that supports the message. An accepted or unknown outcome never falls back. See Retries and fallback.
retry
- Type:
{ maxAttempts?: number; baseDelayMs?: number; maxDelayMs?: number } - Optional
- Defaults to
{ maxAttempts: 2, baseDelayMs: 500, maxDelayMs: 10000 } - Retries on the same adapter, only after a
rate_limitedrejection.maxAttemptscounts the first request, so1turns retries off. Backoff is exponential with full jitter. A providerRetry-Afterof at mostmaxDelayMsis used as the delay. A longer one stops retrying that adapter.
timeoutMs
- Type:
number - Optional
- Defaults to
10000 - Timeout for each provider request. A timeout after the request starts throws
HandoffUnknownErrorwithreason: "timeout".
idempotency
- Type:
{ store: IdempotencyStore; ttlSec?: number; staleReservationSec?: number } - Optional
- Off by default
- Enables
idempotencyKeydeduplication throughstore.ttlSec(default86400) is how long accepted and rejected records are kept. AfterstaleReservationSec(default300), an unfinished reservation counts as an unknown outcome. See Idempotency and Idempotency store.
beforeSend
- Type:
(context: BeforeSendContext) => PolicyDecision | Promise<PolicyDecision> - Optional
- Runs once per logical send, after local validation and the idempotency check, before any request. Return
{ kind: "allow" }or{ kind: "reject", reason }. A rejection throwsPolicyRejectedErrorand never falls back. If it throws, the error propagates and nothing is sent. See Policy checks and hooks.
hooks
- Type:
{ onAttempt?, onAccepted?, onFailure? } - Optional
- Redacted observability callbacks. Errors thrown by hooks are swallowed. See Policy checks and hooks.
Returns: SmsClient
| Method | Returns | Reference |
|---|---|---|
send(input, options?) |
Promise<SmsSendResult> |
send() |
validate(input) |
SmsValidationResult |
validate() |
capabilities() |
readonly SmsAdapterInfo[] |
below |
sms.capabilities()
Returns each configured adapter’s metadata, in order:
import { sms } from "./sms";
for (const adapter of sms.capabilities()) {
console.log(adapter.name, adapter.support.status, adapter.capabilities.mms, adapter.defaultFrom);
}
Each entry is { name, capabilities: SmsCapabilities, support: AdapterSupport, defaultFrom: SmsFrom | undefined }. See Adapter contract for the field types.
Types
type SmsClientOptions = {
readonly adapters: readonly [SmsAdapter, ...SmsAdapter[]];
readonly fallback?: "none" | "on-known-rejection";
readonly retry?: RetryOptions;
readonly timeoutMs?: number;
readonly idempotency?: { readonly store: IdempotencyStore; readonly ttlSec?: number; readonly staleReservationSec?: number };
readonly beforeSend?: (context: BeforeSendContext) => PolicyDecision | Promise<PolicyDecision>;
readonly hooks?: SmsClientHooks;
};
type RetryOptions = { readonly maxAttempts?: number; readonly baseDelayMs?: number; readonly maxDelayMs?: number };
type BeforeSendContext = { readonly id: string; readonly message: SmsSendInput; readonly encoding: "gsm7" | "ucs2"; readonly segments: number };
type PolicyDecision = { readonly kind: "allow" } | { readonly kind: "reject"; readonly reason: string };
type SmsClientHooks = {
readonly onAttempt?: (event: SmsAttemptEvent) => void | Promise<void>;
readonly onAccepted?: (event: SmsAcceptedEvent) => void | Promise<void>;
readonly onFailure?: (event: SmsFailureEvent) => void | Promise<void>;
};
type SmsHookEventBase = { readonly id: string; readonly to: string; readonly encoding: SmsEncoding; readonly segments: number; readonly idempotencyKey?: string };
type SmsAttemptEvent = SmsHookEventBase & { readonly attempt: SendAttempt };
type SmsAcceptedEvent = SmsHookEventBase & { readonly provider: string; readonly providerId: string; readonly attempts: readonly SendAttempt[] };
type SmsFailureEvent = SmsHookEventBase & { readonly errorCode: string; readonly retrySafe: boolean; readonly attempts: readonly SendAttempt[] };
type SmsAdapterInfo = { readonly name: string; readonly capabilities: SmsCapabilities; readonly support: AdapterSupport; readonly defaultFrom: SmsFrom | undefined };
All of these are exported as types from @opencoredev/sms-sdk.