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

Telnyx

Send and receive SMS and MMS through the Telnyx Messaging API v2 with the SMS SDK telnyx() adapter: setup, senders, webhooks, errors, and limits.

The telnyx() adapter sends SMS and MMS through the Telnyx Messaging API v2 and parses Telnyx’s Ed25519-signed messaging webhooks.

To compare providers, see the Providers overview.

Support status

Supported. Request fields, responses, the error catalog, and Ed25519 webhook signing follow Telnyx’s official documentation and its official Node SDK source. The adapter passes the shared contract tests.

Installation

npm install @opencoredev/sms-sdk
pnpm add @opencoredev/sms-sdk
yarn add @opencoredev/sms-sdk
bun add @opencoredev/sms-sdk
nub add @opencoredev/sms-sdk
aube add @opencoredev/sms-sdk
import { telnyx } from "@opencoredev/sms-sdk/telnyx";

Environment variables

Variable Required Used for
TELNYX_API_KEY Yes API v2 key, sent as a Bearer token.
TELNYX_FROM Unless you send from a profile Default sender: a Telnyx number, short code, or alphanumeric sender ID.
TELNYX_MESSAGING_PROFILE_ID For alphanumeric senders Messaging profile ID sent with every message.
TELNYX_PUBLIC_KEY For webhooks Base64 Ed25519 public key from Mission Control, Keys & Credentials, Public Key.

The examples and the doctor CLI use these names. The adapter itself reads no environment variables. Keep the API key on the server.

Basic usage

import { createSmsClient } from "@opencoredev/sms-sdk";
import { telnyx } from "@opencoredev/sms-sdk/telnyx";

const sms = createSmsClient({
  adapters: [telnyx({ apiKey: process.env.TELNYX_API_KEY ?? "", from: "+15550100001" })],
});

const result = await sms.send({ to: "+14155550123", body: "Your order has shipped." });
console.log(result.providerId); // the Telnyx message ID (data.id)

telnyx() throws ConfigurationError at startup when apiKey is empty.

Authentication

Each request carries an API v2 key as Authorization: Bearer <apiKey>. Create keys in Mission Control under API Keys. Webhooks are verified with a separate public key, not the API key.

Configuration

Option Type Default Meaning
apiKey string required API v2 key.
from SmsFrom none Default sender. { messagingService: profileId } sends from the profile’s number pool.
messagingProfileId string none Sent as messaging_profile_id with every message. Required for alphanumeric senders.
baseUrl string https://api.telnyx.com API origin, for a proxy or a mock server.
fetch FetchLike global fetch Custom fetch.

API version and endpoint

  • POST https://api.telnyx.com/v2/messages
  • Authorization: Bearer <apiKey>, JSON body.
  • Fields sent: to, from or messaging_profile_id, text, media_urls with type: "MMS", webhook_url (from webhookUrl), send_at (from sendAt), and any provider options.
  • Success: a 2xx response with data.id. Delivery comes from data.to[0].status, usually queued. A 2xx without data.id is an unknown outcome.

Senders

Sender Write Sent as
Long code or toll-free number "+15550100001" from
Short code { shortCode: "12345" } from
Alphanumeric sender ID { senderId: "Acme" } from, plus messaging_profile_id from the adapter
Messaging profile number pool { messagingService: "<profile id>" } messaging_profile_id, with no from

An alphanumeric sender without messagingProfileId on the adapter fails local validation before any request. In Telnyx, assign each number to a messaging profile before it sends.

Regions and countries

Telnyx documents these rules in Choosing a sender type:

  • 10DLC brand and campaign registration is required for A2P messaging to US mobile numbers.
  • Toll-free numbers need verification, and they work for both US and Canada destinations.
  • Short codes need carrier approval and work in one country. A US short code does not reach Canada.
  • Alphanumeric senders can’t send to the US, Canada, or Puerto Rico. They are one-way, so recipients can’t reply.

Some countries, such as the UK and France, require alphanumeric sender IDs to be registered in advance. Telnyx’s international SMS compliance guide covers country rules.

SMS SDK does not check these rules before sending, and they have not been tested against live accounts. See Sender registration and consent.

Capabilities

