---
title: "Telnyx"
description: "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 verifies Telnyx's Ed25519-signed messaging webhooks. To compare providers, see [Choose a provider](/providers/overview).

## Support status

**Supported.** Requests, responses, the error catalog, and webhook signing follow Telnyx's official documentation and its official Node SDK, and the adapter passes the shared contract tests. Limits to know:

- No validity period. Telnyx's send request has no expiry field, so `validityPeriodSec` throws `UnsupportedFieldError`.
- Alphanumeric senders need `messagingProfileId` on the adapter.
- Webhooks are verified with a public key, not the API key. The URL is not signed.
- Webhook timestamps must be within 300 seconds of now by default.

## Setup

```package-install
npm install @opencoredev/sms-sdk
```

| Variable | Required | Used for |
| --- | --- | --- |
| `TELNYX_API_KEY` | Yes | API v2 key, sent as a Bearer token. Create it in Mission Control under API Keys. |
| `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 adapter reads no environment variables itself; the examples and the [doctor CLI](/reference/doctor-cli) use these names. Keep the API key on the server.

```ts
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",
      // Needed only for alphanumeric senders:
      // messagingProfileId: process.env.TELNYX_MESSAGING_PROFILE_ID,
    }),
  ],
});

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. Two more options exist for tests and proxies: `baseUrl` (default `https://api.telnyx.com`) and `fetch`. To test without a Telnyx account, see [Test without a provider](/testing/local-testing).

The adapter calls `POST /v2/messages` with a JSON body. 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.

US A2P traffic to mobile numbers needs 10DLC registration, toll-free numbers need verification, and a short code works in one country only. Alphanumeric senders can't reach the US, Canada, or Puerto Rico, and recipients can't reply to them. Some countries, such as the UK and France, require sender ID registration in advance. Telnyx's [sender type guide](https://developers.telnyx.com/docs/messaging/getting-started/choosing-your-sender-type) covers the rules. SMS SDK does not check them before sending; see [Registration and consent](/providers/registration-and-consent).

## Features

| Feature | Telnyx |
| --- | --- |
| MMS | Yes, sent with `type: "MMS"` |
| Scheduling (`sendAt`) | Yes, from any sender |
| Validity period | No |
| Per-message `webhookUrl` | Yes, sent as `webhook_url` |

## Provider options

Pass other `POST /v2/messages` fields in `providerOptions.telnyx`:

```ts
import { sms } from "./sms";

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 and wrong types throw before any request, and the options go out only when the `telnyx` adapter sends. `extra` cannot set the fields SMS SDK owns: `to`, `from`, `messaging_profile_id`, `text`, `media_urls`, `type`, `webhook_url`, and `send_at`. See [`providerOptions`](/reference/send#provideroptions) for how options interact with fallback and idempotency.

## Webhooks

```ts
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;
  }
}
```

- **Credential:** your account's Ed25519 public key. A key that is not 32 bytes of base64 fails with `invalid_credentials`.
- **Point Telnyx at it:** for status, pass `webhookUrl` per message or set the webhook URL on the messaging profile. For inbound, set the inbound webhook URL on the profile the number belongs to.
- **Signing:** `telnyx-signature-ed25519` covers `` `${telnyx-timestamp}|${rawBody}` ``. The URL is not signed, so proxies don't affect verification and there is no `publicUrl` option.
- **Replay window:** 300 seconds either side of now by default. Older or future timestamps fail with `stale_timestamp`. Change it with `toleranceSec`.

SMS SDK reads `message.received`, `message.sent`, and `message.finalized` events. Status comes from `data.payload.to[0].status`:

| Telnyx status | Event |
| --- | --- |
| `queued`, `sending` | `message.queued` |
| `sent`, `delivery_unconfirmed` | `message.sent` |
| `delivered` | `message.delivered` |
| `sending_failed`, `delivery_failed`, `expired` | `message.undelivered`, or `message.filtered` with error 40002, 40003, or 40322 |
| Other events and statuses, such as `read` | `unrecognized` |
| `message.received` | `message.received`, or a `recipient.*` event from STOP and HELP keywords (`source: "keyword"`) |

Telnyx sends no opt-out flag, so opt-outs come from keyword detection. `dedupeKey` is `telnyx:{data.id}`, the webhook event ID. `providerId` is the message ID, `data.payload.id`, which matches `result.providerId`.

## Limitations

- No `validityPeriodSec`. For an expiry, use Twilio, Plivo, Vonage, or Sinch.
- Error 40008 is not mapped to `message.filtered`, because Telnyx's pages disagree on its meaning. It stays `message.undelivered`.

## Error codes

The code comes from `errors[0].code` in the response.

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

Telnyx does not document a `Retry-After` header, but SMS SDK honors one on a rate limit. `error.provider.requestId` holds the `x-request-id` header. For adapter authors, the subpath also exports `buildTelnyxRequest`, `interpretTelnyxResponse`, `telnyxRejectionCategory`, and `telnyxDelivery`.

## Official documentation

Checked on 2026-10-08. The full evidence list is in [PROVIDERS.md](https://github.com/opencoredev/sms-sdk/blob/main/packages/sms-sdk/PROVIDERS.md).

- [Send a message](https://developers.telnyx.com/api/messaging/send-message)
- [Receiving messaging webhooks](https://developers.telnyx.com/docs/messaging/messages/receiving-webhooks)
- [Webhook delivery and signing](https://developers.telnyx.com/development/api-fundamentals/webhooks/receiving-webhooks)
- [API errors](https://developers.telnyx.com/development/api-fundamentals/api-errors)
- [Alphanumeric sender ID](https://developers.telnyx.com/docs/messaging/messages/alphanumeric-sender-id)
- [International SMS compliance](https://developers.telnyx.com/docs/messaging/messages/international-sms-compliance)
- [Official Node SDK webhook verification](https://github.com/team-telnyx/telnyx-node/blob/master/src/lib/webhooks.ts)

## Next steps

- [Switch providers](/migration/switch-providers)
- [Receive webhooks](/receiving/webhooks)
- [Fallback and retries](/sending/fallback-and-retries)