Capability Telnyx adapter
MMS Yes
Scheduling Yes, any sender
Validity period No. Telnyx’s request has no such field.
Per-message webhookUrl Yes
Delivery status webhooks Yes
Inbound webhooks Yes
Native idempotency No

Provider options

Pass Telnyx’s other POST /v2/messages fields in providerOptions.telnyx:

import { createSmsClient } from "@opencoredev/sms-sdk";
import { telnyx } from "@opencoredev/sms-sdk/telnyx";

const sms = createSmsClient({
  adapters: [telnyx({ apiKey: process.env.TELNYX_API_KEY ?? "", from: "+15550100001" })],
});

await sms.send({
  to: "+14155550123",
  body: "Your order has shipped.",
  webhookUrl: "https://example.com/webhooks/sms/telnyx",
  providerOptions: {
    telnyx: { encoding: "gsm7", webhookFailoverUrl: "https://backup.example.com/webhooks/sms/telnyx" },
  },
});
Option Telnyx field Type
subject subject string, the subject of an MMS
webhookFailoverUrl webhook_failover_url http or https URL
useProfileWebhooks use_profile_webhooks boolean, Telnyx default true
autoDetect auto_detect boolean, Telnyx default false
encoding encoding "auto" | "gsm7" | "ucs2". With "gsm7", Telnyx answers 400 if a character has no GSM-7 form.
extra any other field Record<string, string | number | boolean>, sent as is

Unknown keys, wrong types, and extra keys that name a field SMS SDK sets throw before any request. The options go out only when the telnyx adapter sends. See providerOptions for fallback and idempotency.

SMS SDK sets to, from, messaging_profile_id, text, media_urls, type, webhook_url, and send_at itself, so extra cannot.

Example: send and track delivery

import { createSmsClient, isE164, isSmsError } from "@opencoredev/sms-sdk";
import { telnyx } from "@opencoredev/sms-sdk/telnyx";

const from = process.env.TELNYX_FROM;
const to = process.env.SMS_EXAMPLE_TO;
if (!isE164(from) || !isE164(to)) {
  throw new Error("Set TELNYX_FROM and SMS_EXAMPLE_TO to E.164 numbers.");
}

const sms = createSmsClient({
  adapters: [telnyx({ apiKey: process.env.TELNYX_API_KEY ?? "", from })],
});

try {
  const result = await sms.send({
    to,
    body: "Your order has shipped.",
    idempotencyKey: "order:123:shipped:v1",
    webhookUrl: "https://example.com/webhooks/sms/telnyx",
  });
  console.log(`Telnyx accepted ${result.providerId} (${result.delivery})`);
} catch (error) {
  if (isSmsError(error)) console.error(error.toJSON());
  throw error;
}

You see Telnyx accepted <uuid> (queued). Acceptance is not delivery. Telnyx posts message.sent and message.finalized events to the webhookUrl, covered in Delivery status webhooks.

Delivery and inbound webhooks

  • Status: pass webhookUrl per message, or set the webhook URL on the messaging profile.
  • Inbound: set the inbound webhook URL on the messaging profile the number belongs to.
import { parseSmsWebhook, WebhookSignatureError } from "@opencoredev/sms-sdk/webhooks";
import { db } from "./db";

export async function POST(request: Request): Promise<Response> {
  try {
    const event = await parseSmsWebhook({
      provider: "telnyx",
      request,
      credentials: { publicKey: process.env.TELNYX_PUBLIC_KEY ?? "" },
    });
    if (event.type === "message.delivered" || event.type === "message.undelivered") {
      await db.messages.updateStatus(event.providerId, event.type, event.errorCode);
    } else if (event.type === "recipient.opted_out") {
      await db.suppressions.add(event.from);
    }
    return new Response(null, { status: 200 });
  } catch (error) {
    if (error instanceof WebhookSignatureError) return new Response("Invalid signature", { status: 401 });
    throw error;
  }
}

SMS SDK maps Telnyx’s message.received, message.sent, and message.finalized events. Status comes from data.payload.to[0].status: queued and sending become message.queued; sent and delivery_unconfirmed become message.sent; delivered becomes message.delivered; sending_failed, delivery_failed, and expired become message.undelivered, or message.filtered with error 40002, 40003, or 40322. Other event types and statuses, such as read, become unrecognized.

dedupeKey is telnyx:{data.id}, the webhook event ID. providerId is the message ID, data.payload.id, which matches result.providerId.

Telnyx sends no opt-out flag on inbound messages, so STOP and HELP come from keyword detection, with source: "keyword".

Signature verification

Telnyx signs each webhook with two headers, telnyx-timestamp in Unix seconds and telnyx-signature-ed25519 in base64. The signature is Ed25519 over `${timestamp}|${rawBody}`. SMS SDK verifies it with your account’s public key through Web Crypto.

  • A timestamp more than 300 seconds before or after now fails with stale_timestamp. Change the window with toleranceSec.
  • The URL is not signed, so proxies do not affect verification and there is no publicUrl option.
  • A public key that is not 32 bytes of base64 fails with invalid_credentials.

Error mapping

Telnyx response Category Falls back
10009, 10010, 20001, 20002, 20003, 20006, 20008; 401 or 403 without a listed code auth Yes
10011, 40318 (queue full) rate_limited Yes, after retries
40305, 40306, 40308, 40315, 40320, 40321, 40329, 40330 sender Yes
20013, 20100, 40309, 40312, 40314, 40331, 40333 (spend limit) account Yes
40301, 40310, 40319 recipient No
40300 (blocked due to STOP), 40322 (blocked content) compliance Never
Any other 4xx request No
429 without code 10011 or 40318 unknown Never
5xx, network error, timeout, 2xx without data.id unknown Never

The code comes from errors[0].code in the response. Telnyx does not document a Retry-After header, but SMS SDK honors one if present.

Limitations

  • No validityPeriodSec. For an expiry, use Twilio, Plivo, or Vonage.
  • Alphanumeric senders need a messaging profile ID on the adapter.
  • Error 40008 is not mapped to message.filtered, because Telnyx’s pages disagree on its meaning. It stays message.undelivered.

Testing

Point telnyx() at a fake API with mockFetch:

import { createSmsClient } from "@opencoredev/sms-sdk";
import { telnyx } from "@opencoredev/sms-sdk/telnyx";
import { mockFetch } from "@opencoredev/sms-sdk/testing";

const fake = mockFetch({
  status: 200,
  body: { data: { id: "40385f64-5717-4562-b3fc-2c963f66afa6", to: [{ phone_number: "+14155550123", status: "queued" }] } },
});
const sms = createSmsClient({ adapters: [telnyx({ apiKey: "test", from: "+15550100001", fetch: fake.fetch })] });

const result = await sms.send({ to: "+14155550123", body: "Hi" });
console.log(result.providerId, JSON.parse(fake.calls[0]?.body ?? "{}")); // the id, and the JSON request body

Test your webhook handler with real signatures from generateTelnyxKeyPair() and signedTelnyxRequest(). See Local testing.

The adapter’s own tests live in the SMS SDK repository. From packages/sms-sdk:

bun test test/contracts/telnyx.test.ts test/webhooks/telnyx.test.ts

The live test sends one real, billable message to each configured provider. It is skipped unless you opt in:

LIVE_SMS_TESTS=true LIVE_SMS_TO=+14155550123 LIVE_SMS_ALLOWLIST=+14155550123 \
TELNYX_API_KEY=... TELNYX_FROM=+15550100001 \
bun test test/integration/live.test.ts

Check your configuration first with npx @opencoredev/sms-sdk doctor --adapter telnyx.

API reference

telnyx(options: TelnyxOptions): SmsAdapter

type TelnyxOptions = {
  apiKey: string;
  from?: SmsFrom;
  messagingProfileId?: string;
  baseUrl?: string;
  fetch?: FetchLike;
};

Also exported from @opencoredev/sms-sdk/telnyx: TELNYX_CAPABILITIES, TELNYX_PROVIDER_OPTIONS, buildTelnyxRequest(message, messagingProfileId), interpretTelnyxResponse(status, text, headers), telnyxRejectionCategory(status, code), telnyxDelivery(status), and the types TelnyxOptions, TelnyxMessageRequest, TelnyxSendOptions.

Official documentation

Checked on 2026-10-08. The full evidence list is in PROVIDERS.md.

Next steps