# Telnyx Messaging: SMS & MMS — Full Documentation > Complete page content for SMS & MMS (Messaging section) of the Telnyx developer docs (https://developers.telnyx.com). > Root index: https://developers.telnyx.com/llms.txt · Lightweight index for this subsection: https://telnyx-openapi-ng.s3.us-east-1.amazonaws.com/llms/messaging/sms-mms.txt ## Sending Messages ### Send Your First Message > Source: https://developers.telnyx.com/docs/messaging/messages/send-message.md Send your first SMS using the Telnyx Messaging API. This guide takes you from zero to sending a message in about 5 minutes by testing between two Telnyx numbers—no carrier registration required. ## Prerequisites - A [Telnyx account](https://telnyx.com/sign-up) (free to create) ## 1. Get two phone numbers Purchase two Telnyx numbers so you can test messaging between them without registration requirements. Navigate to [Numbers > Search & Buy](https://portal.telnyx.com/#/app/numbers/search-numbers) in the portal. Enter your preferred area code or region, check **SMS** under features, and click **Search**. Click **Add to Cart** on two numbers, then **Place Order**. Having two numbers lets you test on-net (Telnyx-to-Telnyx) messaging immediately, and also test receiving inbound messages. ## 2. Create a Messaging Profile Navigate to [Messaging](https://portal.telnyx.com/#/app/messaging) in the portal. Click **Add new profile**, give it a name (e.g., "My App"), and click **Save**. Go to [My Numbers](https://portal.telnyx.com/#/app/numbers/my-numbers), and for each number, click the **Messaging Profile** dropdown, select your profile, and save. ## 3. Get your API key Go to [API Keys](https://portal.telnyx.com/#/app/api-keys) and copy your API key (or create one if needed). ## 4. Send a message ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "from": "+15551234567", "to": "+15559876543", "text": "Hello, world!" }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.send({ from: '+15551234567', to: '+15559876543', text: 'Hello, world!' }); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.send( from_="+15551234567", to="+15559876543", text="Hello, world!" ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_( from: "+15551234567", to: "+15559876543", text: "Hello, world!" ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "+15551234567", To: "+15559876543", Text: "Hello, world!", }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import com.telnyx.sdk.models.messages.MessageSendResponse; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessageSendParams params = MessageSendParams.builder() .from("+15551234567") .to("+15559876543") .text("Hello, world!") .build(); MessageSendResponse response = client.messages().send(params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = "+15551234567", To = "+15559876543", Text = "Hello, world!" }); Console.WriteLine(response.Data); ``` ```php PHP '+15551234567', 'to' => '+15559876543', 'text' => 'Hello, world!' ]); print_r($response); ``` MMS messages support media attachments. Your number must be MMS-enabled. ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "from": "+15551234567", "to": "+15559876543", "text": "Check out this image!", "subject": "Picture", "media_urls": ["https://example.com/image.jpg"] }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.send({ from: '+15551234567', to: '+15559876543', text: 'Check out this image!', subject: 'Picture', media_urls: ['https://example.com/image.jpg'] }); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.send( from_="+15551234567", to="+15559876543", text="Check out this image!", subject="Picture", media_urls=["https://example.com/image.jpg"] ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_( from: "+15551234567", to: "+15559876543", text: "Check out this image!", subject: "Picture", media_urls: ["https://example.com/image.jpg"] ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "+15551234567", To: "+15559876543", Text: "Check out this image!", Subject: "Picture", MediaURLs: []string{"https://example.com/image.jpg"}, }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import com.telnyx.sdk.models.messages.MessageSendResponse; import java.util.List; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessageSendParams params = MessageSendParams.builder() .from("+15551234567") .to("+15559876543") .text("Check out this image!") .subject("Picture") .mediaUrls(List.of("https://example.com/image.jpg")) .build(); MessageSendResponse response = client.messages().send(params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = "+15551234567", To = "+15559876543", Text = "Check out this image!", Subject = "Picture", MediaUrls = new[] { "https://example.com/image.jpg" } }); Console.WriteLine(response.Data); ``` ```php PHP '+15551234567', 'to' => '+15559876543', 'text' => 'Check out this image!', 'subject' => 'Picture', 'media_urls' => ['https://example.com/image.jpg'] ]); print_r($response); ``` Media URLs must be publicly accessible. Replace the placeholder values: - `YOUR_API_KEY`: Your API key from step 3 - `from`: Your first Telnyx number (the sender) - `to`: Your second Telnyx number (the recipient) **E.164 format is required.** Always include the `+` prefix, country code, and full number with no spaces or punctuation. | Country | Format | Example | |---------|--------|---------| | US/Canada | +1 + 10 digits | `+15551234567` | | UK | +44 + 10-11 digits (drop leading 0) | `+447911123456` | | Germany | +49 + 10-11 digits (drop leading 0) | `+4915123456789` | | Australia | +61 + 9 digits (drop leading 0) | `+61412345678` | | Brazil | +55 + 10-11 digits | `+5511987654321` | | India | +91 + 10 digits | `+919876543210` | **Common mistakes:** - ❌ `15551234567` (missing `+`) - ❌ `+1 (555) 123-4567` (contains spaces and punctuation) - ❌ `+1-555-123-4567` (contains dashes) - ✅ `+15551234567` **Sending to non-Telnyx numbers?** Off-net messaging to external carriers typically requires sender registration (10DLC, toll-free verification, etc.). See [Next steps](#next-steps) for registration guides. ### Response A successful response looks like this: ```json { "data": { "record_type": "message", "direction": "outbound", "id": "b0c7e8cb-6227-4c74-9f32-c7f80c30934b", "type": "SMS", "messaging_profile_id": "16fd2706-8baf-433b-82eb-8c7fada847da", "from": { "phone_number": "+15551234567", "carrier": "Telnyx", "line_type": "Wireless" }, "to": [ { "phone_number": "+15559876543", "status": "queued", "carrier": "CARRIER", "line_type": "Wireless" } ], "text": "Hello, world!", "encoding": "GSM-7", "parts": 1, "cost": { "amount": 0.0051, "currency": "USD" } } } ``` The `status: "queued"` means your message is on its way. Save the `id` to track delivery status. ## Error handling API errors return structured JSON responses with an error code, title, and detail message. Handle these in your application to provide clear feedback and enable automatic recovery. ### Error response format ```json { "errors": [ { "code": "40300", "title": "Forbidden", "detail": "The from number +15551234567 is not assigned to a messaging profile.", "meta": { "url": "https://developers.telnyx.com/docs/messaging/messages/error-codes" } } ] } ``` ### SDK error handling examples ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); try { const response = await client.messages.send({ from: '+15551234567', to: '+15559876543', text: 'Hello, world!' }); console.log('Message sent:', response.data.id); } catch (error) { switch (error.status) { case 400: console.error('Bad request:', error.message); // Malformed JSON, missing required fields break; case 401: console.error('Authentication failed. Check your API key.'); break; case 403: console.error('Forbidden:', error.message); // Number not assigned to profile, or registration required break; case 422: console.error('Validation error:', error.message); // Invalid phone number format, text too long, etc. break; case 429: // Rate limited — extract retry-after header const retryAfter = error.headers?.['retry-after'] || 1; console.warn(`Rate limited. Retrying after ${retryAfter}s...`); await new Promise(r => setTimeout(r, retryAfter * 1000)); // Retry the request break; default: console.error(`Error (${error.status}):`, error.message); } } ``` ```python Python import os import time from telnyx import Telnyx from telnyx import APIError, AuthenticationError, RateLimitError client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) def send_with_retry(from_number, to_number, text, max_retries=3): for attempt in range(max_retries): try: response = client.messages.send( from_=from_number, to=to_number, text=text, ) print(f"Message sent: {response.data.id}") return response except AuthenticationError: print("Authentication failed. Check your API key.") raise # Don't retry auth errors except RateLimitError as e: retry_after = int(e.headers.get("retry-after", 1)) print(f"Rate limited. Retrying in {retry_after}s (attempt {attempt + 1})") time.sleep(retry_after) except APIError as e: if e.status_code == 422: print(f"Validation error: {e.message}") raise # Don't retry validation errors elif e.status_code == 403: print(f"Forbidden: {e.message}") raise # Don't retry permission errors elif e.status_code >= 500: wait = 2 ** attempt print(f"Server error ({e.status_code}). Retrying in {wait}s...") time.sleep(wait) else: raise raise Exception("Max retries exceeded") send_with_retry("+15551234567", "+15559876543", "Hello, world!") ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) begin response = client.messages.send_( from: "+15551234567", to: "+15559876543", text: "Hello, world!" ) puts "Message sent: #{response.id}" rescue Telnyx::AuthenticationError puts "Authentication failed. Check your API key." rescue Telnyx::RateLimitError => e retry_after = e.http_headers["retry-after"]&.to_i || 1 puts "Rate limited. Retry after #{retry_after}s" sleep(retry_after) retry rescue Telnyx::InvalidRequestError => e puts "Validation error: #{e.message}" rescue Telnyx::APIError => e puts "API error (#{e.http_status}): #{e.message}" end ``` ```go Go package main import ( "context" "errors" "fmt" "os" "time" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func sendWithRetry(client *telnyx.Client, params telnyx.MessageSendParams, maxRetries int) error { for attempt := 0; attempt < maxRetries; attempt++ { response, err := client.Messages.Send(context.TODO(), params) if err == nil { fmt.Printf("Message sent: %s\n", response.Data.ID) return nil } var apiErr *telnyx.Error if errors.As(err, &apiErr) { switch apiErr.StatusCode { case 401: return fmt.Errorf("authentication failed: %s", apiErr.Message) case 403: return fmt.Errorf("forbidden: %s", apiErr.Message) case 422: return fmt.Errorf("validation error: %s", apiErr.Message) case 429: wait := time.Duration(1<= 500 { wait := time.Duration(1<= 500) { long waitMs = (long) Math.pow(2, attempt) * 1000; System.out.printf("Server error (%d). Retrying in %dms...%n", e.getStatusCode(), waitMs); Thread.sleep(waitMs); } else { throw e; } } } throw new Exception("Max retries exceeded"); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var options = new MessageSendOptions { From = "+15551234567", To = "+15559876543", Text = "Hello, world!" }; int maxRetries = 3; for (int attempt = 0; attempt < maxRetries; attempt++) { try { var response = await service.SendAsync(options); Console.WriteLine($"Message sent: {response.Data.Id}"); break; } catch (TelnyxException e) when (e.HttpStatusCode == 429) { var wait = (int)Math.Pow(2, attempt) * 1000; Console.WriteLine($"Rate limited. Retrying in {wait}ms..."); await Task.Delay(wait); } catch (TelnyxException e) when (e.HttpStatusCode == 401) { Console.Error.WriteLine("Authentication failed. Check your API key."); throw; } catch (TelnyxException e) when (e.HttpStatusCode == 422) { Console.Error.WriteLine($"Validation error: {e.Message}"); throw; // Don't retry validation errors } catch (TelnyxException e) when (e.HttpStatusCode >= 500) { var wait = (int)Math.Pow(2, attempt) * 1000; Console.WriteLine($"Server error ({e.HttpStatusCode}). Retrying in {wait}ms..."); await Task.Delay(wait); } } ``` ```php PHP id . "\n"; return $response; } catch (\Telnyx\Exception\AuthenticationException $e) { echo "Authentication failed. Check your API key.\n"; throw $e; } catch (\Telnyx\Exception\InvalidRequestException $e) { echo "Validation error: " . $e->getMessage() . "\n"; throw $e; // Don't retry validation errors } catch (\Telnyx\Exception\RateLimitException $e) { $wait = pow(2, $attempt); echo "Rate limited. Retrying in {$wait}s...\n"; sleep($wait); } catch (\Telnyx\Exception\ApiException $e) { if ($e->getHttpStatus() >= 500) { $wait = pow(2, $attempt); echo "Server error ({$e->getHttpStatus()}). Retrying in {$wait}s...\n"; sleep($wait); } else { throw $e; } } } throw new \Exception("Max retries exceeded"); } sendWithRetry([ 'from' => '+15551234567', 'to' => '+15559876543', 'text' => 'Hello, world!' ]); ``` ### HTTP error codes | HTTP Status | Meaning | Retryable | Action | |-------------|---------|-----------|--------| | `400` | Bad Request | No | Fix the request body — malformed JSON or missing required fields | | `401` | Unauthorized | No | Check your API key is correct and active | | `402` | Payment Required | No | Add funds to your [account balance](https://portal.telnyx.com/#/app/billing) | | `403` | Forbidden | No | Number not assigned to a messaging profile, or sender registration required | | `404` | Not Found | No | The resource (message ID, profile ID) does not exist | | `422` | Unprocessable Entity | No | Validation failed — see error detail for the specific field | | `429` | Too Many Requests | **Yes** | Rate limited — wait for the `retry-after` header value, then retry | | `500` | Internal Server Error | **Yes** | Telnyx server error — retry with exponential backoff | | `503` | Service Unavailable | **Yes** | Temporary outage — retry with exponential backoff | ### Messaging-specific error codes These codes appear in the `errors[].code` field and provide more specific detail than HTTP status codes alone: | Code | Description | Resolution | |------|-------------|------------| | `40001` | Phone number not in E.164 format | Format as `+[country code][number]` with no spaces or punctuation | | `40002` | Missing required field | Include all required fields: `from`, `to`, and `text` (or `media_urls`) | | `40300` | Number not assigned to messaging profile | Go to [My Numbers](https://portal.telnyx.com/#/app/numbers/my-numbers) and assign a messaging profile | | `40301` | Sender registration required | Register for [10DLC](/docs/messaging/10dlc/quickstart/index), [toll-free verification](/docs/messaging/toll-free-verification), or another sender type | | `40302` | Messaging profile disabled | Re-enable the profile in the [portal](https://portal.telnyx.com/#/app/messaging) | | `42200` | Invalid `from` number | Verify the number belongs to your account and supports messaging | | `42201` | Invalid `to` number | Verify the destination is a valid, active phone number | | `42202` | Message body too long | SMS max: 1,600 characters (concatenated). Reduce content or split into multiple messages | | `42203` | Invalid media URL | Ensure `media_urls` are publicly accessible HTTPS URLs | | `42204` | Too many media attachments | MMS supports up to 10 media URLs per message | | `42205` | Media file too large | Individual media files must be under 1 MB; total under 2 MB | For a complete error code reference including delivery failure codes, see the [Messaging Error Codes](/docs/messaging/messages/error-codes) guide. ### Rate limiting The Telnyx Messaging API enforces rate limits to ensure platform stability. When you exceed the limit, the API returns `429 Too Many Requests` with a `retry-after` header. **Rate limit headers:** | Header | Description | |--------|-------------| | `x-ratelimit-limit` | Maximum requests allowed in the current window | | `x-ratelimit-remaining` | Requests remaining in the current window | | `x-ratelimit-reset` | Unix timestamp when the window resets | | `retry-after` | Seconds to wait before retrying (only on `429` responses) | **Best practices for high-volume sending:** - Implement exponential backoff: wait `2^attempt` seconds between retries (1s, 2s, 4s, 8s...) - Add jitter to prevent thundering herd: `wait = base_wait * (0.5 + random())` - Set a maximum retry count (3–5 attempts) to avoid infinite loops - Use a message queue (Redis, RabbitMQ, SQS) to buffer outbound messages and control throughput - Monitor `x-ratelimit-remaining` and slow down before hitting the limit ### Troubleshooting checklist If your message fails to send, work through this checklist: Confirm your API key is active at [API Keys](https://portal.telnyx.com/#/app/api-keys). Revoked or expired keys return `401`. Verify your `from` number is assigned to a messaging profile at [My Numbers](https://portal.telnyx.com/#/app/numbers/my-numbers). Unassigned numbers return `403`. Both `from` and `to` must be in E.164 format: `+15551234567`. No spaces, dashes, or parentheses. Sending to US carriers off-net requires registration. Check your registration status: - **10DLC:** [10DLC Registration](https://portal.telnyx.com/#/app/messaging/10dlc) - **Toll-free:** [Toll-Free Verification](https://portal.telnyx.com/#/app/messaging/toll-free-verification) - **Short code:** [Short Codes](https://portal.telnyx.com/#/app/messaging/short-codes) Insufficient balance returns `402`. Check and top up at [Billing](https://portal.telnyx.com/#/app/billing). - SMS body must not exceed 1,600 characters - MMS media URLs must be publicly accessible HTTPS URLs - Content must comply with carrier guidelines (no SHAFT content without proper registration) If the API returns `200` but the message doesn't arrive, check `message.finalized` webhook events for delivery failure details. See [Webhooks and delivery tracking](#webhooks-and-delivery-tracking). **Still stuck?** Check the [Telnyx Status Page](https://status.telnyx.com) for platform issues, or contact [support](https://support.telnyx.com) with your message ID from the API response. ## Webhooks and delivery tracking After sending a message, Telnyx delivers real-time status updates via webhooks. Configure a webhook URL on your [Messaging Profile](https://portal.telnyx.com/#/app/messaging) to receive these events automatically. ### Message lifecycle events Messages progress through these statuses: | Event | Status | Description | |-------|--------|-------------| | `message.sent` | `sent` | Message accepted and sent to the carrier | | `message.finalized` | `delivered` | Carrier confirmed delivery to the handset | | `message.finalized` | `delivery_failed` | Carrier could not deliver the message | | `message.finalized` | `delivery_unconfirmed` | No delivery confirmation received from the carrier | Not all carriers return delivery receipts. Some messages may remain in `sent` status without a finalized event. US carriers generally support delivery receipts for SMS; international coverage varies. ### Webhook payload example ```json { "data": { "event_type": "message.finalized", "id": "e6e3e550-4e3f-4b3a-9e10-1c2d3e4f5a6b", "occurred_at": "2026-03-05T18:30:00.000+00:00", "payload": { "id": "b0c7e8cb-6227-4c74-9f32-c7f80c30934b", "record_type": "message", "direction": "outbound", "type": "SMS", "from": { "phone_number": "+15551234567" }, "to": [ { "phone_number": "+15559876543", "status": "delivered" } ], "text": "Hello, world!", "parts": 1, "cost": { "amount": "0.0051", "currency": "USD" }, "errors": [], "completed_at": "2026-03-05T18:30:00.000+00:00" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://example.com/webhooks" } } ``` ### Processing webhooks Set up an endpoint to receive webhook `POST` requests and return a `200` response. Telnyx retries failed deliveries with exponential backoff. ```javascript Node import express from 'express'; const app = express(); app.use(express.json()); app.post('/webhooks/messaging', (req, res) => { const event = req.body.data; switch (event.event_type) { case 'message.sent': console.log(`Message ${event.payload.id} sent`); break; case 'message.finalized': { const status = event.payload.to[0].status; if (status === 'delivered') { console.log(`Message ${event.payload.id} delivered`); } else if (status === 'delivery_failed') { console.error(`Message ${event.payload.id} failed:`, event.payload.errors); } break; } } res.sendStatus(200); }); app.listen(3000, () => console.log('Webhook server listening on port 3000')); ``` ```python Python from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/webhooks/messaging", methods=["POST"]) def handle_webhook(): event = request.json["data"] if event["event_type"] == "message.sent": print(f"Message {event['payload']['id']} sent") elif event["event_type"] == "message.finalized": status = event["payload"]["to"][0]["status"] if status == "delivered": print(f"Message {event['payload']['id']} delivered") elif status == "delivery_failed": print(f"Message {event['payload']['id']} failed:", event["payload"]["errors"]) return jsonify(success=True), 200 if __name__ == "__main__": app.run(port=3000) ``` ```ruby Ruby require "sinatra" require "json" post "/webhooks/messaging" do event = JSON.parse(request.body.read)["data"] case event["event_type"] when "message.sent" puts "Message #{event['payload']['id']} sent" when "message.finalized" status = event["payload"]["to"][0]["status"] if status == "delivered" puts "Message #{event['payload']['id']} delivered" elsif status == "delivery_failed" puts "Message #{event['payload']['id']} failed: #{event['payload']['errors']}" end end status 200 json success: true end ``` ```go Go package main import ( "encoding/json" "fmt" "log" "net/http" ) type WebhookEvent struct { Data struct { EventType string `json:"event_type"` Payload struct { ID string `json:"id"` To []struct { Status string `json:"status"` } `json:"to"` Errors []map[string]interface{} `json:"errors"` } `json:"payload"` } `json:"data"` } func handleWebhook(w http.ResponseWriter, r *http.Request) { var event WebhookEvent if err := json.NewDecoder(r.Body).Decode(&event); err != nil { http.Error(w, "Bad request", 400) return } switch event.Data.EventType { case "message.sent": fmt.Printf("Message %s sent\n", event.Data.Payload.ID) case "message.finalized": if len(event.Data.Payload.To) > 0 { status := event.Data.Payload.To[0].Status if status == "delivered" { fmt.Printf("Message %s delivered\n", event.Data.Payload.ID) } else if status == "delivery_failed" { fmt.Printf("Message %s failed: %v\n", event.Data.Payload.ID, event.Data.Payload.Errors) } } } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]bool{"success": true}) } func main() { http.HandleFunc("/webhooks/messaging", handleWebhook) log.Println("Webhook server listening on port 3000") log.Fatal(http.ListenAndServe(":3000", nil)) } ``` ```java Java package com.telnyx.example; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpExchange; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; public class WebhookServer { private static final ObjectMapper mapper = new ObjectMapper(); public static void main(String[] args) throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0); server.createContext("/webhooks/messaging", WebhookServer::handleWebhook); System.out.println("Webhook server listening on port 3000"); server.start(); } private static void handleWebhook(HttpExchange exchange) throws IOException { JsonNode body = mapper.readTree(exchange.getRequestBody()); JsonNode data = body.get("data"); String eventType = data.get("event_type").asText(); String messageId = data.get("payload").get("id").asText(); if ("message.sent".equals(eventType)) { System.out.printf("Message %s sent%n", messageId); } else if ("message.finalized".equals(eventType)) { String status = data.get("payload").get("to").get(0).get("status").asText(); if ("delivered".equals(status)) { System.out.printf("Message %s delivered%n", messageId); } else if ("delivery_failed".equals(status)) { System.out.printf("Message %s failed: %s%n", messageId, data.get("payload").get("errors")); } } String response = "{\"success\":true}"; exchange.sendResponseHeaders(200, response.length()); try (OutputStream os = exchange.getResponseBody()) { os.write(response.getBytes()); } } } ``` ```csharp .NET using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using System.Text.Json; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapPost("/webhooks/messaging", async (HttpContext context) => { using var doc = await JsonDocument.ParseAsync(context.Request.Body); var data = doc.RootElement.GetProperty("data"); var eventType = data.GetProperty("event_type").GetString(); var messageId = data.GetProperty("payload").GetProperty("id").GetString(); if (eventType == "message.sent") { Console.WriteLine($"Message {messageId} sent"); } else if (eventType == "message.finalized") { var status = data.GetProperty("payload").GetProperty("to")[0].GetProperty("status").GetString(); if (status == "delivered") Console.WriteLine($"Message {messageId} delivered"); else if (status == "delivery_failed") Console.WriteLine($"Message {messageId} failed: {data.GetProperty("payload").GetProperty("errors")}"); } return Results.Ok(new { success = true }); }); app.Run("http://localhost:3000"); ``` ```php PHP true]); ``` ### Retrieve message status via API You can also check a message's current status by its ID: ```bash curl curl -X GET "https://api.telnyx.com/v2/messages/b0c7e8cb-6227-4c74-9f32-c7f80c30934b" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Node const message = await client.messages.retrieve('b0c7e8cb-6227-4c74-9f32-c7f80c30934b'); console.log(message.data.to[0].status); // "delivered" ``` ```python Python message = client.messages.retrieve("b0c7e8cb-6227-4c74-9f32-c7f80c30934b") print(message.data.to[0].status) # "delivered" ``` ### Delivery failure error codes When a message fails delivery, the `errors` array in the webhook payload contains error codes: | Code | Description | Action | |------|-------------|--------| | `30003` | Unreachable destination | Verify the number is active and can receive SMS | | `30004` | Message blocked by carrier | Check content compliance and sender registration | | `30005` | Unknown destination | Number may be disconnected or invalid | | `30006` | Landline or unreachable | Number cannot receive SMS (landline, VoIP) | | `30007` | Carrier violation | Message rejected due to content filtering | | `30008` | Destination capacity exceeded | Retry after a delay | For a complete error code reference, see the [Messaging Error Codes](/docs/messaging/messages/error-codes) guide. ### Webhook security Validate incoming webhooks to ensure they're from Telnyx: 1. **IP allowlisting** — Telnyx sends webhooks from `192.76.120.192/27` 2. **HTTPS endpoints** — Always use HTTPS for your webhook URL 3. **Respond quickly** — Return `200` within 5 seconds to prevent retries If your endpoint consistently fails to respond, Telnyx will retry with exponential backoff and eventually disable the webhook. Monitor your endpoint health to avoid missing delivery events. ## Next steps Set up webhooks to receive incoming SMS Learn about long codes, toll-free, and short codes Register your brand for US messaging compliance Explore all messaging parameters --- ### Receive Messages > Source: https://developers.telnyx.com/docs/messaging/messages/receive-message.md Receive inbound SMS and MMS messages via webhooks. When someone texts your Telnyx number, Telnyx sends an HTTP `POST` request with the message details to your configured webhook URL. ## Prerequisites - A Telnyx phone number assigned to a [messaging profile](/docs/messaging/messages/send-message) - A webhook URL configured on your messaging profile - [ngrok](/development/development-tools/ngrok-setup) or similar tool for local development Already have a webhook server? Skip to [Configure your webhook URL](#3-configure-your-webhook-url) to point it at your messaging profile. ## Quick Start Build a web server that accepts `POST` requests from Telnyx: ```javascript Node import express from 'express'; const app = express(); app.use(express.json()); app.post('/webhooks', (req, res) => { const { data } = req.body; if (data.event_type === 'message.received') { const { payload } = data; console.log(`From: ${payload.from.phone_number}`); console.log(`Text: ${payload.text}`); console.log(`Type: ${payload.type}`); // SMS or MMS if (payload.media?.length > 0) { console.log(`Media attachments: ${payload.media.length}`); payload.media.forEach(m => console.log(` ${m.content_type}: ${m.url}`)); } } res.sendStatus(200); }); app.listen(5000, () => console.log('Webhook server running on port 5000')); ``` ```python Python from flask import Flask, request app = Flask(__name__) @app.route('/webhooks', methods=['POST']) def webhooks(): data = request.json.get('data', {}) if data.get('event_type') == 'message.received': payload = data.get('payload', {}) print(f"From: {payload['from']['phone_number']}") print(f"Text: {payload.get('text')}") print(f"Type: {payload.get('type')}") # SMS or MMS for media in payload.get('media', []): print(f" {media['content_type']}: {media['url']}") return '', 200 if __name__ == "__main__": app.run(port=5000) ``` ```ruby Ruby require 'sinatra' require 'json' set :port, 5000 post '/webhooks' do body = JSON.parse(request.body.read) data = body['data'] if data['event_type'] == 'message.received' payload = data['payload'] puts "From: #{payload['from']['phone_number']}" puts "Text: #{payload['text']}" puts "Type: #{payload['type']}" (payload['media'] || []).each do |media| puts " #{media['content_type']}: #{media['url']}" end end status 200 end ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) type WebhookEvent struct { Data struct { EventType string `json:"event_type"` Payload struct { From struct { PhoneNumber string `json:"phone_number"` } `json:"from"` Text string `json:"text"` Type string `json:"type"` Media []struct { URL string `json:"url"` ContentType string `json:"content_type"` } `json:"media"` } `json:"payload"` } `json:"data"` } func webhookHandler(w http.ResponseWriter, r *http.Request) { var event WebhookEvent json.NewDecoder(r.Body).Decode(&event) if event.Data.EventType == "message.received" { p := event.Data.Payload fmt.Printf("From: %s\n", p.From.PhoneNumber) fmt.Printf("Text: %s\n", p.Text) fmt.Printf("Type: %s\n", p.Type) for _, m := range p.Media { fmt.Printf(" %s: %s\n", m.ContentType, m.URL) } } w.WriteHeader(http.StatusOK) } func main() { http.HandleFunc("/webhooks", webhookHandler) fmt.Println("Webhook server running on port 5000") http.ListenAndServe(":5000", nil) } ``` ```java Java package com.example.webhook; import com.sun.net.httpserver.HttpServer; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import java.io.InputStream; import java.net.InetSocketAddress; public class WebhookServer { public static void main(String[] args) throws Exception { HttpServer server = HttpServer.create(new InetSocketAddress(5000), 0); server.createContext("/webhooks", exchange -> { if ("POST".equals(exchange.getRequestMethod())) { InputStream is = exchange.getRequestBody(); String body = new String(is.readAllBytes()); JsonObject json = JsonParser.parseString(body).getAsJsonObject(); JsonObject data = json.getAsJsonObject("data"); if ("message.received".equals(data.get("event_type").getAsString())) { JsonObject payload = data.getAsJsonObject("payload"); String from = payload.getAsJsonObject("from").get("phone_number").getAsString(); System.out.println("From: " + from); System.out.println("Text: " + payload.get("text").getAsString()); System.out.println("Type: " + payload.get("type").getAsString()); JsonArray media = payload.getAsJsonArray("media"); if (media != null) { media.forEach(m -> { JsonObject obj = m.getAsJsonObject(); System.out.println(" " + obj.get("content_type").getAsString() + ": " + obj.get("url").getAsString()); }); } } exchange.sendResponseHeaders(200, -1); } }); server.start(); System.out.println("Webhook server running on port 5000"); } } ``` ```csharp .NET using System.Text.Json; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapPost("/webhooks", async (HttpRequest request) => { using var doc = await JsonDocument.ParseAsync(request.Body); var data = doc.RootElement.GetProperty("data"); if (data.GetProperty("event_type").GetString() == "message.received") { var payload = data.GetProperty("payload"); Console.WriteLine($"From: {payload.GetProperty("from").GetProperty("phone_number").GetString()}"); Console.WriteLine($"Text: {payload.GetProperty("text").GetString()}"); Console.WriteLine($"Type: {payload.GetProperty("type").GetString()}"); if (payload.TryGetProperty("media", out var media)) { foreach (var m in media.EnumerateArray()) { Console.WriteLine($" {m.GetProperty("content_type").GetString()}: {m.GetProperty("url").GetString()}"); } } } return Results.Ok(); }); app.Run("http://localhost:5000"); ``` ```php PHP { const { data } = req.body; if (data.event_type === 'message.received') { const { payload } = data; const from = payload.from.phone_number; const to = payload.to[0].phone_number; // Send auto-reply await client.messages.send({ from: to, // Reply from the number that received the message to: from, // Reply to the sender text: `Thanks for your message! We received: "${payload.text}"` }); console.log(`Auto-reply sent to ${from}`); } res.sendStatus(200); }); app.listen(5000); ``` ```python Python import os from flask import Flask, request from telnyx import Telnyx app = Flask(__name__) client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) @app.route('/webhooks', methods=['POST']) def webhooks(): data = request.json.get('data', {}) if data.get('event_type') == 'message.received': payload = data.get('payload', {}) sender = payload['from']['phone_number'] recipient = payload['to'][0]['phone_number'] # Send auto-reply client.messages.send( from_=recipient, to=sender, text=f'Thanks for your message! We received: "{payload.get("text")}"' ) print(f"Auto-reply sent to {sender}") return '', 200 if __name__ == "__main__": app.run(port=5000) ``` ```ruby Ruby require 'sinatra' require 'json' require 'telnyx' Telnyx.api_key = ENV['TELNYX_API_KEY'] post '/webhooks' do body = JSON.parse(request.body.read) data = body['data'] if data['event_type'] == 'message.received' payload = data['payload'] sender = payload['from']['phone_number'] recipient = payload['to'][0]['phone_number'] Telnyx::Message.create( from: recipient, to: sender, text: "Thanks for your message! We received: \"#{payload['text']}\"" ) puts "Auto-reply sent to #{sender}" end status 200 end ``` ```go Go package main import ( "context" "encoding/json" "fmt" "net/http" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) http.HandleFunc("/webhooks", func(w http.ResponseWriter, r *http.Request) { var event WebhookEvent json.NewDecoder(r.Body).Decode(&event) if event.Data.EventType == "message.received" { p := event.Data.Payload sender := p.From.PhoneNumber recipient := p.To[0].PhoneNumber client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: recipient, To: sender, Text: fmt.Sprintf("Thanks for your message! We received: \"%s\"", p.Text), }) fmt.Printf("Auto-reply sent to %s\n", sender) } w.WriteHeader(http.StatusOK) }) fmt.Println("Server running on port 5000") http.ListenAndServe(":5000", nil) } ``` **Avoid reply loops.** If both sides auto-reply, they'll ping each other forever. Guard against this by checking the sender isn't one of your own numbers, or by tracking recently replied conversations. --- ## Webhook Payload Reference ### Inbound SMS ```json { "data": { "event_type": "message.received", "id": "b301ed3f-1490-491f-995f-6e64e69674d4", "occurred_at": "2024-01-15T20:16:07.588+00:00", "payload": { "direction": "inbound", "encoding": "GSM-7", "from": { "carrier": "T-Mobile USA", "line_type": "long_code", "phone_number": "+13125550001", "status": "webhook_delivered" }, "id": "84cca175-9755-4859-b67f-4730d7f58aa3", "media": [], "messaging_profile_id": "740572b6-099c-44a1-89b9-6c92163bc68d", "parts": 1, "received_at": "2024-01-15T20:16:07.503+00:00", "record_type": "message", "text": "Hello from Telnyx!", "to": [ { "carrier": "Telnyx", "line_type": "Wireless", "phone_number": "+17735550002", "status": "webhook_delivered" } ], "type": "SMS" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://example.com/webhooks" } } ``` ### Inbound MMS MMS messages include a `media` array with downloadable attachments: ```json { "data": { "event_type": "message.received", "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "occurred_at": "2024-01-15T20:18:30.000+00:00", "payload": { "direction": "inbound", "from": { "carrier": "T-Mobile USA", "line_type": "long_code", "phone_number": "+13125550001" }, "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "media": [ { "url": "https://media.telnyx.com/example-image.png", "content_type": "image/png", "sha256": "ab1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", "size": 102400 } ], "messaging_profile_id": "740572b6-099c-44a1-89b9-6c92163bc68d", "parts": 1, "received_at": "2024-01-15T20:18:30.000+00:00", "record_type": "message", "text": "Check out this photo!", "to": [ { "carrier": "Telnyx", "line_type": "Wireless", "phone_number": "+17735550002" } ], "type": "MMS" }, "record_type": "event" } } ``` MMS media URLs expire after **30 days**. Download and store media files if you need long-term access. ### Webhook Payload Schema | Field | Type | Description | |-------|------|-------------| | `data.event_type` | `string` | Always `message.received` for inbound messages | | `data.id` | `string` | Unique event ID (UUID) — use for idempotency | | `data.occurred_at` | `string` | ISO 8601 timestamp when the event occurred | | `data.record_type` | `string` | Always `event` | | `meta.attempt` | `integer` | Webhook delivery attempt number (1–3) | | `meta.delivered_to` | `string` | The webhook URL this was delivered to | | Field | Type | Description | |-------|------|-------------| | `payload.id` | `string` | Unique message ID (UUID) — use for deduplication | | `payload.direction` | `string` | Always `inbound` for received messages | | `payload.from.phone_number` | `string` | Sender's phone number (E.164 format) | | `payload.from.carrier` | `string` | Sender's carrier name | | `payload.from.line_type` | `string` | `long_code`, `toll_free`, `short_code`, etc. | | `payload.to[].phone_number` | `string` | Your Telnyx number that received the message | | `payload.text` | `string` | Message body (may be `null` for media-only MMS) | | `payload.type` | `string` | `SMS` or `MMS` | | `payload.encoding` | `string` | `GSM-7` or `UCS-2` ([details](/docs/messaging/messages/message-encoding)) | | `payload.parts` | `integer` | Number of message segments | | `payload.messaging_profile_id` | `string` | Messaging profile that received the message | | `payload.received_at` | `string` | ISO 8601 timestamp when the message was received | | `payload.record_type` | `string` | Always `message` | | Field | Type | Description | |-------|------|-------------| | `payload.media` | `array` | Array of media attachments (empty for SMS) | | `payload.media[].url` | `string` | Authenticated download URL (expires in 30 days) | | `payload.media[].content_type` | `string` | MIME type (e.g., `image/png`, `video/mp4`) | | `payload.media[].sha256` | `string` | SHA-256 hash of the media file | | `payload.media[].size` | `integer` | File size in bytes | **Supported MMS media types:** - Images: `image/jpeg`, `image/png`, `image/gif`, `image/bmp`, `image/webp` - Video: `video/mp4`, `video/3gpp` - Audio: `audio/mpeg`, `audio/ogg`, `audio/amr` - Files: `application/pdf`, `text/vcard`, `text/calendar` --- ## Downloading MMS Media Download media attachments from inbound MMS messages using the URLs in the `media` array. Authenticate with your API key: ```javascript Node import fs from 'fs'; async function downloadMedia(mediaUrl, apiKey) { const response = await fetch(mediaUrl, { headers: { 'Authorization': `Bearer ${apiKey}` } }); const buffer = Buffer.from(await response.arrayBuffer()); const filename = mediaUrl.split('/').pop(); fs.writeFileSync(filename, buffer); console.log(`Downloaded: ${filename} (${buffer.length} bytes)`); } // In your webhook handler: for (const media of payload.media) { await downloadMedia(media.url, process.env.TELNYX_API_KEY); } ``` ```python Python import requests import os def download_media(media_url, api_key): response = requests.get( media_url, headers={"Authorization": f"Bearer {api_key}"} ) filename = media_url.split("/")[-1] with open(filename, "wb") as f: f.write(response.content) print(f"Downloaded: {filename} ({len(response.content)} bytes)") # In your webhook handler: for media in payload.get("media", []): download_media(media["url"], os.environ["TELNYX_API_KEY"]) ``` ```go Go func downloadMedia(mediaURL, apiKey string) error { req, _ := http.NewRequest("GET", mediaURL, nil) req.Header.Set("Authorization", "Bearer "+apiKey) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() parts := strings.Split(mediaURL, "/") filename := parts[len(parts)-1] file, _ := os.Create(filename) defer file.Close() written, _ := io.Copy(file, resp.Body) fmt.Printf("Downloaded: %s (%d bytes)\n", filename, written) return nil } ``` ```ruby Ruby require 'net/http' require 'uri' def download_media(media_url, api_key) uri = URI.parse(media_url) request = Net::HTTP::Get.new(uri) request['Authorization'] = "Bearer #{api_key}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end filename = File.basename(uri.path) File.open(filename, 'wb') { |f| f.write(response.body) } puts "Downloaded: #{filename} (#{response.body.bytesize} bytes)" end ``` ```java Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Path; public static void downloadMedia(String mediaUrl, String apiKey) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(mediaUrl)) .header("Authorization", "Bearer " + apiKey) .build(); String filename = mediaUrl.substring(mediaUrl.lastIndexOf('/') + 1); client.send(request, HttpResponse.BodyHandlers.ofFile(Path.of(filename))); System.out.println("Downloaded: " + filename); } ``` ```csharp .NET using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey); var response = await httpClient.GetAsync(mediaUrl); var bytes = await response.Content.ReadAsByteArrayAsync(); var filename = new Uri(mediaUrl).Segments.Last(); await File.WriteAllBytesAsync(filename, bytes); Console.WriteLine($"Downloaded: {filename} ({bytes.Length} bytes)"); ``` ```php PHP { const signature = req.headers['telnyx-signature-ed25519']; const timestamp = req.headers['telnyx-timestamp']; const payload = JSON.stringify(req.body); try { const event = telnyx.webhooks.constructEvent( payload, signature, timestamp, process.env.TELNYX_PUBLIC_KEY ); console.log('Verified event:', event.data.event_type); res.sendStatus(200); } catch (err) { console.error('Signature verification failed:', err.message); res.sendStatus(400); } }); ``` ```python Python import telnyx telnyx.api_key = "YOUR_API_KEY" telnyx.public_key = "YOUR_PUBLIC_KEY" @app.route('/webhooks', methods=['POST']) def webhooks(): payload = request.data signature = request.headers.get('telnyx-signature-ed25519') timestamp = request.headers.get('telnyx-timestamp') try: event = telnyx.Webhook.construct_event(payload, signature, timestamp) print(f"Verified event: {event['data']['event_type']}") return '', 200 except telnyx.error.SignatureVerificationError as e: print(f"Signature verification failed: {e}") return '', 400 ``` ```ruby Ruby require 'telnyx' Telnyx.api_key = 'YOUR_API_KEY' Telnyx.public_key = 'YOUR_PUBLIC_KEY' post '/webhooks' do payload = request.body.read signature = request.env['HTTP_TELNYX_SIGNATURE_ED25519'] timestamp = request.env['HTTP_TELNYX_TIMESTAMP'] begin event = Telnyx::Webhook.construct_event(payload, signature, timestamp) puts "Verified event: #{event['data']['event_type']}" status 200 rescue Telnyx::SignatureVerificationError => e puts "Signature verification failed: #{e.message}" status 400 end end ``` ```csharp .NET using Telnyx; app.MapPost("/webhooks", async (HttpRequest request) => { var payload = await new StreamReader(request.Body).ReadToEndAsync(); var signature = request.Headers["telnyx-signature-ed25519"].ToString(); var timestamp = request.Headers["telnyx-timestamp"].ToString(); try { var webhookEvent = Webhook.ConstructEvent(payload, signature, timestamp); Console.WriteLine($"Verified event: {webhookEvent.EventType}"); return Results.Ok(); } catch (TelnyxSignatureVerificationException e) { Console.WriteLine($"Signature verification failed: {e.Message}"); return Results.BadRequest(); } }); ``` ```php PHP getMessage(); http_response_code(400); } ``` Get your public key from the [Mission Control Portal](https://portal.telnyx.com) under **Account Settings > Keys & Credentials > Public Key**. For detailed webhook signature verification, see [Webhook Fundamentals](/development/api-fundamentals/webhooks/receiving-webhooks). --- ## Handling Failed Webhooks When webhook delivery fails, Telnyx retries automatically. Build resilience into your architecture: ```javascript Node import express from 'express'; import { createClient } from 'redis'; const app = express(); app.use(express.json()); const redis = createClient(); await redis.connect(); app.post('/webhooks', async (req, res) => { // Respond immediately to avoid timeout res.sendStatus(200); const { data } = req.body; const messageId = data.payload?.id; // Deduplicate using Redis (TTL: 24 hours) if (messageId) { const exists = await redis.get(`msg:${messageId}`); if (exists) return; // Already processed await redis.set(`msg:${messageId}`, '1', { EX: 86400 }); } // Process asynchronously try { await processMessage(data); } catch (err) { console.error(`Failed to process ${messageId}:`, err); // Log to dead letter queue for manual review await redis.lpush('webhook:failed', JSON.stringify({ data, error: err.message })); } }); ``` ```python Python import redis import json import threading from flask import Flask, request app = Flask(__name__) r = redis.Redis() def process_async(data, message_id): """Process webhook in background thread.""" try: process_message(data) except Exception as e: print(f"Failed to process {message_id}: {e}") r.lpush("webhook:failed", json.dumps({"data": data, "error": str(e)})) @app.route('/webhooks', methods=['POST']) def webhooks(): data = request.json.get('data', {}) message_id = data.get('payload', {}).get('id') # Deduplicate if message_id: if r.get(f"msg:{message_id}"): return '', 200 r.set(f"msg:{message_id}", "1", ex=86400) # Process in background thread — return 200 immediately # For production, use Celery or RQ instead of threads threading.Thread(target=process_async, args=(data, message_id)).start() return '', 200 ``` For high-volume applications, use a message queue (Redis, SQS, RabbitMQ) between your webhook endpoint and processing logic. This ensures you always respond within the 2-second timeout. --- ## Troubleshooting **Check your webhook URL is configured:** ```bash curl -s "https://api.telnyx.com/v2/messaging_profiles/YOUR_PROFILE_ID" \ -H "Authorization: Bearer YOUR_API_KEY" | jq '.data.webhook_url' ``` **Common causes:** - Webhook URL not set on the messaging profile - Phone number not assigned to the messaging profile - Server not publicly accessible (use ngrok for local dev) - Firewall blocking Telnyx IPs **Test your endpoint directly:** ```bash curl -X POST https://your-server.com/webhooks \ -H "Content-Type: application/json" \ -d '{"data":{"event_type":"message.received","payload":{"text":"test"}}}' ``` Duplicates happen when your server doesn't respond with `2xx` within the timeout (2 seconds for API v2). Telnyx retries up to 2 times. **Fix:** Return `200` immediately, then process the message asynchronously. Use the `payload.id` field to deduplicate. ```javascript // Deduplicate using a Set (use Redis in production) const processed = new Set(); app.post('/webhooks', (req, res) => { res.sendStatus(200); // Respond immediately const messageId = req.body.data.payload?.id; if (messageId && processed.has(messageId)) return; processed.add(messageId); // Process message asynchronously handleMessage(req.body.data); }); ``` Your server must respond within **2 seconds** (API v2) or 5 seconds (API v1). **Solutions:** - Return `200` immediately, process in background - Use a message queue (Redis, SQS, RabbitMQ) for heavy processing - Set up a [failover URL](/api-reference/profiles/update-a-messaging-profile) as a backup - Check the `type` field is `MMS` (SMS messages have an empty `media` array) - Media URLs require authentication — include your API key in the `Authorization` header - Media URLs expire after 30 days - Verify your number is MMS-enabled in the [Portal](https://portal.telnyx.com/#/app/numbers/my-numbers) --- ## Webhook URL Hierarchy Telnyx checks for webhook URLs in this order: 1. **Request body** — URLs provided when [sending a message](/api-reference/messages/send-a-message) (outbound delivery receipts only) 2. **Messaging profile** — URLs configured on the profile 3. **No URL** — Webhook delivery is skipped Configure a **failover URL** on your messaging profile for redundancy. If the primary URL fails all retry attempts, Telnyx sends to the failover URL. --- ## Related Webhook Events Your webhook URL receives more than just `message.received`. For delivery status tracking and other events, see [Receiving Webhooks](/docs/messaging/messages/receiving-webhooks). | Event | Description | |-------|-------------| | `message.received` | Inbound SMS or MMS received (this guide) | | `message.sent` | Outbound message accepted by carrier | | `message.finalized` | Final delivery status (delivered, failed, etc.) | --- ## Next Steps Send outbound SMS and MMS messages All webhook event types and payload details Understand GSM-7, UCS-2, and message segmentation Handle STOP/HELP keywords automatically --- ### Webhooks > Source: https://developers.telnyx.com/docs/messaging/messages/receiving-webhooks.md Telnyx sends webhooks to notify your application about messaging events in real time — inbound messages, delivery status updates, and errors. This guide covers every event type, payload structure, signature verification, and best practices for production webhook handling. ## Prerequisites - A [Telnyx account](https://telnyx.com/sign-up) with a phone number assigned to a [messaging profile](/docs/messaging/messages/send-message) - A publicly accessible HTTPS endpoint (or [ngrok](/development/development-tools/ngrok-setup) for local development) - Your [API key](https://portal.telnyx.com/#/app/api-keys) and [public key](https://portal.telnyx.com/#/app/api-keys) (for signature verification) ## How webhook delivery works A message is received by your number, or a sent message changes status (queued → sent → delivered). An HTTP `POST` with a JSON payload is sent to your configured webhook URL. Return a `2xx` status code within **2 seconds** to acknowledge receipt. If your server doesn't respond in time, Telnyx retries (up to 3 attempts per URL) and then tries your failover URL if configured. ### Webhook URL hierarchy Telnyx determines where to send webhooks using this priority order: 1. **Per-message URLs** — `webhook_url` and `webhook_failover_url` in the [send message request body](/api-reference/messages/send-a-message) 2. **Messaging profile URLs** — Configured on the [messaging profile](/api-reference/profiles/create-a-messaging-profile) 3. **No webhook** — If neither is set, no webhook is delivered (events are still available in [Message Detail Records](/docs/messaging/messages/message-detail-records)) --- ## Webhook event types Telnyx messaging produces the following webhook events: | Event Type | Trigger | Direction | |---|---|---| | `message.received` | An inbound SMS/MMS arrives at your number | Inbound | | `message.sent` | An outbound message has been accepted and sent to the carrier | Outbound | | `message.finalized` | An outbound message has reached a terminal state (delivered, failed, etc.) | Outbound | --- ## Payload structure All messaging webhooks share this top-level structure: ```json { "data": { "event_type": "message.received", "id": "unique-event-id", "occurred_at": "2024-01-15T20:16:07.588+00:00", "payload": { ... }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://example.com/webhooks" } } ``` | Field | Description | |---|---| | `data.event_type` | The event type (`message.received`, `message.sent`, `message.finalized`) | | `data.id` | Unique identifier for this webhook event | | `data.occurred_at` | ISO 8601 timestamp of when the event occurred | | `data.payload` | Message details (see examples below) | | `data.record_type` | Always `"event"` | | `meta.attempt` | Delivery attempt number (starts at 1) | | `meta.delivered_to` | The URL this webhook was delivered to | --- ## Event examples ### Inbound message (`message.received`) Triggered when your Telnyx number receives an SMS or MMS: ```json { "data": { "event_type": "message.received", "id": "b301ed3f-1490-491f-995f-6e64e69674d4", "occurred_at": "2024-01-15T20:16:07.588+00:00", "payload": { "completed_at": null, "cost": { "amount": "0.0000", "currency": "USD" }, "direction": "inbound", "encoding": "GSM-7", "errors": [], "from": { "carrier": "T-Mobile USA", "line_type": "long_code", "phone_number": "+13125550001" }, "id": "84cca175-9755-4859-b67f-4730d7f58aa3", "media": [], "messaging_profile_id": "740572b6-099c-44a1-89b9-6c92163bc68d", "organization_id": "47a530f8-4362-4526-829b-bcee17fd9f7a", "parts": 1, "received_at": "2024-01-15T20:16:07.503+00:00", "record_type": "message", "sent_at": null, "tags": [], "text": "Hello from Telnyx!", "to": [ { "carrier": "Telnyx", "line_type": "Wireless", "phone_number": "+17735550002", "status": "webhook_delivered" } ], "type": "SMS", "valid_until": null, "webhook_failover_url": null, "webhook_url": "https://example.com/webhooks" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://example.com/webhooks" } } ``` MMS messages include a `media` array with URLs, content types, and file sizes: ```json { "media": [ { "url": "https://media.telnyx.com/example-image.png", "content_type": "image/png", "sha256": "ab1c2d3e4f...", "size": 102400 } ], "type": "MMS" } ``` MMS media links expire after **30 days**. Download and store media files if you need long-term access. ### Message sent (`message.sent`) Triggered when an outbound message has been accepted by the downstream carrier: ```json { "data": { "event_type": "message.sent", "id": "a1b2c3d4-5678-9012-abcd-ef1234567890", "occurred_at": "2024-01-15T21:32:13.596+00:00", "payload": { "completed_at": null, "cost": { "amount": "0.0051", "currency": "USD" }, "direction": "outbound", "encoding": "GSM-7", "errors": [], "from": { "carrier": "Telnyx", "line_type": "Wireless", "phone_number": "+13125550001" }, "id": "ac012cbf-5e09-46af-a69a-7c0e2d90993c", "media": [], "messaging_profile_id": "83d2343b-553f-4c5f-b8c8-fd27004f94bf", "organization_id": "9d76d591-1b7d-405d-8c64-1320ee070245", "parts": 1, "received_at": "2024-01-15T21:32:13.552+00:00", "record_type": "message", "sent_at": "2024-01-15T21:32:13.596+00:00", "text": "Hello there!", "to": [ { "carrier": "T-MOBILE USA, INC.", "line_type": "Wireless", "phone_number": "+13125550002", "status": "sent" } ], "type": "SMS", "valid_until": "2024-01-15T22:32:13.552+00:00", "webhook_url": "https://example.com/webhooks" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://example.com/webhooks" } } ``` ### Delivery receipt (`message.finalized`) Triggered when a message reaches a terminal delivery state: ```json { "data": { "event_type": "message.finalized", "id": "4ee8c3a6-4995-4309-a3c6-38e3db9ea4be", "occurred_at": "2024-01-15T21:32:14.148+00:00", "payload": { "completed_at": "2024-01-15T21:32:14.148+00:00", "cost": { "amount": "0.0051", "currency": "USD" }, "cost_breakdown": { "carrier_fee": { "amount": "0.00305", "currency": "USD" }, "rate": { "amount": "0.00205", "currency": "USD" } }, "direction": "outbound", "encoding": "GSM-7", "errors": [], "from": { "carrier": "Telnyx", "line_type": "Wireless", "phone_number": "+13125550001", "status": "webhook_delivered" }, "id": "ac012cbf-5e09-46af-a69a-7c0e2d90993c", "media": [], "messaging_profile_id": "83d2343b-553f-4c5f-b8c8-fd27004f94bf", "organization_id": "9d76d591-1b7d-405d-8c64-1320ee070245", "parts": 1, "received_at": "2024-01-15T21:32:13.552+00:00", "record_type": "message", "sent_at": "2024-01-15T21:32:13.596+00:00", "tags": ["tag-a", "tag-b"], "text": "Hello there!", "to": [ { "carrier": "T-MOBILE USA, INC.", "line_type": "Wireless", "phone_number": "+13125550002", "status": "delivered" } ], "type": "SMS", "valid_until": "2024-01-15T22:32:13.552+00:00", "webhook_url": "https://example.com/webhooks", "tcr_campaign_billable": true, "tcr_campaign_id": "CNZO3VL", "tcr_campaign_registered": "REGISTERED" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://example.com/webhooks" } } ``` ### Delivery statuses The `to[].status` field in `message.finalized` events indicates the final delivery outcome: | Status | Description | |---|---| | `queued` | Message is queued on Telnyx's side | | `sending` | Message is being sent to an upstream carrier | | `sent` | Message has been sent to the upstream carrier | | `delivered` | Carrier has confirmed delivery to the recipient | | `sending_failed` | Telnyx failed to send the message to the carrier | | `delivery_failed` | The carrier failed to deliver the message to the recipient | | `delivery_unconfirmed` | No delivery confirmation was received from the carrier | When a message fails, the `errors` array in the payload contains details: ```json { "errors": [ { "code": "40300", "title": "Destination number unreachable", "detail": "The destination number is not reachable on the carrier network.", "source": { "pointer": "/to/0/phone_number" } } ] } ``` Common error codes: | Code | Meaning | |---|---| | `40001` | Destination number invalid | | `40002` | Destination number not in service | | `40300` | Destination unreachable | | `40008` | Message filtered by carrier | | `40010` | Message blocked (spam/content filter) | | `47000` | 10DLC campaign required | For a complete list, see the [Error Codes reference](/development/api-fundamentals/api-errors). --- ## Webhook signature verification Telnyx signs every webhook using **Ed25519 public key cryptography** so you can verify that requests genuinely come from Telnyx. **This is strongly recommended for production deployments.** Each webhook request includes two headers: | Header | Description | |---|---| | `telnyx-signature-ed25519` | Base64-encoded Ed25519 signature | | `telnyx-timestamp` | Unix timestamp of when the request was signed | The signature is computed over the string `{timestamp}|{json_payload}`. ### Get your public key Find your public key in the [Mission Control Portal](https://portal.telnyx.com/#/app/api-keys) under **Keys & Credentials → Public Key**. ### Verification examples ```javascript Node import express from 'express'; import Telnyx from 'telnyx'; const app = express(); app.use(express.json()); const telnyx = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); app.post('/webhooks', (req, res) => { const signature = req.headers['telnyx-signature-ed25519']; const timestamp = req.headers['telnyx-timestamp']; const payload = JSON.stringify(req.body); try { const event = telnyx.webhooks.constructEvent( payload, signature, timestamp, process.env.TELNYX_PUBLIC_KEY ); console.log('Verified event:', event.data.event_type); res.sendStatus(200); } catch (err) { console.error('Signature verification failed:', err.message); res.sendStatus(403); } }); app.listen(5000, () => console.log('Server running on port 5000')); ``` ```python Python from flask import Flask, request import telnyx app = Flask(__name__) telnyx.api_key = "YOUR_API_KEY" telnyx.public_key = "YOUR_PUBLIC_KEY" @app.route('/webhooks', methods=['POST']) def webhooks(): payload = request.data signature = request.headers.get('telnyx-signature-ed25519') timestamp = request.headers.get('telnyx-timestamp') try: event = telnyx.Webhook.construct_event(payload, signature, timestamp) print(f"Verified event: {event['data']['event_type']}") return '', 200 except telnyx.error.SignatureVerificationError: return 'Invalid signature', 403 ``` ```ruby Ruby require 'sinatra' require 'telnyx' require 'json' Telnyx.api_key = ENV['TELNYX_API_KEY'] Telnyx.public_key = ENV['TELNYX_PUBLIC_KEY'] post '/webhooks' do payload = request.body.read signature = request.env['HTTP_TELNYX_SIGNATURE_ED25519'] timestamp = request.env['HTTP_TELNYX_TIMESTAMP'] begin event = Telnyx::Webhook.construct_event(payload, signature, timestamp) puts "Verified event: #{event['data']['event_type']}" status 200 rescue Telnyx::SignatureVerificationError status 403 body 'Invalid signature' end end ``` ```go Go package main import ( "crypto/ed25519" "encoding/base64" "fmt" "io" "net/http" "os" ) func verifySignature(payload, signature, timestamp string, publicKey ed25519.PublicKey) bool { signedPayload := timestamp + "|" + payload sigBytes, err := base64.StdEncoding.DecodeString(signature) if err != nil { return false } return ed25519.Verify(publicKey, []byte(signedPayload), sigBytes) } func webhookHandler(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) signature := r.Header.Get("telnyx-signature-ed25519") timestamp := r.Header.Get("telnyx-timestamp") pubKeyBytes, _ := base64.StdEncoding.DecodeString(os.Getenv("TELNYX_PUBLIC_KEY")) publicKey := ed25519.PublicKey(pubKeyBytes) if !verifySignature(string(body), signature, timestamp, publicKey) { http.Error(w, "Invalid signature", http.StatusForbidden) return } fmt.Println("Webhook verified and received") w.WriteHeader(http.StatusOK) } func main() { http.HandleFunc("/webhooks", webhookHandler) fmt.Println("Server running on port 5000") http.ListenAndServe(":5000", nil) } ``` ```java Java package com.example.webhook; import com.sun.net.httpserver.HttpServer; import java.io.InputStream; import java.net.InetSocketAddress; import java.security.*; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; public class WebhookServer { private static boolean verifySignature(String payload, String signature, String timestamp, String publicKeyBase64) throws Exception { byte[] pubKeyBytes = Base64.getDecoder().decode(publicKeyBase64); KeyFactory keyFactory = KeyFactory.getInstance("Ed25519"); PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(pubKeyBytes)); Signature sig = Signature.getInstance("Ed25519"); sig.initVerify(publicKey); sig.update((timestamp + "|" + payload).getBytes()); return sig.verify(Base64.getDecoder().decode(signature)); } public static void main(String[] args) throws Exception { String publicKeyBase64 = System.getenv("TELNYX_PUBLIC_KEY"); HttpServer server = HttpServer.create(new InetSocketAddress(5000), 0); server.createContext("/webhooks", exchange -> { InputStream is = exchange.getRequestBody(); String body = new String(is.readAllBytes()); String signature = exchange.getRequestHeaders().getFirst("telnyx-signature-ed25519"); String timestamp = exchange.getRequestHeaders().getFirst("telnyx-timestamp"); try { if (verifySignature(body, signature, timestamp, publicKeyBase64)) { System.out.println("Webhook verified"); exchange.sendResponseHeaders(200, -1); } else { exchange.sendResponseHeaders(403, -1); } } catch (Exception e) { exchange.sendResponseHeaders(500, -1); } }); server.start(); System.out.println("Server running on port 5000"); } } ``` ```csharp .NET // Requires: dotnet add package NSec.Cryptography using System.Text; using NSec.Cryptography; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapPost("/webhooks", async (HttpContext context) => { using var reader = new StreamReader(context.Request.Body); var body = await reader.ReadToEndAsync(); var signature = context.Request.Headers["telnyx-signature-ed25519"].ToString(); var timestamp = context.Request.Headers["telnyx-timestamp"].ToString(); var publicKeyBytes = Convert.FromBase64String( Environment.GetEnvironmentVariable("TELNYX_PUBLIC_KEY")!); var signedPayload = Encoding.UTF8.GetBytes($"{timestamp}|{body}"); var signatureBytes = Convert.FromBase64String(signature); var algorithm = SignatureAlgorithm.Ed25519; var publicKey = PublicKey.Import(algorithm, publicKeyBytes, KeyBlobFormat.RawPublicKey); if (algorithm.Verify(publicKey, signedPayload, signatureBytes)) { Console.WriteLine("Webhook verified"); return Results.Ok(); } return Results.StatusCode(403); }); app.Run("http://0.0.0.0:5000"); ``` ```php PHP { // Respond immediately to avoid timeout res.sendStatus(200); const { data } = req.body; switch (data.event_type) { case 'message.received': handleInboundMessage(data.payload); break; case 'message.sent': handleMessageSent(data.payload); break; case 'message.finalized': handleDeliveryReceipt(data.payload); break; } }); function handleInboundMessage(payload) { const from = payload.from.phone_number; const text = payload.text; console.log(`Inbound from ${from}: ${text}`); // Check for MMS media if (payload.media?.length > 0) { payload.media.forEach(m => console.log(`Media: ${m.url} (${m.content_type})`)); } } function handleMessageSent(payload) { console.log(`Message ${payload.id} sent to carrier`); } function handleDeliveryReceipt(payload) { const status = payload.to[0]?.status; console.log(`Message ${payload.id} finalized: ${status}`); if (status === 'delivery_failed') { console.error('Delivery failed:', payload.errors); } } app.listen(5000, () => console.log('Webhook server running on port 5000')); ``` ```python Python from flask import Flask, request app = Flask(__name__) @app.route('/webhooks', methods=['POST']) def webhooks(): data = request.json.get('data', {}) event_type = data.get('event_type') payload = data.get('payload', {}) if event_type == 'message.received': handle_inbound_message(payload) elif event_type == 'message.sent': handle_message_sent(payload) elif event_type == 'message.finalized': handle_delivery_receipt(payload) return '', 200 def handle_inbound_message(payload): from_number = payload['from']['phone_number'] text = payload.get('text', '') print(f"Inbound from {from_number}: {text}") for media in payload.get('media', []): print(f"Media: {media['url']} ({media['content_type']})") def handle_message_sent(payload): print(f"Message {payload['id']} sent to carrier") def handle_delivery_receipt(payload): status = payload['to'][0]['status'] print(f"Message {payload['id']} finalized: {status}") if status == 'delivery_failed': print(f"Delivery failed: {payload.get('errors')}") if __name__ == '__main__': app.run(port=5000) ``` ```ruby Ruby require 'sinatra' require 'json' post '/webhooks' do body = JSON.parse(request.body.read) data = body['data'] payload = data['payload'] case data['event_type'] when 'message.received' puts "Inbound from #{payload['from']['phone_number']}: #{payload['text']}" payload['media']&.each { |m| puts "Media: #{m['url']}" } when 'message.sent' puts "Message #{payload['id']} sent to carrier" when 'message.finalized' status_val = payload['to'][0]['status'] puts "Message #{payload['id']} finalized: #{status_val}" puts "Errors: #{payload['errors']}" if status_val == 'delivery_failed' end status 200 end ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) type Webhook struct { Data struct { EventType string `json:"event_type"` Payload struct { ID string `json:"id"` From struct { PhoneNumber string `json:"phone_number"` } `json:"from"` To []struct { PhoneNumber string `json:"phone_number"` Status string `json:"status"` } `json:"to"` Text string `json:"text"` Media []struct { URL string `json:"url"` ContentType string `json:"content_type"` } `json:"media"` Errors []struct { Code string `json:"code"` Title string `json:"title"` } `json:"errors"` } `json:"payload"` } `json:"data"` } func webhookHandler(w http.ResponseWriter, r *http.Request) { var wh Webhook json.NewDecoder(r.Body).Decode(&wh) switch wh.Data.EventType { case "message.received": fmt.Printf("Inbound from %s: %s\n", wh.Data.Payload.From.PhoneNumber, wh.Data.Payload.Text) for _, m := range wh.Data.Payload.Media { fmt.Printf("Media: %s (%s)\n", m.URL, m.ContentType) } case "message.sent": fmt.Printf("Message %s sent to carrier\n", wh.Data.Payload.ID) case "message.finalized": if len(wh.Data.Payload.To) > 0 { fmt.Printf("Message %s finalized: %s\n", wh.Data.Payload.ID, wh.Data.Payload.To[0].Status) } } w.WriteHeader(http.StatusOK) } func main() { http.HandleFunc("/webhooks", webhookHandler) fmt.Println("Webhook server running on port 5000") http.ListenAndServe(":5000", nil) } ``` ```java Java package com.example.webhook; import com.sun.net.httpserver.HttpServer; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import java.io.InputStream; import java.net.InetSocketAddress; public class WebhookServer { public static void main(String[] args) throws Exception { HttpServer server = HttpServer.create(new InetSocketAddress(5000), 0); server.createContext("/webhooks", exchange -> { InputStream is = exchange.getRequestBody(); String body = new String(is.readAllBytes()); JsonObject json = JsonParser.parseString(body).getAsJsonObject(); JsonObject data = json.getAsJsonObject("data"); String eventType = data.get("event_type").getAsString(); JsonObject payload = data.getAsJsonObject("payload"); switch (eventType) { case "message.received": String from = payload.getAsJsonObject("from") .get("phone_number").getAsString(); String text = payload.get("text").getAsString(); System.out.println("Inbound from " + from + ": " + text); break; case "message.sent": System.out.println("Message " + payload.get("id").getAsString() + " sent"); break; case "message.finalized": String status = payload.getAsJsonArray("to").get(0) .getAsJsonObject().get("status").getAsString(); System.out.println("Message " + payload.get("id").getAsString() + " finalized: " + status); break; } exchange.sendResponseHeaders(200, -1); }); server.start(); System.out.println("Webhook server running on port 5000"); } } ``` ```csharp .NET using Microsoft.AspNetCore.Mvc; using System.Text.Json; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapPost("/webhooks", async (HttpContext context) => { using var reader = new StreamReader(context.Request.Body); var body = await reader.ReadToEndAsync(); var json = JsonDocument.Parse(body); var data = json.RootElement.GetProperty("data"); var eventType = data.GetProperty("event_type").GetString(); var payload = data.GetProperty("payload"); switch (eventType) { case "message.received": var from = payload.GetProperty("from").GetProperty("phone_number").GetString(); var text = payload.GetProperty("text").GetString(); Console.WriteLine($"Inbound from {from}: {text}"); break; case "message.sent": Console.WriteLine($"Message {payload.GetProperty("id").GetString()} sent"); break; case "message.finalized": var status = payload.GetProperty("to")[0].GetProperty("status").GetString(); Console.WriteLine($"Message {payload.GetProperty("id").GetString()} finalized: {status}"); break; } return Results.Ok(); }); app.Run("http://0.0.0.0:5000"); ``` ```php PHP { const eventId = req.body.data.id; if (processedEvents.has(eventId)) { return res.sendStatus(200); // Already processed } processedEvents.add(eventId); // Process event... res.sendStatus(200); }); ``` For production, use a persistent store (Redis, database) instead of in-memory sets. Telnyx does not guarantee delivery order. For example, `message.finalized` may arrive before `message.sent`. Use the `data.occurred_at` timestamp to determine event sequence, and design your logic to handle any arrival order. 1. **Ensure you're reading the raw body** — Parse the signature against the raw request body, not a re-serialized JSON object. 2. **Check your public key** — Verify you're using the correct public key from the [Portal](https://portal.telnyx.com/#/app/api-keys). 3. **Check timestamp tolerance** — If you're rejecting stale timestamps, ensure your server clock is synchronized (NTP). Your endpoint must respond within **2 seconds**. If your processing takes longer: - Return `200` immediately - Process the event asynchronously (use a message queue like Redis, RabbitMQ, or SQS) --- ## Next steps Step-by-step guide to building a webhook server Send SMS and MMS with the Messaging API Platform-wide webhook concepts, signing, and retry behavior Query historical message data and delivery statuses --- ### Choosing a Sender Type > Source: https://developers.telnyx.com/docs/messaging/getting-started/choosing-your-sender-type.md ## Interactive Product Selector Answer a few quick questions to get a personalized recommendation: This is a high-level recommendation. Contact [Telnyx Sales](https://telnyx.com/contact-us) for detailed guidance on complex use cases. --- ## Use Case Decision Tree Not sure which sender type fits? Start with your primary use case: **Best options:** - **Toll-free** — Fast provisioning (2–3 days), high throughput, handset-level delivery receipts. Ideal for US/CA transactional messaging. - **10DLC long code** — Good alternative if you want a local presence. Requires brand + campaign registration (2–3 business days). - **Short code** — Best for very high volume (200+ MPS). Longer provisioning (2–6 weeks) and higher cost. For OTP/2FA specifically, see our [Two-Factor Authentication guide](/docs/messaging/messages/2fa). **Best options:** - **10DLC long code** — Required for A2P marketing in the US. Register your brand and campaign through [10DLC registration](/docs/messaging/10dlc/quickstart). - **Toll-free** — Good for mixed marketing + transactional. Requires [toll-free verification](/docs/messaging/toll-free-verification). - **Short code** — Premium option for brand recognition and highest throughput. - **RCS** — Rich media cards, carousels, and suggested actions for supported devices. **Best options:** - **10DLC long code** — Local number feel, supports voice + SMS on the same number. - **Toll-free** — Works well for two-way if local presence isn't important. - **RCS** — Rich interactive experience with read receipts, typing indicators, and suggested replies. Alphanumeric sender IDs are **one-way only** — recipients cannot reply. **Best options:** - **Alphanumeric sender ID** — Supported in 100+ countries. No number procurement needed. Great for brand recognition internationally. - **Local long codes** — Required in some countries. Use the coverage checker below for availability. US toll-free and short code numbers only work for US/CA destinations. For international, use alphanumeric IDs or local numbers. **Best options:** - **RCS** — Full rich media support: images, video, carousels, suggested actions, branded sender profiles. - **MMS via long code/toll-free** — Image and video support for US/CA only. See our [RCS Getting Started guide](/docs/messaging/messages/rcs-getting-started) for details. --- ## Sender Comparison ### Capabilities at a Glance | | **10DLC Long Code** | **Toll-Free** | **Short Code** | **RCS** | **Alphanumeric** | | ----------------------- | ------------------- | ------------- | -------------- | ------- | ---------------- | | **Brand Recognition** | Local number | Brand | Brand | Brand (verified) | Brand name | | **Throughput** | 3–75 MPS\* | 3–150 MPS | 200+ MPS | 100+ MPS | 100+ MPS | | **Daily Volume Limits** | 10K–200K (T-Mobile)\*\* | Unlimited | Unlimited | Unlimited | Unlimited | | **Two-Way Messaging** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No | | **Voice Support** | ✅ Yes | ✅ Yes | ❌ No | ❌ No | ❌ No | | **Delivery Receipts** | Carrier only | Handset | Handset | Handset | Handset | | **MMS Support** | US/CA only | US/CA only | US/CA only | Rich media | ❌ No | | **Opt-Out Management** | Telnyx managed | Network managed | Telnyx managed | Telnyx managed | N/A | \* Throughput varies based on TCR Trust Score. \*\* T-Mobile daily limits based on TCR brand score; can be increased upon request. ### Registration & Cost Comparison Understanding the time and cost investment for each sender type helps you plan your launch: | | **10DLC Long Code** | **Toll-Free** | **Short Code** | **RCS** | **Alphanumeric** | | --- | --- | --- | --- | --- | --- | | **Provisioning Time** | 2–3 business days | 2–3 business days | 2–6 weeks | 6–10 weeks | Instant | | **Registration Required** | Brand + Campaign (TCR) | Toll-free verification | Carrier approval | Google verification | None | | **Number Procurement Cost** | Low (~$1/mo) | Low (~$2/mo) | High (~$500–1000/mo) | Agent setup fee | Free | | **Per-Message Cost** | Standard rates | Standard rates | Premium rates | Standard rates | Standard rates | | **Renewal/Ongoing** | Annual brand vetting | One-time verification | Monthly lease | Ongoing | None | Register your brand and campaign for US A2P messaging. Verify your toll-free number for higher throughput. Apply for a dedicated short code. Set up RCS business messaging with rich media. Send branded one-way messages internationally. Bring your existing numbers to Telnyx messaging. --- ## Check Coverage by Country Sender type availability varies by country. Use the tool below to check which options are available for your destination: ### Key Regional Considerations - **10DLC** is required for A2P messaging to US mobile numbers (enforced by carriers since 2023) - **Toll-free** numbers work for both US and CA - **Short codes** are country-specific (US short codes don't work in CA and vice versa) - **MMS** is supported on long code, toll-free, and short code - **RCS** is available for Android users - **Alphanumeric sender IDs** are widely supported and commonly used - Some countries require pre-registration of alphanumeric IDs (e.g., UK, France) - **Local long codes** may be required for two-way messaging - Short codes are available in select markets - GDPR compliance required for all messaging - **Alphanumeric sender IDs** supported in most countries - **Local long codes** recommended for better deliverability - Some carriers require pre-approved sender IDs or templates - WhatsApp is dominant — consider RCS as an alternative rich channel - Regulations vary significantly by country - **India** requires DLT registration and approved templates - **Australia** supports alphanumeric IDs and local numbers - Some countries require local entity for number procurement - Check coverage tool above for specific country details --- ## Quick Start: Send Your First Message Once you've chosen your sender type, sending a message uses the same API regardless of sender: ```bash cURL curl -X POST https://api.telnyx.com/v2/messages \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+15551234567", "to": "+15559876543", "text": "Hello from Telnyx!" }' ``` ```python Python import telnyx telnyx.api_key = "YOUR_API_KEY" message = telnyx.Message.create( from_="+15551234567", to="+15559876543", text="Hello from Telnyx!" ) print(message.id) ``` ```javascript Node.js const telnyx = require('telnyx')('YOUR_API_KEY'); const message = await telnyx.messages.create({ from: '+15551234567', to: '+15559876543', text: 'Hello from Telnyx!' }); console.log(message.data.id); ``` ```ruby Ruby require 'telnyx' Telnyx.api_key = 'YOUR_API_KEY' message = Telnyx::Message.create( from: '+15551234567', to: '+15559876543', text: 'Hello from Telnyx!' ) puts message.id ``` ```java Java import com.telnyx.sdk.*; import com.telnyx.sdk.api.MessagesApi; import com.telnyx.sdk.model.CreateMessageRequest; ApiClient client = Configuration.getDefaultApiClient(); client.setApiKey("YOUR_API_KEY"); MessagesApi api = new MessagesApi(client); CreateMessageRequest request = new CreateMessageRequest() .from("+15551234567") .to("+15559876543") .text("Hello from Telnyx!"); api.createMessage(request); ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey("YOUR_API_KEY"); var service = new MessagingSenderIdService(); var message = service.CreateMessage(new NewMessage { From = "+15551234567", To = "+15559876543", Text = "Hello from Telnyx!" }); Console.WriteLine(message.Id); ``` ```php PHP require 'vendor/autoload.php'; $telnyx = new \Telnyx\TelnyxClient('YOUR_API_KEY'); $message = $telnyx->messages->create([ 'from' => '+15551234567', 'to' => '+15559876543', 'text' => 'Hello from Telnyx!' ]); echo $message->id; ``` ```go Go package main import ( "context" "fmt" telnyx "github.com/telnyx/telnyx-go" ) func main() { client := telnyx.NewClient("YOUR_API_KEY") message, err := client.Messages.Create(context.Background(), &telnyx.MessageParams{ From: "+15551234567", To: "+15559876543", Text: "Hello from Telnyx!", }, ) if err != nil { panic(err) } fmt.Println(message.ID) } ``` The `from` field determines your sender type automatically: - **Phone number** (`+15551234567`) → Long code or toll-free - **Short code** (`12345`) → Short code - **Alphanumeric** (`"MyBrand"`) → Alphanumeric sender ID You can also use a [Messaging Profile](/docs/messaging/messages/configuration-and-usage) to let Telnyx select the best sender from your number pool. --- ## Next Steps Complete guide to sending your first SMS/MMS. Configure number pools, webhooks, and features. Understand throughput tiers and daily caps. --- ### Message Encoding > Source: https://developers.telnyx.com/docs/messaging/messages/message-encoding.md SMS messages are encoded into **segments** of 140 bytes each. You are billed per segment, so understanding encoding is key to controlling costs. The encoding determines how many characters fit in each segment: | Encoding | Bits per char | Single segment | Multi-part segment | |----------|--------------|----------------|-------------------| | GSM 7-bit | 7 | 160 chars | 153 chars | | ASCII 7-bit | 7 | 160 chars | 153 chars | | ASCII 8-bit | 8 | 140 chars | 134 chars | | UTF-16 | 16 | 70 chars | 67 chars | A single non-GSM-7 character (like an emoji or curly quote) switches the **entire message** to UTF-16, cutting capacity from 160 to 70 characters per segment. This can more than double your costs. ## Segment calculator Use this interactive tool to check how your message will be encoded and segmented: ## How segments work Every SMS message is transmitted in units of **140 bytes**. When a message exceeds one segment, a **6-byte header** (User Data Header, or UDH) is added to each segment for reassembly, reducing the usable space. ``` Single segment: 140 bytes available → 160 GSM-7 chars or 70 UTF-16 chars Multi-part: 134 bytes per segment → 153 GSM-7 chars or 67 UTF-16 chars Maximum: 10 segments per message ``` ### Segment calculation formula To calculate the number of segments for a message: ``` Characters ≤ 160 → 1 segment Characters > 160 → ⌈characters / 153⌉ segments Examples: - 100 chars = 1 segment - 160 chars = 1 segment - 161 chars = 2 segments (153 + 8) - 306 chars = 2 segments (153 + 153) - 307 chars = 3 segments (153 + 153 + 1) - 1530 chars = 10 segments (maximum) ``` ``` Characters ≤ 70 → 1 segment Characters > 70 → ⌈characters / 67⌉ segments Examples: - 50 chars = 1 segment - 70 chars = 1 segment - 71 chars = 2 segments (67 + 4) - 134 chars = 2 segments (67 + 67) - 135 chars = 3 segments (67 + 67 + 1) - 670 chars = 10 segments (maximum) ``` ### Cost impact example Consider a 200-character message: | Scenario | Encoding | Segments | Relative cost | |----------|----------|----------|---------------| | All GSM-7 characters | GSM-7 | 2 | 2× | | Contains one emoji 😀 | UTF-16 | 3 | 3× | | Contains one curly quote " | UTF-16 | 3 | 3× | | With [smart encoding](/docs/messaging/messages/smart-encoding/index) enabled | GSM-7 | 2 | 2× | Enable [smart encoding](/docs/messaging/messages/smart-encoding/index) to automatically replace common Unicode characters (like curly quotes and em dashes) with GSM-7 equivalents, reducing segment counts. --- ## Encoding by sender type | Sender type | Default encoding | Fallback | |-------------|-----------------|----------| | Long Code | GSM 7-bit | UTF-16 | | Toll-Free | GSM 7-bit | UTF-16 | | Short Code | ASCII 7-bit | UTF-16 | | Alphanumeric | GSM 7-bit | UTF-16 | If your message contains characters outside the default encoding's character set, the fallback encoding is used automatically for the entire message. MMS and RCS messages use **UTF-8** encoding by default and are not affected by these limits. --- ## GSM 7-bit character set Telnyx uses a GSM 7-bit encoding optimized for maximum carrier compatibility. Only characters in this set will keep your message in the efficient GSM-7 encoding. **Letters:** ``` A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z ``` **Digits:** ``` 0 1 2 3 4 5 6 7 8 9 ``` **Symbols and punctuation:** ``` ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ ``` **Special characters:** | Character | Description | |-----------|-------------| | `space` | Space | | `\n` | Line feed | | `\r` | Carriage return | | `_` | Underscore | | `£` | Pound sign | | `¥` | Yen sign | | `è` | e grave | | `é` | e acute | | `ù` | u grave | | `ì` | i grave | | `ò` | o grave | | `Ø` | O with stroke | | `ø` | o with stroke | | `Å` | A with ring | | `å` | a with ring | | `Æ` | AE ligature | | `æ` | ae ligature | | `ß` | Sharp s | | `É` | E acute | | `¡` | Inverted exclamation | | `Ä` | A umlaut | | `Ö` | O umlaut | | `Ñ` | N tilde | | `Ü` | U umlaut | | `§` | Section sign | | `¿` | Inverted question | | `ä` | a umlaut | | `ö` | o umlaut | | `ñ` | n tilde | | `ü` | u umlaut | | `à` | a grave | These characters require an escape sequence and count as **2 characters** in segment calculations: | Character | Description | Character count | |-----------|-------------|----------------| | `~` | Tilde | 2 | | `^` | Circumflex | 2 | | `\|` | Pipe / vertical bar | 2 | | `\` | Backslash | 2 | | `{` | Left curly bracket | 2 | | `}` | Right curly bracket | 2 | | `[` | Left square bracket | 2 | | `]` | Right square bracket | 2 | | `€` | Euro sign | 2 | Extended characters are easy to overlook when estimating segment counts. A message with 155 standard characters and 3 pipe characters (`|`) uses 155 + (3 × 2) = 161 character slots, requiring **2 segments** instead of 1. --- ## Detecting encoding in your application Before sending, you can check if a message will use GSM-7 or UTF-16 encoding to estimate costs. Here are helper functions for each language: ```python Python import re # GSM-7 basic character set GSM7_BASIC = set( "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ" " !\"#¤%&'()*+,-./0123456789:;<=>?" "¡ABCDEFGHIJKLMNOPQRSTUVWXYZ" "ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz" "äöñüà" ) GSM7_EXTENDED = set("^{}\\[~]|€") def calculate_segments(text: str) -> dict: """Calculate encoding and segment count for an SMS message.""" is_gsm7 = all(c in GSM7_BASIC or c in GSM7_EXTENDED for c in text) if is_gsm7: # Count extended chars as 2 char_count = sum(2 if c in GSM7_EXTENDED else 1 for c in text) if char_count <= 160: segments = 1 else: segments = -(-char_count // 153) # ceiling division return {"encoding": "GSM-7", "char_count": char_count, "segments": segments} else: # UTF-16: emojis count as 2 chars (surrogate pairs) char_count = 0 for c in text: char_count += 2 if ord(c) > 0xFFFF else 1 if char_count <= 70: segments = 1 else: segments = -(-char_count // 67) return {"encoding": "UTF-16", "char_count": char_count, "segments": segments} # Example usage result = calculate_segments("Hello, world!") print(f"Encoding: {result['encoding']}, Segments: {result['segments']}") # Output: Encoding: GSM-7, Segments: 1 result = calculate_segments("Hello 😀") print(f"Encoding: {result['encoding']}, Segments: {result['segments']}") # Output: Encoding: UTF-16, Segments: 1 ``` ```javascript Node const GSM7_BASIC = new Set( '@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ' + ' !"#¤%&\'()*+,-./0123456789:;<=>?' + '¡ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz' + 'äöñüà' ); const GSM7_EXTENDED = new Set('^{}\\[~]|€'); function calculateSegments(text) { const chars = [...text]; const isGsm7 = chars.every(c => GSM7_BASIC.has(c) || GSM7_EXTENDED.has(c)); if (isGsm7) { const charCount = chars.reduce( (sum, c) => sum + (GSM7_EXTENDED.has(c) ? 2 : 1), 0 ); const segments = charCount <= 160 ? 1 : Math.ceil(charCount / 153); return { encoding: 'GSM-7', charCount, segments }; } else { const charCount = chars.reduce( (sum, c) => sum + (c.codePointAt(0) > 0xFFFF ? 2 : 1), 0 ); const segments = charCount <= 70 ? 1 : Math.ceil(charCount / 67); return { encoding: 'UTF-16', charCount, segments }; } } // Example usage console.log(calculateSegments('Hello, world!')); // { encoding: 'GSM-7', charCount: 13, segments: 1 } console.log(calculateSegments('Hello 😀')); // { encoding: 'UTF-16', charCount: 8, segments: 1 } ``` ```ruby Ruby GSM7_BASIC = Set.new( "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ" \ " !\"#¤%&'()*+,-./0123456789:;<=>?" \ "¡ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz" \ "äöñüà" ).flat_map(&:chars).to_set GSM7_EXTENDED = Set.new("^{}\\[~]|€".chars) def calculate_segments(text) chars = text.chars is_gsm7 = chars.all? { |c| GSM7_BASIC.include?(c) || GSM7_EXTENDED.include?(c) } if is_gsm7 char_count = chars.sum { |c| GSM7_EXTENDED.include?(c) ? 2 : 1 } segments = char_count <= 160 ? 1 : (char_count.to_f / 153).ceil { encoding: "GSM-7", char_count: char_count, segments: segments } else char_count = chars.sum { |c| c.ord > 0xFFFF ? 2 : 1 } segments = char_count <= 70 ? 1 : (char_count.to_f / 67).ceil { encoding: "UTF-16", char_count: char_count, segments: segments } end end puts calculate_segments("Hello, world!") # {:encoding=>"GSM-7", :char_count=>13, :segments=>1} ``` ```go Go package main import ( "fmt" "math" "unicode/utf8" ) var gsm7Basic = map[rune]bool{ '@': true, '£': true, '$': true, '¥': true, 'è': true, 'é': true, 'ù': true, 'ì': true, 'ò': true, 'Ç': true, '\n': true, 'Ø': true, 'ø': true, '\r': true, 'Å': true, 'å': true, 'Δ': true, '_': true, 'Φ': true, 'Γ': true, 'Λ': true, 'Ω': true, 'Π': true, 'Ψ': true, 'Σ': true, 'Θ': true, 'Ξ': true, ' ': true, 'Æ': true, 'æ': true, 'ß': true, 'É': true, '¡': true, 'Ä': true, 'Ö': true, 'Ñ': true, 'Ü': true, '§': true, '¿': true, 'ä': true, 'ö': true, 'ñ': true, 'ü': true, 'à': true, } var gsm7Extended = map[rune]bool{ '^': true, '{': true, '}': true, '\\': true, '[': true, '~': true, ']': true, '|': true, '€': true, } func init() { // Add printable ASCII for c := '!'; c <= '~'; c++ { gsm7Basic[c] = true } } type SegmentResult struct { Encoding string CharCount int Segments int } func CalculateSegments(text string) SegmentResult { isGSM7 := true for _, r := range text { if !gsm7Basic[r] && !gsm7Extended[r] { isGSM7 = false break } } if isGSM7 { charCount := 0 for _, r := range text { if gsm7Extended[r] { charCount += 2 } else { charCount++ } } segments := 1 if charCount > 160 { segments = int(math.Ceil(float64(charCount) / 153)) } return SegmentResult{"GSM-7", charCount, segments} } charCount := 0 for _, r := range text { if r > 0xFFFF { charCount += 2 } else { charCount++ } } _ = utf8.RuneCountInString(text) // ensure valid UTF-8 segments := 1 if charCount > 70 { segments = int(math.Ceil(float64(charCount) / 67)) } return SegmentResult{"UTF-16", charCount, segments} } func main() { fmt.Println(CalculateSegments("Hello, world!")) // {GSM-7 13 1} } ``` ```java Java import java.util.Set; public class SmsEncoding { private static final Set GSM7_EXTENDED = Set.of('^', '{', '}', '\\', '[', '~', ']', '|', '€'); private static final String GSM7_CHARS = "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ" + " !\"#¤%&'()*+,-./0123456789:;<=>?" + "¡ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz" + "äöñüà^{}\\[~]|€"; private static final Set GSM7_BASIC = new java.util.HashSet<>(); static { for (char c : GSM7_CHARS.toCharArray()) { GSM7_BASIC.add(c); } } public record SegmentResult(String encoding, int charCount, int segments) {} public static SegmentResult calculateSegments(String text) { boolean isGsm7 = text.chars() .mapToObj(c -> (char) c) .allMatch(c -> GSM7_BASIC.contains(c) || GSM7_EXTENDED.contains(c)); if (isGsm7) { int charCount = text.chars() .map(c -> GSM7_EXTENDED.contains((char) c) ? 2 : 1) .sum(); int segments = charCount <= 160 ? 1 : (int) Math.ceil(charCount / 153.0); return new SegmentResult("GSM-7", charCount, segments); } else { int charCount = text.codePoints() .map(cp -> cp > 0xFFFF ? 2 : 1) .sum(); int segments = charCount <= 70 ? 1 : (int) Math.ceil(charCount / 67.0); return new SegmentResult("UTF-16", charCount, segments); } } public static void main(String[] args) { System.out.println(calculateSegments("Hello, world!")); // SegmentResult[encoding=GSM-7, charCount=13, segments=1] } } ``` ```csharp .NET using System; using System.Collections.Generic; using System.Linq; public record SegmentResult(string Encoding, int CharCount, int Segments); public static class SmsEncoding { private static readonly HashSet Gsm7Extended = new("^{}\\[~]|€"); private static readonly HashSet Gsm7Basic = new( "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ" + " !\"#¤%&'()*+,-./0123456789:;<=>?" + "¡ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz" + "äöñüà" ); public static SegmentResult CalculateSegments(string text) { bool isGsm7 = text.All(c => Gsm7Basic.Contains(c) || Gsm7Extended.Contains(c)); if (isGsm7) { int charCount = text.Sum(c => Gsm7Extended.Contains(c) ? 2 : 1); int segments = charCount <= 160 ? 1 : (int)Math.Ceiling(charCount / 153.0); return new SegmentResult("GSM-7", charCount, segments); } else { int charCount = 0; for (int i = 0; i < text.Length; i++) { charCount += char.IsHighSurrogate(text[i]) ? 2 : 1; if (char.IsHighSurrogate(text[i])) i++; // skip low surrogate } int segments = charCount <= 70 ? 1 : (int)Math.Ceiling(charCount / 67.0); return new SegmentResult("UTF-16", charCount, segments); } } } // Usage: // var result = SmsEncoding.CalculateSegments("Hello, world!"); // Console.WriteLine($"{result.Encoding}, {result.Segments} segment(s)"); ``` ```php PHP ?" . "¡ABCDEFGHIJKLMNOPQRSTUVWXYZ" . "ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz" . "äöñüà" ); $basicSet = array_flip($gsm7Basic); $extSet = array_flip($gsm7Extended); $chars = mb_str_split($text); $isGsm7 = true; foreach ($chars as $c) { if (!isset($basicSet[$c]) && !isset($extSet[$c])) { $isGsm7 = false; break; } } if ($isGsm7) { $charCount = array_sum(array_map( fn($c) => isset($extSet[$c]) ? 2 : 1, $chars )); $segments = $charCount <= 160 ? 1 : (int) ceil($charCount / 153); return ['encoding' => 'GSM-7', 'char_count' => $charCount, 'segments' => $segments]; } $charCount = 0; foreach ($chars as $c) { $charCount += mb_ord($c) > 0xFFFF ? 2 : 1; } $segments = $charCount <= 70 ? 1 : (int) ceil($charCount / 67); return ['encoding' => 'UTF-16', 'char_count' => $charCount, 'segments' => $segments]; } // Usage $result = calculateSegments("Hello, world!"); echo "{$result['encoding']}, {$result['segments']} segment(s)\n"; // GSM-7, 1 segment(s) ``` --- ## Common encoding issues **Symptom:** Your message uses more segments than expected. **Cause:** A non-GSM-7 character is present, forcing the entire message to UTF-16. Common culprits: | Character | Source | GSM-7? | |-----------|--------|--------| | `"` `"` (curly quotes) | Word processors, mobile keyboards | ❌ | | `'` `'` (curly apostrophes) | Auto-correct, CMS platforms | ❌ | | `—` (em dash) | Word processors | ❌ | | `…` (ellipsis) | Mobile keyboards | ❌ | | `€` (euro sign) | Manual entry | ✅ (extended, costs 2 chars) | **Fix:** 1. Enable [smart encoding](/docs/messaging/messages/smart-encoding/index) to auto-replace these characters 2. Or manually replace them with GSM-7 equivalents before sending **Symptom:** Adding a single emoji doubles or triples the number of segments. **Cause:** Emojis force UTF-16 encoding (70 chars/segment instead of 160). Additionally, most emojis use **surrogate pairs** and count as 2 UTF-16 characters. **Example:** ``` "Thanks for your order!" → GSM-7, 1 segment (22 chars) "Thanks for your order! 🎉" → UTF-16, 1 segment (25 chars) "Thanks for your order! ... 🎉" → UTF-16, 2 segments (71+ chars) ``` **Fix:** If cost is a concern, avoid emojis in SMS. Use emojis freely in MMS/RCS where encoding isn't a factor. **Symptom:** A 155-character message that looks like it should fit in one segment actually requires two. **Cause:** Characters like `[`, `]`, `{`, `}`, `|`, `\`, `^`, `~`, and `€` are in the GSM-7 extended set and count as **2 characters** each. **Example:** ``` "Price: $100 [USD]" → 18 visible chars but 20 GSM-7 chars ([ and ] each cost 2) ``` **Fix:** Account for extended characters when calculating message length. Use the segment calculator above or the SDK helpers in this guide. **Symptom:** Text that looks like normal ASCII actually contains Unicode characters. **Cause:** Word processors often replace straight quotes with curly quotes, hyphens with em dashes, and three periods with an ellipsis character. These are invisible differences that force UTF-16. **Fix:** 1. Enable [smart encoding](/docs/messaging/messages/smart-encoding/index) — this handles the most common substitutions automatically 2. Sanitize text before sending by replacing known problem characters 3. Use the `encoding` parameter set to `gsm7` to get a `400` error if non-GSM-7 characters are present (fail-fast approach) **Symptom:** The recipient sees a message split in unexpected places, or parts arrive out of order. **Cause:** Multi-part messages are reassembled by the recipient's device using the UDH (User Data Header). Some older devices or carriers may not support reassembly for messages over a certain number of segments. **Fix:** - Keep messages under 3-4 segments for maximum compatibility - Telnyx supports up to 10 segments, but recipient device support varies - Consider using MMS for longer content **Symptom:** Messages in non-Latin scripts use significantly more segments than English messages of similar visible length. **Cause:** Non-Latin characters have no GSM-7 equivalents, so the entire message uses UTF-16 encoding (70 characters per segment). Smart encoding cannot help here. **Fix:** - This is expected behavior — plan for higher segment counts when messaging in non-Latin scripts - Keep messages concise - Consider MMS for longer non-Latin content --- ## Best practices Turn on [smart encoding](/docs/messaging/messages/smart-encoding/index) on your messaging profile to automatically handle Unicode-to-GSM-7 substitutions. This is the single biggest cost-saving measure. Use the encoding detection helpers above to check segment counts before sending. Alert your application when messages will be unexpectedly expensive. If you accept user-generated content, sanitize it before sending. Strip or replace invisible Unicode characters, curly quotes, and other common problem characters. Stay under 160 characters (GSM-7) or 70 characters (UTF-16) to avoid multi-part message overhead. Each additional segment adds 7 characters of UDH overhead. For messages that need emojis, rich formatting, or non-Latin scripts, consider [MMS](/docs/messaging/messages/configuration-and-usage/index) or [RCS](/docs/messaging/messages/send-an-rcs-message/index) instead of SMS. --- ## Related resources Automatically replace Unicode characters with GSM-7 equivalents to reduce costs. Get started with the Telnyx Messaging API. API reference for sending messages with encoding options. Configure smart encoding and other profile settings. --- ### Rate Limiting > Source: https://developers.telnyx.com/docs/messaging/messages/rate-limiting.md This guide covers message delivery throughput. For API request limits, see [API Rate Limiting](/development/api-fundamentals/reliability/rate-limiting). ## Rate Limits The following are the default rate limits applied by Telnyx for each message type and sender type. ### Account | Message Type | Default Rate Limit | Max Queue Length | |--------------|-------------------|------------------| | SMS | 50 messages/second | 720,000 | | MMS | 15 messages/second | 216,000 | | RCS | 1 message/second | 14,400 | ### Sender | Sender Type | Rate Limit | Per | Max Queue Length | |-------------|------------|-----|------------------| | Long Code | 0.1 MPS | Number | 1,440 | | Toll-Free | 20 MPS | Number | 288,000 | | Short Code | 1,000 MPS | Number | 14,400,000 | | Alphanumeric | 0.1 MPS | Sender ID | 1,440 | The default Long Code rate limit applies to non-US destinations. For US destinations, throughput is determined at the campaign level based on your 10DLC registration. See [10DLC](#10dlc) for carrier-specific limits. If you need an increased rate limit, contact [Telnyx sales](mailto:sales@telnyx.com) to discuss your options. ### 10DLC When using US long codes for A2P messaging, throughput is determined by mobile network operators (MNOs) based on your registered 10DLC campaign. Each carrier has different throughput systems. AT&T assigns throughput per campaign based on "Message Class," determined by use case type and vetting score. | Message Class | Use Case Type | Vetting Score | SMS TPM | MMS TPM | |---------------|---------------|---------------|---------|---------| | A | Standard (Dedicated) | 75-100 | 4,500 | 2,400 | | B | Standard (Mixed/Marketing) | 75-100 | 4,500 | 2,400 | | C | Standard (Dedicated) | 50-74 | 2,400 | 1,200 | | D | Standard (Mixed/Marketing) | 50-74 | 2,400 | 1,200 | | E | Standard (Dedicated) | 1-49 | 240 | 150 | | F | Standard (Mixed/Marketing) | 1-49 | 240 | 150 | | T | Low Volume Mixed | - | 75 | 50 | | K | Political | - | 4,500 | 2,400 | | P | Charity | - | 2,400 | 1,200 | | S | Social | - | 9,000 | 2,400 | | X | Emergency / Public Safety | - | 4,500 | 2,400 | | W | Sole Proprietor | - | 15 | 50 | | G | Proxy | - | 60/number | 50/number | | N | Agents and Franchises | - | 60/number | 50/number | TPM = Throughput Per Minute. For standard use cases, the vetting score from your 10DLC brand registration determines which message class (and throughput) your campaign receives. Special use cases have fixed throughput regardless of vetting score. T-Mobile assigns daily message caps at the brand level, shared across all campaigns under that brand. | Brand Tier | Vetting Score | Daily Cap | |------------|---------------|-----------| | Top | 75-100 | 200,000 | | High Mid | 50-74 | 40,000 | | Low Mid | 25-49 | 10,000 | | Low | 1-24 | 2,000 | Unvetted brands default to Low tier unless listed on the Russell 3000. Sole Proprietor campaigns have a 1,000 daily cap. Verizon has not published specific throughput limits but uses content filtering for 10DLC traffic. --- ## Queuing When you send messages faster than your rate limit allows, excess messages are automatically queued for delivery. ### How Queuing Works ```mermaid flowchart LR A[API Request] --> B{Rate LimitCheck} B -->|Under limit| C[Send to Carrier] B -->|Over limit| D[Queue] D --> E{Queue Full?} E -->|No| F[Wait forRate Window] E -->|Yes| G[Error 40318] F --> C C --> H[MDR Search] ``` 1. **Message submitted** — Request validated against your Messaging Profile 2. **Rate limit check** — Under limit: sent immediately. Over limit: queued 3. **Queue processing** — Messages held up to 4 hours, released in FIFO order 4. **Delivery** — Sent to carrier, webhook fired, visible in MDR search ### Calculating Queue Size Each sender type and message type combination has its own queue. The maximum queue length is: ``` Max Queue Length = Rate Limit (MPS) × 14,400 seconds (4 hours) ``` The following examples illustrate how sender and account queues interact: Acme Corp sends SMS from a single Toll-Free number. Their application submits messages at 50 MPS, but the Toll-Free rate limit is 20 MPS. | Queue | Rate Limit | Max Queue Length | |-------|------------|------------------| | Toll-Free #1 | 20 MPS | 288,000 segments | Messages are delivered at 20 MPS, but 30 MPS (50 - 20) accumulates in the queue. After 4 hours of sustained sending, the queue reaches its 288,000 segment limit. Any additional messages return error `40318` (queue full). Acme Corp sends SMS from 5 Toll-Free numbers simultaneously, each at 20 MPS. | Queue | Rate Limit | Max Queue Length | |-------|------------|------------------| | Toll-Free #1 | 20 MPS | 288,000 segments | | Toll-Free #2 | 20 MPS | 288,000 segments | | Toll-Free #3 | 20 MPS | 288,000 segments | | Toll-Free #4 | 20 MPS | 288,000 segments | | Toll-Free #5 | 20 MPS | 288,000 segments | | **Account SMS** | **50 MPS** | **720,000 segments** | Combined sender capacity is 100 MPS (5 × 20), but the account limit is 50 MPS. Messages exceeding the account limit queue at the account level. Once the account queue (720,000) fills, additional messages return error `40318`. Acme Corp sends SMS from 10 Long Code numbers simultaneously, each at 0.1 MPS. | Queue | Rate Limit | Max Queue Length | |-------|------------|------------------| | Long Codes (10 total) | 1 MPS combined | 14,400 segments each | | **Account SMS** | **50 MPS** | **720,000 segments** | Here, the sender limit (1 MPS combined) is well below the account limit (50 MPS). The sender queues will fill first. Each Long Code queue holds 1,440 segments — once full, messages to that specific number return error `40318`, even though the account has capacity. When a queue is full, additional messages return error code `40318`. See [API Errors](/development/api-fundamentals/api-errors) for details. ### Monitoring Queued Messages Queued messages return a `queued` status and won't appear in MDR search until delivered. Monitor queue depth via the [Mission Control Portal](https://portal.telnyx.com/#/reports/messaging-deliverability). To avoid queue buildup, implement client-side rate limiting to match your throughput limits. See [Client-Side Rate Limiting](#client-side-rate-limiting) below. --- ## Client-Side Rate Limiting Implementing rate limiting in your application prevents queue buildup, avoids `40318` errors, and gives you control over message pacing. The examples below show a token bucket rate limiter that works for any sender type. ```python Python import time import threading import os try: from telnyx import Telnyx except ImportError: Telnyx = None class RateLimiter: """Token bucket rate limiter for SMS sending.""" def __init__(self, rate: float, burst: int | None = None): """ Args: rate: Messages per second (e.g., 0.1 for long code, 20 for toll-free). burst: Max burst size. Defaults to rate (no bursting). """ self.rate = rate self.burst = burst or max(1, int(rate)) self.tokens = self.burst self.last_refill = time.monotonic() self.lock = threading.Lock() def acquire(self, timeout: float = 30.0) -> bool: """Wait until a token is available. Returns False on timeout.""" deadline = time.monotonic() + timeout while True: with self.lock: self._refill() if self.tokens >= 1: self.tokens -= 1 return True wait_time = min(1.0 / self.rate, deadline - time.monotonic()) if wait_time <= 0: return False time.sleep(wait_time) def _refill(self): now = time.monotonic() elapsed = now - self.last_refill self.tokens = min(self.burst, self.tokens + elapsed * self.rate) self.last_refill = now # Usage: Toll-Free at 20 MPS limiter = RateLimiter(rate=20) if Telnyx: client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) recipients = ["+15551234567", "+15559876543"] # your recipient list for to_number in recipients: if not limiter.acquire(timeout=60): print(f"Rate limit timeout sending to {to_number}") continue if Telnyx: response = client.messages.send( from_="+15550001111", to=to_number, text="Hello from Telnyx!", ) print(f"Sent to {to_number}: {response.data.id}") ``` ```javascript Node const Telnyx = require('telnyx'); class RateLimiter { /** * Token bucket rate limiter. * @param {number} rate - Messages per second * @param {number} [burst] - Max burst size */ constructor(rate, burst) { this.rate = rate; this.burst = burst || Math.max(1, Math.floor(rate)); this.tokens = this.burst; this.lastRefill = Date.now(); } async acquire(timeoutMs = 30000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { this._refill(); if (this.tokens >= 1) { this.tokens -= 1; return true; } const waitMs = Math.min(1000 / this.rate, deadline - Date.now()); if (waitMs <= 0) break; await new Promise(r => setTimeout(r, waitMs)); } return false; } _refill() { const now = Date.now(); const elapsed = (now - this.lastRefill) / 1000; this.tokens = Math.min(this.burst, this.tokens + elapsed * this.rate); this.lastRefill = now; } } // Usage: Toll-Free at 20 MPS const limiter = new RateLimiter(20); const client = new Telnyx(process.env.TELNYX_API_KEY); const recipients = ['+15551234567', '+15559876543']; (async () => { for (const to of recipients) { if (!(await limiter.acquire(60000))) { console.log(`Rate limit timeout sending to ${to}`); continue; } try { const response = await client.messages.send({ from: '+15550001111', to, text: 'Hello from Telnyx!', }); console.log(`Sent to ${to}: ${response.data.id}`); } catch (err) { console.error(`Error sending to ${to}:`, err.message); } } })(); ``` ```ruby Ruby require "telnyx" class RateLimiter def initialize(rate, burst: nil) @rate = rate.to_f @burst = burst || [@rate.ceil, 1].max @tokens = @burst.to_f @last_refill = Process.clock_gettime(Process::CLOCK_MONOTONIC) @mutex = Mutex.new end def acquire(timeout: 30) deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout loop do @mutex.synchronize do refill if @tokens >= 1 @tokens -= 1 return true end end wait = [1.0 / @rate, deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min return false if wait <= 0 sleep(wait) end end private def refill now = Process.clock_gettime(Process::CLOCK_MONOTONIC) elapsed = now - @last_refill @tokens = [@burst, @tokens + elapsed * @rate].min @last_refill = now end end # Usage: Toll-Free at 20 MPS limiter = RateLimiter.new(20) Telnyx.api_key = ENV["TELNYX_API_KEY"] recipients = ["+15551234567", "+15559876543"] recipients.each do |to| unless limiter.acquire(timeout: 60) puts "Rate limit timeout sending to #{to}" next end begin response = Telnyx::Message.create( from: "+15550001111", to: to, text: "Hello from Telnyx!" ) puts "Sent to #{to}: #{response.id}" rescue => e puts "Error sending to #{to}: #{e.message}" end end ``` ```go Go package main import ( "context" "fmt" "os" "sync" "time" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) // RateLimiter implements a token bucket algorithm. type RateLimiter struct { rate float64 burst float64 tokens float64 lastRefill time.Time mu sync.Mutex } // NewRateLimiter creates a rate limiter. // rate is messages per second (e.g., 20 for toll-free). func NewRateLimiter(rate float64) *RateLimiter { burst := rate if burst < 1 { burst = 1 } return &RateLimiter{ rate: rate, burst: burst, tokens: burst, lastRefill: time.Now(), } } // Acquire blocks until a token is available or the timeout expires. func (r *RateLimiter) Acquire(timeout time.Duration) bool { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { r.mu.Lock() r.refill() if r.tokens >= 1 { r.tokens-- r.mu.Unlock() return true } r.mu.Unlock() wait := time.Duration(float64(time.Second) / r.rate) remaining := time.Until(deadline) if remaining < wait { wait = remaining } if wait <= 0 { return false } time.Sleep(wait) } return false } func (r *RateLimiter) refill() { now := time.Now() elapsed := now.Sub(r.lastRefill).Seconds() r.tokens = min(r.burst, r.tokens+elapsed*r.rate) r.lastRefill = now } func min(a, b float64) float64 { if a < b { return a } return b } func main() { limiter := NewRateLimiter(20) // Toll-Free at 20 MPS client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) recipients := []string{"+15551234567", "+15559876543"} for _, to := range recipients { if !limiter.Acquire(60 * time.Second) { fmt.Printf("Rate limit timeout sending to %s\n", to) continue } resp, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "+15550001111", To: to, Text: "Hello from Telnyx!", }) if err != nil { fmt.Printf("Error sending to %s: %v\n", to, err) continue } fmt.Printf("Sent to %s: %s\n", to, resp.Data.ID) } } ``` ```java Java import java.util.List; import java.util.concurrent.locks.ReentrantLock; public class RateLimitedSender { static class RateLimiter { private final double rate; private final double burst; private double tokens; private long lastRefillNanos; private final ReentrantLock lock = new ReentrantLock(); public RateLimiter(double rate) { this.rate = rate; this.burst = Math.max(1, rate); this.tokens = this.burst; this.lastRefillNanos = System.nanoTime(); } public boolean acquire(long timeoutMs) throws InterruptedException { long deadlineNanos = System.nanoTime() + timeoutMs * 1_000_000L; while (System.nanoTime() < deadlineNanos) { lock.lock(); try { refill(); if (tokens >= 1) { tokens--; return true; } } finally { lock.unlock(); } long waitMs = Math.min( (long) (1000.0 / rate), (deadlineNanos - System.nanoTime()) / 1_000_000L ); if (waitMs <= 0) return false; Thread.sleep(waitMs); } return false; } private void refill() { long now = System.nanoTime(); double elapsed = (now - lastRefillNanos) / 1e9; tokens = Math.min(burst, tokens + elapsed * rate); lastRefillNanos = now; } } public static void main(String[] args) throws Exception { RateLimiter limiter = new RateLimiter(20); // Toll-Free at 20 MPS // Use with Telnyx Java SDK: // TelnyxClient client = TelnyxOkHttpClient.fromEnv(); List recipients = List.of("+15551234567", "+15559876543"); for (String to : recipients) { if (!limiter.acquire(60_000)) { System.out.println("Rate limit timeout: " + to); continue; } // MessageSendParams params = MessageSendParams.builder() // .from("+15550001111") // .to(to) // .text("Hello from Telnyx!") // .build(); // var response = client.messages().send(params); System.out.println("Sent to " + to); } } } ``` ```csharp .NET using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; public class RateLimiter { private readonly double _rate; private readonly double _burst; private double _tokens; private DateTime _lastRefill; private readonly object _lock = new(); public RateLimiter(double rate, double? burst = null) { _rate = rate; _burst = burst ?? Math.Max(1, rate); _tokens = _burst; _lastRefill = DateTime.UtcNow; } public async Task AcquireAsync(TimeSpan timeout) { var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { lock (_lock) { Refill(); if (_tokens >= 1) { _tokens--; return true; } } var waitMs = Math.Min(1000.0 / _rate, (deadline - DateTime.UtcNow).TotalMilliseconds); if (waitMs <= 0) return false; await Task.Delay(TimeSpan.FromMilliseconds(waitMs)); } return false; } private void Refill() { var now = DateTime.UtcNow; var elapsed = (now - _lastRefill).TotalSeconds; _tokens = Math.Min(_burst, _tokens + elapsed * _rate); _lastRefill = now; } } // Usage: Toll-Free at 20 MPS // var limiter = new RateLimiter(20); // TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); // var service = new MessageService(); // // foreach (var to in recipients) // { // if (!await limiter.AcquireAsync(TimeSpan.FromSeconds(60))) // { // Console.WriteLine($"Timeout: {to}"); // continue; // } // var response = await service.SendAsync(new MessageSendOptions // { // From = "+15550001111", To = to, Text = "Hello from Telnyx!" // }); // Console.WriteLine($"Sent to {to}: {response.Data.Id}"); // } ``` ```php PHP rate = $rate; $this->burst = $burst ?? max(1, $rate); $this->tokens = $this->burst; $this->lastRefill = hrtime(true) / 1e9; } public function acquire(float $timeout = 30.0): bool { $deadline = hrtime(true) / 1e9 + $timeout; while (hrtime(true) / 1e9 < $deadline) { $this->refill(); if ($this->tokens >= 1) { $this->tokens--; return true; } $wait = min(1.0 / $this->rate, $deadline - hrtime(true) / 1e9); if ($wait <= 0) { return false; } usleep((int) ($wait * 1e6)); } return false; } private function refill(): void { $now = hrtime(true) / 1e9; $elapsed = $now - $this->lastRefill; $this->tokens = min($this->burst, $this->tokens + $elapsed * $this->rate); $this->lastRefill = $now; } } // Usage: Toll-Free at 20 MPS $limiter = new RateLimiter(20); \Telnyx\Telnyx::setApiKey(getenv('TELNYX_API_KEY')); $recipients = ['+15551234567', '+15559876543']; foreach ($recipients as $to) { if (!$limiter->acquire(60)) { echo "Rate limit timeout: {$to}\n"; continue; } try { $response = \Telnyx\Message::Create([ 'from' => '+15550001111', 'to' => $to, 'text' => 'Hello from Telnyx!', ]); echo "Sent to {$to}: {$response->id}\n"; } catch (\Exception $e) { echo "Error: {$e->getMessage()}\n"; } } ``` ### Adapting for your sender type Change the `rate` parameter to match your sender type: | Sender Type | Rate Parameter | Example | |-------------|---------------|---------| | Long Code | `0.1` | `RateLimiter(0.1)` | | Toll-Free | `20` | `RateLimiter(20)` | | Short Code | `1000` | `RateLimiter(1000)` | | Alphanumeric | `0.1` | `RateLimiter(0.1)` | For [Number Pool](/docs/messaging/messages/number-pool/index) configurations, the effective rate is the per-number limit multiplied by the number of numbers in the pool. For example, 10 Long Codes at 0.1 MPS each gives an effective 1 MPS pool rate. --- ## Handling Rate Limit Errors When your sending rate exceeds limits, the API returns specific error codes. Handle them gracefully with retry logic: ```python Python import time import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) def send_with_retry(to: str, from_: str, text: str, max_retries: int = 3): """Send a message with exponential backoff on rate limit errors.""" for attempt in range(max_retries + 1): try: response = client.messages.send(from_=from_, to=to, text=text) return response except Exception as e: error_code = getattr(e, "code", None) if error_code == "40318": # Queue full wait = min(2**attempt, 30) print(f"Queue full, retrying in {wait}s (attempt {attempt + 1})") time.sleep(wait) else: raise raise Exception(f"Failed to send to {to} after {max_retries} retries") ``` ```javascript Node const Telnyx = require('telnyx'); const client = new Telnyx(process.env.TELNYX_API_KEY); async function sendWithRetry(to, from, text, maxRetries = 3) { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await client.messages.send({ from, to, text }); } catch (err) { if (err.rawType === '40318') { const wait = Math.min(2 ** attempt * 1000, 30000); console.log(`Queue full, retrying in ${wait}ms (attempt ${attempt + 1})`); await new Promise(r => setTimeout(r, wait)); } else { throw err; } } } throw new Error(`Failed to send to ${to} after ${maxRetries} retries`); } ``` --- ## Related Resources HTTP API request rate limits (separate from message throughput). Carrier-specific throughput for 10DLC campaigns. Distribute messages across multiple numbers for higher throughput. Monitor delivery status and diagnose throughput issues. --- ### Error Codes > Source: https://developers.telnyx.com/docs/messaging/messages/error-codes.md Complete reference for errors returned by the Telnyx Messaging API. Errors fall into three categories: **API request errors** (returned immediately), **delivery errors** (reported via webhooks), and **configuration errors** (number/profile issues). For general Telnyx API errors (authentication, rate limiting, etc.), see the [API Error Codes](/development/api-fundamentals/api-errors/index) reference. --- ## Delivery errors (40xxx) These errors occur after the message is accepted by the API but fails during delivery. They appear in [message detail records](/docs/messaging/messages/message-detail-records/index) and `message.finalized` webhooks. ### Carrier rejections | Code | Error | Cause | Action | |------|-------|-------|--------| | `40001` | Not routable | Destination is a landline or non-routable number | Verify the recipient can receive SMS/MMS | | `40002` | Blocked as spam (temporary) | Carrier spam filter triggered | Reduce sending rate; review message content for spam patterns | | `40003` | Blocked as spam (permanent) | Sending number permanently blocked by carrier | Use a different sending number; contact support for appeal | | `40004` | Rejected by destination | Recipient carrier rejecting for unknown reason | Retry after delay; contact support if persistent | | `40005` | Message expired | Message TTL expired before delivery | Increase validity period or check carrier delays | | `40006` | Recipient unavailable | Recipient's carrier server is down | Retry with exponential backoff | | `40008` | Undeliverable | Carrier did not accept the message | Check number validity; try alternate route | | `40009` | Invalid message body | Message content rejected | Check for invalid characters or encoding issues; see [Message Encoding](/docs/messaging/messages/message-encoding/index) | | `40011` | Rate limit exceeded (upstream) | Exceeded carrier throughput limit | Reduce sending rate; see [Rate Limiting](/docs/messaging/messages/rate-limiting/index) | | `40012` | Invalid destination number | Carrier rejected the destination number | Verify E.164 format and number validity | | `40013` | Invalid source number | Carrier rejected the sending number | Check that your number is active and messaging-enabled | | `40014` | Expired in queue | Message sat in queue past validity period | Check for throughput bottlenecks; increase sending capacity | | `40015` | Internal spam filter | Telnyx internal spam filter triggered | Review message content; contact support if false positive | ### 10DLC-specific errors | Code | Error | Cause | Action | |------|-------|-------|--------| | `40010` | Not 10DLC registered | Sending number lacks 10DLC campaign registration | [Register for 10DLC](/docs/messaging/10dlc/quickstart/index) | | `40016` | T-Mobile sending limit | Exceeded T-Mobile throughput for this campaign | Reduce rate or improve brand vetting score for higher limits | | `40017` | AT&T spam rejection | AT&T flagged message as spam | Review content; avoid URL shorteners and spam-like patterns | | `40018` | AT&T sending limit | Exceeded AT&T throughput for this campaign | Reduce rate or improve brand vetting score | | `40019` | AT&T invalid tag data | Incorrect 10DLC tagging information | Verify campaign and number assignment; [troubleshoot 10DLC](/docs/messaging/10dlc/troubleshooting/index) | | `40020` | Artificial traffic inflation | 2FA traffic blocked for 24 hours | Wait 24 hours; review for fraud patterns on your numbers | ### Toll-free errors | Code | Error | Cause | Action | |------|-------|-------|--------| | `40329` | Toll-free not verified | Number hasn't passed toll-free verification | Complete [toll-free verification](/docs/messaging/toll-free-verification) | | `40330` | Toll-free not provisioned | Number not fully provisioned for messaging | Wait for provisioning to complete; contact support | --- ## API request errors (403xx) These errors are returned immediately in the API response when the message request is invalid. ### Sender/recipient errors | Code | Error | Cause | Action | |------|-------|-------|--------| | `40300` | Blocked (STOP) | Recipient sent STOP — opt-out in effect | Do not retry; wait for recipient to opt back in | | `40301` | Unsupported message type | Cannot send between these number types | Check source and destination number capabilities | | `40305` | Invalid 'from' address | Sending number not associated with messaging profile | Assign the number to your messaging profile | | `40306` | Alpha sender not configured | Alphanumeric sender ID not set on profile | Configure alpha sender on your [messaging profile](https://portal.telnyx.com/#/app/messaging) | | `40307` | Alpha sender mismatch | From address doesn't match configured alpha sender | Use the exact alpha sender configured on the profile | | `40308` | Invalid 'from' for MMS | MMS requires US long code or short code | Use an MMS-capable number | | `40309` | Invalid destination region | Destination country not in profile whitelist | Add the destination region to your messaging profile | | `40310` | Invalid 'to' address | Destination number is invalid | Verify E.164 format: `+[country code][number]` | | `40319` | Incompatible message type | Source and destination types incompatible | Check number type compatibility | | `40320` | Temporarily unusable sender | Sending number in pending/transitional state | Wait for number provisioning to complete | | `40321` | No usable numbers in pool | Number pool empty or all numbers unhealthy | Add numbers to pool or check number health | | `40325` | Invalid alpha sender ID | Alphanumeric sender ID format is invalid | Use 1–11 alphanumeric characters | ### Content errors | Code | Error | Cause | Action | |------|-------|-------|--------| | `40302` | Message too large | SMS exceeds maximum segment count | Shorten message or send as MMS | | `40304` | Invalid content combination | Mixed SMS/MMS content parameters | Use `text` for SMS; `media_urls` (with optional `text`) for MMS | | `40316` | No content | Message has no text or media | Include `text` and/or `media_urls` | | `40317` | Invalid MMS content | Too many media items or total size > 1 MB | Reduce to ≤10 URLs and ≤1 MB total | | `40322` | Blocked content | Message contains prohibited content | Remove flagged content | | `40328` | SMS too large (warning) | Message splits into too many parts | Consider sending as MMS instead | ### Profile/configuration errors | Code | Error | Cause | Action | |------|-------|-------|--------| | `40311` | Invalid profile secret | `X-Profile-Secret` header is wrong | Check your messaging profile secret | | `40312` | Profile disabled | Messaging profile is disabled | Re-enable in [Mission Control](https://portal.telnyx.com/#/app/messaging) | | `40313` | Missing profile secret | `X-Profile-Secret` header required but not sent | Include the header in your request | | `40314` | Messaging disabled | Messaging disabled on your account | Contact [Telnyx support](https://support.telnyx.com) | | `40315` | Unhealthy sender | Sending number has poor health metrics | Check number success/spam rates; consider replacing | | `40318` | Queue full | Internal message queue at capacity | Back off and retry after a delay | | `40323` | Activation failed | Could not enable messaging on number | Contact support | | `40331` | Missing whitelist | No whitelisted destinations on profile | Add destination regions to your messaging profile | | `40333` | Spend limit reached | Messaging profile cost limit hit | Increase [spend limit](/docs/messaging/messages/configurable-spend-limits/index) or wait for reset | --- ## Number provisioning errors (401xx) | Code | Error | Cause | Action | |------|-------|-------|--------| | `40100` | Not messaging enabled | Number not configured for messaging | Enable messaging in Mission Control | | `40150` | Not in voice registry | Toll-free number missing from registry | Contact support | | `40151` | Enablement pending elsewhere | Another provider is enabling messaging | Wait for transfer to complete | | `40155` | LOA required | Letter of Authorization needed | Submit LOA through support | --- ## Handle errors in code ```python Python import telnyx telnyx.api_key = "YOUR_API_KEY" try: message = telnyx.Message.create( from_="+18005550100", to="+18005550101", text="Hello!", messaging_profile_id="YOUR_PROFILE_ID" ) print(f"Sent: {message.id}") except telnyx.error.InvalidRequestError as e: # API request errors (40xxx) error = e.json_body.get("errors", [{}])[0] code = error.get("code", "unknown") detail = error.get("detail", "No details") if code == "40300": print(f"Recipient opted out (STOP). Do not retry.") elif code == "40333": print(f"Spend limit reached. Increase limit or wait for reset.") elif code == "40310": print(f"Invalid destination number. Check E.164 format.") else: print(f"Error {code}: {detail}") except telnyx.error.AuthenticationError: print("Invalid API key") ``` ```javascript Node const telnyx = require("telnyx")("YOUR_API_KEY"); try { const message = await telnyx.messages.create({ from: "+18005550100", to: "+18005550101", text: "Hello!", messaging_profile_id: "YOUR_PROFILE_ID", }); console.log(`Sent: ${message.data.id}`); } catch (err) { const error = err.rawType || "unknown"; const detail = err.message; switch (error) { case "40300": console.log("Recipient opted out (STOP). Do not retry."); break; case "40333": console.log("Spend limit reached."); break; case "40310": console.log("Invalid destination number."); break; default: console.log(`Error ${error}: ${detail}`); } } ``` ```ruby Ruby require "telnyx" Telnyx.api_key = "YOUR_API_KEY" begin message = Telnyx::Message.create( from: "+18005550100", to: "+18005550101", text: "Hello!", messaging_profile_id: "YOUR_PROFILE_ID" ) puts "Sent: #{message.id}" rescue Telnyx::InvalidRequestError => e error = e.json_body&.dig(:errors, 0) || {} code = error[:code] || "unknown" detail = error[:detail] || "No details" case code when "40300" puts "Recipient opted out. Do not retry." when "40333" puts "Spend limit reached." else puts "Error #{code}: #{detail}" end end ``` ```go Go package main import ( "context" "fmt" "strings" telnyx "github.com/telnyx/telnyx-go" ) func main() { client := telnyx.NewClient("YOUR_API_KEY") _, err := client.Messages.Create(context.Background(), &telnyx.MessageParams{ From: "+18005550100", To: "+18005550101", Text: "Hello!", MessagingProfileID: "YOUR_PROFILE_ID", }) if err != nil { if strings.Contains(err.Error(), "40300") { fmt.Println("Recipient opted out. Do not retry.") } else if strings.Contains(err.Error(), "40333") { fmt.Println("Spend limit reached.") } else { fmt.Printf("Error: %v\n", err) } return } fmt.Println("Message sent successfully") } ``` ```bash curl # Check response for errors response=$(curl -s -w "\n%{http_code}" -X POST https://api.telnyx.com/v2/messages \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+18005550100", "to": "+18005550101", "text": "Hello!" }') http_code=$(echo "$response" | tail -1) body=$(echo "$response" | head -n -1) if [ "$http_code" != "200" ]; then error_code=$(echo "$body" | python3 -c "import sys,json; print(json.load(sys.stdin).get('errors',[{}])[0].get('code','unknown'))") echo "Error $error_code (HTTP $http_code)" echo "$body" | python3 -m json.tool fi ``` ### Handle delivery errors via webhooks Delivery errors arrive asynchronously in `message.finalized` webhooks: ```python Python @app.route("/webhooks", methods=["POST"]) def webhooks(): body = request.json event_type = body["data"]["event_type"] if event_type == "message.finalized": payload = body["data"]["payload"] status = payload["to"][0]["status"] if status == "delivery_failed" or status == "sending_failed": errors = payload.get("errors", []) for error in errors: code = error.get("code", "unknown") detail = error.get("detail", "") print(f"Delivery failed [{code}]: {detail}") # Decide whether to retry if code in ("40006", "40008"): print("Temporary error — retry with exponential backoff") elif code in ("40003", "40300", "40001", "40010", "40314", "40322"): print("Permanent error — do not retry") elif code in ("40002", "40011", "40016", "40017", "40018"): print("Intervention required — review content/rate, then retry") else: print("Review error and decide") return "", 200 ``` --- ## Retriable vs permanent errors Most delivery errors require you to **change something before retrying** — blindly retrying the same message with the same configuration will not resolve the issue and may result in further blocking. | Category | Codes | Action | |----------|-------|--------| | **Auto-retriable** (genuinely temporary) | `40006`, `40008` | Carrier-side issue — retry with exponential backoff (1s, 2s, 4s, 8s...). `40006`: recipient carrier is down. `40008`: general undeliverable, may resolve on retry. | | **Retriable after intervention** (fix first) | `40002`, `40005`, `40011`, `40014`, `40016`, `40017`, `40018`, `40318` | These errors indicate a problem with your sending rate, content, or throughput. Reduce sending rate, review message content for spam patterns, or resolve queue pressure **before** retrying. Automatic retry without changes will likely fail again. | | **Temporary hold** | `40020`, `40320` | `40020`: 2FA traffic blocked for 24 hours — wait, do not retry. `40320`: sender in transitional state — wait for provisioning to complete. | | **Permanent** (do not retry) | `40001`, `40003`, `40010`, `40300`, `40314`, `40322` | Fix root cause before attempting again. These will not resolve on their own. | | **Action required** (config/compliance) | `40010`, `40015`, `40019`, `40315`, `40329`, `40333` | Resolve the underlying configuration or compliance issue, then send again. | --- ## Related resources Understand and handle API and carrier rate limits. Query delivery status and error details for sent messages. Diagnose 10DLC registration and delivery issues. Configure and manage messaging profile spend limits. --- ### Smart Encoding > Source: https://developers.telnyx.com/docs/messaging/messages/smart-encoding.md Smart encoding automatically replaces Unicode characters with visually similar GSM-7 characters. This keeps your messages in the more efficient GSM-7 encoding, reducing segment counts and costs. Smart encoding applies to SMS only. MMS and RCS messages use UTF-8 encoding by default and are not affected. ## Why use smart encoding SMS messages using GSM-7 encoding fit **160 characters per segment**. When a message contains even one Unicode character outside GSM-7, the entire message switches to UTF-16 encoding, which only fits **70 characters per segment**. A single smart quote (`"`) or em dash (`—`) can more than double your messaging costs. **Example:** | Message | Encoding | Segments | Cost impact | |---------|----------|----------|-------------| | `Hello, how are you?` (150 chars) | GSM-7 | 1 | Base cost | | `Hello, how are you?` (150 chars with `"smart quotes"`) | UTF-16 | 3 | 3× cost | | `Hello, how are you?` (same, smart encoding ON) | GSM-7 | 1 | Base cost | Smart encoding is especially valuable when your message text originates from word processors, CMS platforms, or mobile keyboards that silently insert Unicode characters like curly quotes, em dashes, or non-breaking spaces. ## How it works When smart encoding is enabled: 1. Your message text is scanned for Unicode characters that have GSM-7 equivalents. 2. Matching characters are automatically replaced (e.g., `"` → `"`, `—` → `-`, `…` → `...`). 3. The final encoding (GSM-7 or UTF-16) is determined **after** all substitutions. 4. The segment count is recalculated based on the transformed text. 5. The API response includes metadata about the transformation. **Webhooks return the original text.** The `text` field in delivery webhooks contains your original message, not the smart-encoded version. This ensures your application's message tracking stays consistent. ## Enable smart encoding You can enable smart encoding at two levels: on a **messaging profile** (applies to all messages) or on a **per-request** basis. ### On a messaging profile Enable smart encoding as a default for all messages sent through a profile. ```bash curl curl -X PATCH https://api.telnyx.com/v2/messaging_profiles/{profile_id} \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "smart_encoding": true }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messagingProfiles.update( 'your_messaging_profile_id', { smart_encoding: true } ); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messaging_profiles.update( "your_messaging_profile_id", smart_encoding=True, ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messaging_profiles.update( "your_messaging_profile_id", smart_encoding: true ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.MessagingProfiles.Update( context.TODO(), "your_messaging_profile_id", telnyx.MessagingProfileUpdateParams{ SmartEncoding: telnyx.Bool(true), }, ) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messagingprofiles.MessagingProfileUpdateParams; import com.telnyx.sdk.models.messagingprofiles.MessagingProfileUpdateResponse; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessagingProfileUpdateParams params = MessagingProfileUpdateParams.builder() .smartEncoding(true) .build(); MessagingProfileUpdateResponse response = client .messagingProfiles() .update("your_messaging_profile_id", params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessagingProfileService(); var response = await service.UpdateAsync( "your_messaging_profile_id", new MessagingProfileUpdateOptions { SmartEncoding = true } ); Console.WriteLine(response.Data); ``` ```php PHP true] ); print_r($response); ``` Navigate to [Messaging > Messaging Profiles](https://portal.telnyx.com/#/app/messaging) in the portal. Click on the messaging profile you want to update. Toggle **Smart Encoding** to enabled. Click **Save** to apply the change. ### Per-request control Override the profile setting on individual messages using the `encoding` parameter: | Value | Behavior | |-------|----------| | `auto` | Follow the profile's `smart_encoding` setting (default). | | `gsm7` | Force GSM-7 encoding. Smart encoding is applied. Returns `400` if the message contains characters that cannot be converted to GSM-7 (e.g., emoji). | | `ucs2` | Force UCS-2 encoding. **Skips smart encoding entirely.** | The request-level `encoding` parameter **takes precedence** over the messaging profile's `smart_encoding` setting. ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "from": "+15551234567", "to": "+15559876543", "text": "Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!", "encoding": "auto" }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.send({ from: '+15551234567', to: '+15559876543', text: 'Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!', encoding: 'auto', }); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.send( from_="+15551234567", to="+15559876543", text="Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!", encoding="auto", ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_( from: "+15551234567", to: "+15559876543", text: "Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!", encoding: "auto" ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "+15551234567", To: "+15559876543", Text: "Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!", Encoding: telnyx.String("auto"), }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import com.telnyx.sdk.models.messages.MessageSendResponse; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessageSendParams params = MessageSendParams.builder() .from("+15551234567") .to("+15559876543") .text("Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!") .encoding("auto") .build(); MessageSendResponse response = client.messages().send(params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = "+15551234567", To = "+15559876543", Text = "Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!", Encoding = "auto" }); Console.WriteLine(response.Data); ``` ```php PHP '+15551234567', 'to' => '+15559876543', 'text' => "Don\u{2019}t miss our \u{201c}flash sale\u{201d} \u{2014} 50% off!", 'encoding' => 'auto' ]); print_r($response); ``` ## Response metadata When smart encoding is applied, the API response includes detailed metadata: ```json { "data": { "id": "8a0c35c0-5eed-4c0e-b1f0-abc123456789", "type": "SMS", "encoding": "GSM-7", "parts": 1, "smart_encoding": { "smart_encoding_applied": true, "final_encoding": "gsm7", "segment_count": 1, "character_count": 155, "replaced_character_count": 3, "length_change": 2 } } } ``` | Field | Description | |-------|-------------| | `smart_encoding_applied` | Whether any characters were replaced. | | `final_encoding` | The encoding used after transformation (`gsm7` or `ucs2`). | | `segment_count` | Number of segments after smart encoding. | | `character_count` | Message length after transformation. | | `replaced_character_count` | Number of unique characters that were substituted. | | `length_change` | Difference in length (positive means message grew, e.g., `…` → `...`). | The `parts` field in the top-level response reflects the segment count **after** smart encoding, so you always see the actual billing impact. ### Checking the response ```bash curl # Send and inspect the smart_encoding metadata curl -s -X POST https://api.telnyx.com/v2/messages \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "from": "+15551234567", "to": "+15559876543", "text": "Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!" }' | jq '.data.smart_encoding' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.send({ from: '+15551234567', to: '+15559876543', text: 'Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!', }); if (response.data.smart_encoding?.smart_encoding_applied) { console.log('Smart encoding applied!'); console.log(`Encoding: ${response.data.smart_encoding.final_encoding}`); console.log(`Segments: ${response.data.smart_encoding.segment_count}`); console.log(`Characters replaced: ${response.data.smart_encoding.replaced_character_count}`); } ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.send( from_="+15551234567", to="+15559876543", text="Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!", ) smart = response.data.smart_encoding if smart and smart.smart_encoding_applied: print(f"Encoding: {smart.final_encoding}") print(f"Segments: {smart.segment_count}") print(f"Characters replaced: {smart.replaced_character_count}") ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_( from: "+15551234567", to: "+15559876543", text: "Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!" ) smart = response.data.smart_encoding if smart&.smart_encoding_applied puts "Encoding: #{smart.final_encoding}" puts "Segments: #{smart.segment_count}" puts "Characters replaced: #{smart.replaced_character_count}" end ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "+15551234567", To: "+15559876543", Text: "Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!", }) if err != nil { panic(err.Error()) } if response.Data.SmartEncoding != nil && response.Data.SmartEncoding.SmartEncodingApplied { fmt.Printf("Encoding: %s\n", response.Data.SmartEncoding.FinalEncoding) fmt.Printf("Segments: %d\n", response.Data.SmartEncoding.SegmentCount) fmt.Printf("Replaced: %d\n", response.Data.SmartEncoding.ReplacedCharacterCount) } } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import com.telnyx.sdk.models.messages.MessageSendResponse; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessageSendParams params = MessageSendParams.builder() .from("+15551234567") .to("+15559876543") .text("Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!") .build(); MessageSendResponse response = client.messages().send(params); var smart = response.data().smartEncoding(); if (smart != null && smart.smartEncodingApplied()) { System.out.println("Encoding: " + smart.finalEncoding()); System.out.println("Segments: " + smart.segmentCount()); System.out.println("Replaced: " + smart.replacedCharacterCount()); } } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = "+15551234567", To = "+15559876543", Text = "Don\u2019t miss our \u201cflash sale\u201d \u2014 50% off!" }); var smart = response.Data.SmartEncoding; if (smart?.SmartEncodingApplied == true) { Console.WriteLine($"Encoding: {smart.FinalEncoding}"); Console.WriteLine($"Segments: {smart.SegmentCount}"); Console.WriteLine($"Replaced: {smart.ReplacedCharacterCount}"); } ``` ```php PHP '+15551234567', 'to' => '+15559876543', 'text' => "Don\u{2019}t miss our \u{201c}flash sale\u{201d} \u{2014} 50% off!" ]); $smart = $response->smart_encoding; if ($smart && $smart->smart_encoding_applied) { echo "Encoding: " . $smart->final_encoding . "\n"; echo "Segments: " . $smart->segment_count . "\n"; echo "Replaced: " . $smart->replaced_character_count . "\n"; } ``` ## Precedence rules Smart encoding behavior is determined by a combination of your messaging profile setting and the per-request `encoding` parameter: | Profile `smart_encoding` | Request `encoding` | Behavior | |---|---|---| | `true` | _(not set)_ | Smart encoding **applied** | | `false` | _(not set)_ | Smart encoding **not applied** | | `true` | `auto` | Smart encoding **applied** | | `false` | `auto` | Smart encoding **not applied** | | `true` or `false` | `gsm7` | Smart encoding **applied**, must result in GSM-7 or returns `400` | | `true` or `false` | `ucs2` | Smart encoding **skipped**, forced UCS-2 | The request-level `encoding` parameter always takes precedence over the messaging profile setting. ## Character substitutions Smart encoding replaces 200+ Unicode characters with GSM-7 equivalents. The tables below show all supported substitutions grouped by category. | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+00AB | « | Left-pointing double angle quotation mark | " | | U+00BB | » | Right-pointing double angle quotation mark | " | | U+201C | " | Left double quotation mark | " | | U+201D | " | Right double quotation mark | " | | U+02BA | ʺ | Modifier letter double prime | " | | U+02EE | ˮ | Modifier letter double apostrophe | " | | U+201F | ‟ | Double high-reversed-9 quotation mark | " | | U+275D | ❝ | Heavy double turned comma quotation mark ornament | " | | U+275E | ❞ | Heavy double comma quotation mark ornament | " | | U+301D | 〝 | Reversed double prime quotation mark | " | | U+301E | 〞 | Double prime quotation mark | " | | U+FF02 | " | Fullwidth quotation mark | " | | U+201E | „ | Double low quotation mark | " | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+2018 | ' | Left single quotation mark | ' | | U+2019 | ' | Right single quotation mark | ' | | U+02BB | ʻ | Modifier letter turned comma | ' | | U+02C8 | ˈ | Modifier letter vertical line | ' | | U+02BC | ʼ | Modifier letter apostrophe | ' | | U+02BD | ʽ | Modifier letter reversed comma | ' | | U+02B9 | ʹ | Modifier letter prime | ' | | U+201B | ‛ | Single high-reversed-9 quotation mark | ' | | U+FF07 | ' | Fullwidth apostrophe | ' | | U+00B4 | ´ | Acute accent | ' | | U+02CA | ˊ | Modifier letter acute accent | ' | | U+0060 | \` | Grave accent | ' | | U+02CB | ˋ | Modifier letter grave accent | ' | | U+275B | ❛ | Heavy single turned comma quotation mark ornament | ' | | U+275C | ❜ | Heavy single comma quotation mark ornament | ' | | U+0313 | ̓ | Combining comma above | ' | | U+0314 | ̔ | Combining reversed comma above | ' | | U+FE10 | ︐ | Presentation form for vertical comma | ' | | U+FE11 | ︑ | Presentation form for vertical ideographic comma | ' | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+2014 | — | Em dash | - | | U+2013 | – | En dash | - | | U+23BC | ⎼ | Horizontal scan line-7 | - | | U+23BD | ⎽ | Horizontal scan line-9 | - | | U+2015 | ― | Horizontal bar | - | | U+FE63 | ﹣ | Small hyphen-minus | - | | U+FF0D | - | Fullwidth hyphen-minus | - | | U+2010 | ‐ | Hyphen | - | | U+2022 | • | Bullet | - | | U+2043 | ⁃ | Hyphen bullet | - | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+00F7 | ÷ | Division sign | / | | U+00BC | ¼ | Vulgar fraction one quarter | 1/4 | | U+00BD | ½ | Vulgar fraction one half | 1/2 | | U+00BE | ¾ | Vulgar fraction three quarters | 3/4 | | U+29F8 | ⧸ | Big solidus | / | | U+0337 | ̷ | Combining short solidus overlay | / | | U+0338 | ̸ | Combining long solidus overlay | / | | U+2044 | ⁄ | Fraction slash | / | | U+2215 | ∕ | Division slash | / | | U+FF0F | / | Fullwidth solidus | / | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+29F9 | ⧹ | Big reverse solidus | \\ | | U+29F5 | ⧵ | Reverse solidus operator | \\ | | U+20E5 | | Combining reverse solidus overlay | \\ | | U+FE68 | ﹨ | Small reverse solidus | \\ | | U+FF3C | \ | Fullwidth reverse solidus | \\ | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+0332 | ̲ | Combining low line | _ | | U+FF3F | _ | Fullwidth low line | _ | | U+2017 | ‗ | Double low line | _ | | U+20D2 | ⃒ | Combining long vertical line overlay | \| | | U+20D3 | ⃓ | Combining short vertical line overlay | \| | | U+2223 | ∣ | Divides | \| | | U+FF5C | | | Fullwidth vertical line | \| | | U+23B8 | ⎸ | Left vertical box line | \| | | U+23B9 | ⎹ | Right vertical box line | \| | | U+23D0 | ⏐ | Vertical line extension | \| | | U+239C | ⎜ | Left parenthesis extension | \| | | U+239F | ⎟ | Right parenthesis extension | \| | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+FE6B | ﹫ | Small commercial at sign | @ | | U+FF20 | @ | Fullwidth commercial at sign | @ | | U+FE69 | ﹩ | Small dollar sign | $ | | U+FF04 | $ | Fullwidth dollar sign | $ | | U+01C3 | ǃ | Latin letter retroflex click | ! | | U+FE15 | ︕ | Presentation form for vertical exclamation mark | ! | | U+FE57 | ﹗ | Small exclamation mark | ! | | U+FF01 | ! | Fullwidth exclamation mark | ! | | U+203C | ‼ | Double exclamation mark | !! | | U+FE5F | ﹟ | Small number sign | # | | U+FF03 | # | Fullwidth number sign | # | | U+FE6A | ﹪ | Small percent sign | % | | U+FF05 | % | Fullwidth percent sign | % | | U+FE60 | ﹠ | Small ampersand | & | | U+FF06 | & | Fullwidth ampersand | & | | U+2026 | … | Horizontal ellipsis | ... | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+201A | ‚ | Single low-9 quotation mark | , | | U+0326 | ̦ | Combining comma below | , | | U+FE50 | ﹐ | Small comma | , | | U+3001 | 、 | Ideographic comma | , | | U+FE51 | ﹑ | Small ideographic comma | , | | U+FF0C | , | Fullwidth comma | , | | U+FF64 | 、 | Halfwidth ideographic comma | , | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+2768 | ❨ | Medium left parenthesis ornament | ( | | U+276A | ❪ | Medium flattened left parenthesis ornament | ( | | U+FE59 | ﹙ | Small left parenthesis | ( | | U+FF08 | ( | Fullwidth left parenthesis | ( | | U+27EE | ⟮ | Mathematical left flattened parenthesis | ( | | U+2985 | ⦅ | Left white parenthesis | ( | | U+2769 | ❩ | Medium right parenthesis ornament | ) | | U+276B | ❫ | Medium flattened right parenthesis ornament | ) | | U+FE5A | ﹚ | Small right parenthesis | ) | | U+FF09 | ) | Fullwidth right parenthesis | ) | | U+27EF | ⟯ | Mathematical right flattened parenthesis | ) | | U+2986 | ⦆ | Right white parenthesis | ) | | U+2774 | ❴ | Medium left curly bracket ornament | \{ | | U+FE5B | ﹛ | Small left curly bracket | \{ | | U+FF5B | { | Fullwidth left curly bracket | \{ | | U+2775 | ❵ | Medium right curly bracket ornament | \} | | U+FE5C | ﹜ | Small right curly bracket | \} | | U+FF5D | } | Fullwidth right curly bracket | \} | | U+FF3B | [ | Fullwidth left square bracket | \[ | | U+FF3D | ] | Fullwidth right square bracket | \] | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+204E | ⁎ | Low asterisk | * | | U+2217 | ∗ | Asterisk operator | * | | U+229B | ⊛ | Circled asterisk operator | * | | U+2722 | ✢ | Four teardrop-spoked asterisk | * | | U+2723 | ✣ | Four balloon-spoked asterisk | * | | U+2724 | ✤ | Heavy four balloon-spoked asterisk | * | | U+2725 | ✥ | Four club-spoked asterisk | * | | U+2731 | ✱ | Heavy asterisk | * | | U+2732 | ✲ | Open center asterisk | * | | U+2733 | ✳ | Eight spoked asterisk | * | | U+273A | ✺ | Sixteen pointed asterisk | * | | U+273B | ✻ | Teardrop-spoked asterisk | * | | U+273C | ✼ | Open center teardrop-spoked asterisk | * | | U+273D | ✽ | Heavy teardrop-spoked asterisk | * | | U+2743 | ❃ | Heavy teardrop-spoked pinwheel asterisk | * | | U+2749 | ❉ | Balloon-spoked asterisk | * | | U+274A | ❊ | Eight teardrop-spoked propeller asterisk | * | | U+274B | ❋ | Heavy eight teardrop-spoked propeller asterisk | * | | U+29C6 | ⧆ | Squared asterisk | * | | U+FE61 | ﹡ | Small asterisk | * | | U+FF0A | * | Fullwidth asterisk | * | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+02D6 | ˖ | Modifier letter plus sign | + | | U+FE62 | ﹢ | Small plus sign | + | | U+FF0B | + | Fullwidth plus sign | + | | U+FE64 | ﹤ | Small less-than sign | `<` | | U+FF1C | < | Fullwidth less-than sign | `<` | | U+0347 | ͇ | Combining equals sign below | = | | U+A78A | ꞊ | Modifier letter short equals sign | = | | U+FE66 | ﹦ | Small equals sign | = | | U+FF1D | = | Fullwidth equals sign | = | | U+FE65 | ﹥ | Small greater-than sign | `>` | | U+FF1E | > | Fullwidth greater-than sign | `>` | | U+2039 | ‹ | Single left-pointing angle quotation mark | `<` | | U+203A | › | Single right-pointing angle quotation mark | `>` | | U+3002 | 。 | Ideographic full stop | . | | U+FE52 | ﹒ | Small full stop | . | | U+FF0E | . | Fullwidth full stop | . | | U+FF61 | 。 | Halfwidth ideographic full stop | . | | U+02D0 | ː | Modifier letter triangular colon | : | | U+02F8 | ˸ | Modifier letter raised colon | : | | U+2982 | ⦂ | Z notation type colon | : | | U+A789 | ꞉ | Modifier letter colon | : | | U+FE13 | ︓ | Presentation form for vertical colon | : | | U+FF1A | : | Fullwidth colon | : | | U+204F | ⁏ | Reversed semicolon | ; | | U+FE14 | ︔ | Presentation form for vertical semicolon | ; | | U+FE54 | ﹔ | Small semicolon | ; | | U+FF1B | ; | Fullwidth semicolon | ; | | U+FE16 | ︖ | Presentation form for vertical question mark | ? | | U+FE56 | ﹖ | Small question mark | ? | | U+FF1F | ? | Fullwidth question mark | ? | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+FF10 | 0 | Fullwidth digit zero | 0 | | U+FF11 | 1 | Fullwidth digit one | 1 | | U+FF12 | 2 | Fullwidth digit two | 2 | | U+FF13 | 3 | Fullwidth digit three | 3 | | U+FF14 | 4 | Fullwidth digit four | 4 | | U+FF15 | 5 | Fullwidth digit five | 5 | | U+FF16 | 6 | Fullwidth digit six | 6 | | U+FF17 | 7 | Fullwidth digit seven | 7 | | U+FF18 | 8 | Fullwidth digit eight | 8 | | U+FF19 | 9 | Fullwidth digit nine | 9 | **Fullwidth uppercase (U+FF21–U+FF3A)** → A–Z **Fullwidth lowercase (U+FF41–U+FF5A)** → a–z **Small capital letters:** | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+1D00 | ᴀ | Latin letter small capital A | A | | U+0299 | ʙ | Latin letter small capital B | B | | U+1D04 | ᴄ | Latin letter small capital C | C | | U+1D05 | ᴅ | Latin letter small capital D | D | | U+1D07 | ᴇ | Latin letter small capital E | E | | U+A730 | ꜰ | Latin letter small capital F | F | | U+0262 | ɢ | Latin letter small capital G | G | | U+029C | ʜ | Latin letter small capital H | H | | U+026A | ɪ | Latin letter small capital I | I | | U+1D0A | ᴊ | Latin letter small capital J | J | | U+1D0B | ᴋ | Latin letter small capital K | K | | U+029F | ʟ | Latin letter small capital L | L | | U+1D0D | ᴍ | Latin letter small capital M | M | | U+0274 | ɴ | Latin letter small capital N | N | | U+1D0F | ᴏ | Latin letter small capital O | O | | U+1D18 | ᴘ | Latin letter small capital P | P | | U+0280 | ʀ | Latin letter small capital R | R | | U+A731 | ꜱ | Latin letter small capital S | S | | U+1D1B | ᴛ | Latin letter small capital T | T | | U+1D1C | ᴜ | Latin letter small capital U | U | | U+1D20 | ᴠ | Latin letter small capital V | V | | U+1D21 | ᴡ | Latin letter small capital W | W | | U+028F | ʏ | Latin letter small capital Y | Y | | U+1D22 | ᴢ | Latin letter small capital Z | Z | Greek capital letters that visually resemble Latin letters are substituted: | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+0391 | Α | Greek capital letter Alpha | A | | U+0392 | Β | Greek capital letter Beta | B | | U+0395 | Ε | Greek capital letter Epsilon | E | | U+0397 | Η | Greek capital letter Eta | H | | U+0399 | Ι | Greek capital letter Iota | I | | U+039A | Κ | Greek capital letter Kappa | K | | U+039C | Μ | Greek capital letter Mu | M | | U+039D | Ν | Greek capital letter Nu | N | | U+039F | Ο | Greek capital letter Omicron | O | | U+03A1 | Ρ | Greek capital letter Rho | P | | U+03A4 | Τ | Greek capital letter Tau | T | | U+03A7 | Χ | Greek capital letter Chi | X | | U+03A5 | Υ | Greek capital letter Upsilon | Y | | U+0396 | Ζ | Greek capital letter Zeta | Z | | Unicode | Glyph | Description | Replacement | |---------|-------|-------------|-------------| | U+02C6 | ˆ | Modifier letter circumflex accent | ^ | | U+0302 | ̂ | Combining circumflex accent | ^ | | U+FF3E | ^ | Fullwidth circumflex accent | ^ | | U+1DCD | ᷍ | Combining double circumflex above | ^ | | U+02DC | ˜ | Small tilde | ~ | | U+02F7 | ˷ | Modifier letter low tilde | ~ | | U+0303 | ̃ | Combining tilde | ~ | | U+0330 | ̰ | Combining tilde below | ~ | | U+0334 | ̴ | Combining tilde overlay | ~ | | U+223C | ∼ | Tilde operator | ~ | | U+FF5E | ~ | Fullwidth tilde | ~ | These characters are replaced with a standard space or removed: | Unicode | Description | Replacement | |---------|-------------|-------------| | U+00A0 | No-break space | (space) | | U+2000 | En quad | (space) | | U+2001 | Em quad | (space) | | U+2002 | En space | (space) | | U+2003 | Em space | (space) | | U+2004 | Three-per-em space | (space) | | U+2005 | Four-per-em space | (space) | | U+2006 | Six-per-em space | (space) | | U+2007 | Figure space | (space) | | U+2008 | Punctuation space | (space) | | U+2009 | Thin space | (space) | | U+200A | Hair space | (space) | | U+200B | Zero width space | (removed) | | U+202F | Narrow no-break space | (space) | | U+205F | Medium mathematical space | (space) | | U+3000 | Ideographic space | (space) | | U+FEFF | Zero width no-break space | (removed) | | U+2028 | Line separator | (removed) | | U+2029 | Paragraph separator | (removed) | | U+2060 | Word joiner | (removed) | These control characters are removed or transformed: | Unicode | Description | Replacement | |---------|-------------|-------------| | U+0009 | Tab | 7 spaces | | U+0000 | Null | (removed) | | U+0003 | End of text | (removed) | | U+0004 | End of transmission | (removed) | | U+0010 | Escape | (removed) | | U+0011 | Device control one | (removed) | | U+0012 | Device control two | (removed) | | U+0013 | Device control three | (removed) | | U+0014 | Device control four | (removed) | | U+0017 | End of transmission block | (removed) | | U+0019 | End of medium | (removed) | | U+0080 | C1 control codes | (removed) | | U+008D | Reverse line feed | (removed) | | U+0090 | Device control string | (removed) | | U+009B | Control sequence introducer | (removed) | | U+009F | Application program command | (removed) | Tab characters (U+0009) are converted to 7 spaces, which can significantly increase message length and affect segment count. ## Edge cases Some substitutions increase message length. For example: - Horizontal ellipsis (`…`) becomes three periods (`...`) — adds 2 characters - Tab (U+0009) becomes 7 spaces — adds 6 characters - Vulgar fractions like `½` become `1/2` — adds 2 characters The segment count is calculated **after** these replacements, so a message near the 160-character limit may become multi-part after transformation. If your message contains both replaceable Unicode characters and non-replaceable ones (like emojis), smart encoding still applies all possible substitutions. However, the non-replaceable characters will keep the message in UTF-16 encoding. This is still beneficial — fewer Unicode characters means a shorter UTF-16 message and potentially fewer segments. The characters `~`, `^`, `|`, `\`, `{`, `}`, `[`, `]` are part of the GSM-7 extended set and count as **2 characters** each when calculating segment length. Smart encoding accounts for this when determining the final segment count. Zero-width characters (like U+200B zero-width space) are removed entirely. If your message consists entirely of zero-width or control characters that all get removed, the API returns a `400` error — messages cannot be empty after transformation. If you set `encoding=gsm7` on a request but the message contains characters that cannot be represented in GSM-7 (e.g., emoji), the API returns a `400` error rather than silently dropping characters. ## Limitations - **SMS only** — MMS and RCS use UTF-8 encoding by default and are not affected by smart encoding. - **Not all characters convert** — Emojis and non-Latin scripts (e.g., Chinese, Arabic, Cyrillic) have no GSM-7 equivalents and will still trigger UTF-16 encoding. - **Visual differences** — Substitutions may slightly alter the appearance of your message. Review the character tables above to understand what changes will occur. - **Length may increase** — Some substitutions produce longer output (e.g., `…` → `...`). Always check the response metadata for the actual segment count. ## Related resources Learn about GSM-7, UTF-16, and segment calculations. Get started with the Telnyx Messaging API. API reference for updating messaging profile settings. API reference for sending messages with encoding options. --- ### Group Messaging > Source: https://developers.telnyx.com/docs/messaging/messages/group-messaging.md Send group MMS messages to multiple recipients in a single API call. Group messaging uses the MMS protocol to create multi-party conversations where all participants can see and reply to each other — just like a group text on your phone. ## Prerequisites - A [Telnyx account](https://telnyx.com/sign-up) - A [Messaging Profile](/docs/messaging/messages/send-message#2-create-a-messaging-profile) with an MMS-enabled phone number - An [API key](https://portal.telnyx.com/#/app/api-keys) ## How group messaging works Group messaging builds on the MMS protocol to enable multi-party conversations: 1. You send a message to the **`/v2/messages/group_mms`** endpoint with multiple `to` numbers 2. Telnyx delivers the message to all recipients as a group MMS conversation 3. When any recipient replies, all participants (including your Telnyx number) receive the reply 4. You receive inbound group messages via [webhooks](/docs/messaging/messages/receiving-webhooks) with a `cc` field listing all participants **Group messaging constraints:** - Maximum of **8 recipients** per conversation (plus the sender) - **MMS protocol only** — all messages are billed at MMS rates - **US and Canada destinations only** - Requires a **v2 webhook version** on your messaging profile for inbound messages - Charged **per recipient** — standard MMS rates plus carrier passthrough fees apply ## Send a group message Export your API key as an environment variable: ```bash export TELNYX_API_KEY="YOUR_API_KEY" ``` Send a group MMS to multiple recipients. You can include text, media, or both. ```bash curl curl -X POST https://api.telnyx.com/v2/messages/group_mms \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -d '{ "from": "+15551234567", "to": ["+15559876543", "+15558765432"], "text": "Hey team, check out this photo!", "media_urls": ["https://example.com/photo.jpg"] }' ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.send( from_="+15551234567", to=["+15559876543", "+15558765432"], text="Hey team, check out this photo!", media_urls=["https://example.com/photo.jpg"] ) print(response.data) ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.send({ from: '+15551234567', to: ['+15559876543', '+15558765432'], text: 'Hey team, check out this photo!', media_urls: ['https://example.com/photo.jpg'] }); console.log(response.data); ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_( from: "+15551234567", to: ["+15559876543", "+15558765432"], text: "Hey team, check out this photo!", media_urls: ["https://example.com/photo.jpg"] ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "+15551234567", To: []string{"+15559876543", "+15558765432"}, Text: "Hey team, check out this photo!", MediaURLs: []string{"https://example.com/photo.jpg"}, }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import com.telnyx.sdk.models.messages.MessageSendResponse; import java.util.List; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessageSendParams params = MessageSendParams.builder() .from("+15551234567") .to(List.of("+15559876543", "+15558765432")) .text("Hey team, check out this photo!") .mediaUrls(List.of("https://example.com/photo.jpg")) .build(); MessageSendResponse response = client.messages().send(params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = "+15551234567", To = new[] { "+15559876543", "+15558765432" }, Text = "Hey team, check out this photo!", MediaUrls = new[] { "https://example.com/photo.jpg" } }); Console.WriteLine(response.Data); ``` ```php PHP '+15551234567', 'to' => ['+15559876543', '+15558765432'], 'text' => 'Hey team, check out this photo!', 'media_urls' => ['https://example.com/photo.jpg'] ]); print_r($response); ``` ```bash curl curl -X POST https://api.telnyx.com/v2/messages/group_mms \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -d '{ "from": "+15551234567", "to": ["+15559876543", "+15558765432"], "text": "Hey team, lunch at noon?" }' ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.send( from_="+15551234567", to=["+15559876543", "+15558765432"], text="Hey team, lunch at noon?" ) print(response.data) ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.send({ from: '+15551234567', to: ['+15559876543', '+15558765432'], text: 'Hey team, lunch at noon?' }); console.log(response.data); ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_( from: "+15551234567", to: ["+15559876543", "+15558765432"], text: "Hey team, lunch at noon?" ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "+15551234567", To: []string{"+15559876543", "+15558765432"}, Text: "Hey team, lunch at noon?", }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import com.telnyx.sdk.models.messages.MessageSendResponse; import java.util.List; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessageSendParams params = MessageSendParams.builder() .from("+15551234567") .to(List.of("+15559876543", "+15558765432")) .text("Hey team, lunch at noon?") .build(); MessageSendResponse response = client.messages().send(params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = "+15551234567", To = new[] { "+15559876543", "+15558765432" }, Text = "Hey team, lunch at noon?" }); Console.WriteLine(response.Data); ``` ```php PHP '+15551234567', 'to' => ['+15559876543', '+15558765432'], 'text' => 'Hey team, lunch at noon?' ]); print_r($response); ``` ### Response A successful response includes per-recipient status: ```json { "data": { "record_type": "message", "direction": "outbound", "id": "403188fd-58c5-4557-a8de-1700a358d768", "type": "MMS", "messaging_profile_id": "1e0df9c5-8716-4bcf-8fb2-9f6d9527fd95", "from": "+15551234567", "to": [ { "phone_number": "+15559876543", "status": "queued", "carrier": "T-MOBILE USA, INC.", "line_type": "Wireless" }, { "phone_number": "+15558765432", "status": "queued", "carrier": "CELLCO PARTNERSHIP DBA VERIZON WIRELESS", "line_type": "Wireless" } ], "text": "Hey team, check out this photo!", "media": [ { "url": "https://example.com/photo.jpg", "content_type": null, "sha256": null, "size": null } ], "encoding": "GSM-7", "parts": 1, "cost": { "amount": "0.0800", "currency": "USD" } } } ``` Save the `id` to correlate delivery webhooks with your group message. ## Receive group messages When someone replies to the group conversation, you receive a `message.received` webhook. The `cc` field lists all other participants in the conversation: ```json { "data": { "event_type": "message.received", "id": "0d7c4fbe-d075-435f-b71b-694391743967", "occurred_at": "2023-08-08T13:03:05.129+00:00", "payload": { "cc": [ "+15558765432", "+15557654321", "+15551234567" ], "direction": "inbound", "encoding": "UCS-2", "from": { "carrier": "AT&T", "line_type": "Wireless", "phone_number": "+15559876543" }, "id": "9d12c9d0-5172-429a-8fb9-cc9da297717f", "media": [], "messaging_profile_id": "1e0df9c5-8716-4bcf-8fb2-9f6d9527fd95", "record_type": "message", "text": "Sounds good, see you at noon!", "to": [ { "carrier": "Telnyx", "line_type": "Wireless", "phone_number": "+15551234567", "status": "webhook_delivered" } ], "type": "MMS" }, "record_type": "event" } } ``` **Key fields for inbound group messages:** | Field | Description | |-------|-------------| | `from.phone_number` | The participant who sent the reply | | `to` | Your Telnyx number(s) in the conversation | | `cc` | All other participants in the group conversation | ## Webhooks and delivery tracking Group messages generate individual webhook events and detail records for each recipient: - **Per-recipient webhooks:** You receive a separate `message.finalized` event for each recipient with their individual delivery status - **`group_message_id`:** A unique identifier returned in the API response, webhooks, and detail records that lets you correlate all individual records back to the original group message - **Non-Telnyx recipient status:** Handset delivery confirmation is not available for non-Telnyx recipients — their status will show as `unknown` ```json { "data": { "event_type": "message.finalized", "id": "b40653f5-91cd-46b1-9542-0c092bd29795", "occurred_at": "2023-08-08T10:29:36.090+00:00", "payload": { "direction": "outbound", "from": { "phone_number": "+15551234567" }, "group_message_id": "403189d4-b1d6-4993-b263-6470e5224430", "id": "403189d4-b1d6-4993-b263-6470e5224430", "to": [ { "phone_number": "+15559876543", "status": "delivered" } ], "text": "Hey team, check out this photo!", "type": "MMS" }, "record_type": "event" } } ``` ## Comparison with other providers | Feature | Telnyx | Twilio | Vonage | |---------|--------|--------|--------| | Group MMS support | ✅ Native, single API call | ⚠️ Requires Conversations API | ❌ Not natively supported | | Max participants | 8 + sender | 10 total | N/A | | Dedicated endpoint | ✅ `/v2/messages/group_mms` | ❌ Requires Conversations setup | N/A | | Setup complexity | Single POST request | Multi-step (create conversation, add participants) | N/A | | Per-recipient tracking | ✅ Individual webhooks | ✅ Via Conversations events | N/A | | US/Canada support | ✅ | ✅ | N/A | ## Troubleshooting - Verify all `to` numbers are valid US or Canadian wireless numbers - Group MMS requires MMS-capable handsets — landlines and some VoIP numbers won't receive them - Check that you haven't exceeded the 8-recipient limit - Ensure your messaging profile uses **v2 webhook version** — go to [Messaging](https://portal.telnyx.com/#/app/messaging), edit your profile, and confirm the webhook version - Verify your webhook URL is accessible and returning `200 OK` This is expected for non-Telnyx recipients. The MMS protocol does not reliably support delivery receipts across all carriers. Only Telnyx-to-Telnyx messages will show confirmed delivery status. ## Next steps New to Telnyx messaging? Start here Handle delivery confirmations and inbound messages Learn about long codes, toll-free, and short codes Full messaging API documentation --- ### Alphanumeric Sender ID > Source: https://developers.telnyx.com/docs/messaging/messages/alphanumeric-sender-id.md Alphanumeric Sender IDs allow you to send SMS messages using a custom text identifier (like your brand name) instead of a phone number. This makes messages instantly recognizable to recipients—they see "TELNYX" instead of a random number. Alphanumeric Sender IDs are **one-way only**. Recipients cannot reply to messages sent from alphanumeric senders. ## Format requirements Your alphanumeric sender ID must follow these rules: | Requirement | Value | |-------------|-------| | Length | 1–11 characters | | Allowed characters | Letters (A-Z, a-z), numbers (0-9), spaces | | Must contain | At least one letter | **Country restrictions**: Alphanumeric senders **cannot send to the United States, Canada, or Puerto Rico**. Use a [long code](/docs/messaging/getting-started/choosing-your-sender-type/index) or [toll-free number](/docs/messaging/toll-free-verification) for these destinations. ## Before you begin To use alphanumeric sender IDs, you need: - A [Telnyx account](https://telnyx.com/sign-up) with **Level 2 verification** - A [Messaging Profile](https://portal.telnyx.com/#/app/messaging) configured with your alphanumeric sender ID - An [API key](https://portal.telnyx.com/#/app/api-keys) Some countries require sender ID pre-registration. Check with [Telnyx support](https://support.telnyx.com) for destination-specific requirements. ## Send a message Use the [send message endpoint](/api-reference/messages/send-a-message) with your alphanumeric sender ID in the `from` field. ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "from": "YourBrand", "to": "+447700900123", "text": "Your order has shipped!", "messaging_profile_id": "YOUR_MESSAGING_PROFILE_ID" }' ``` ```python Python import telnyx telnyx.api_key = "YOUR_API_KEY" telnyx.Message.create( from_="YourBrand", to="+447700900123", text="Your order has shipped!", messaging_profile_id="YOUR_MESSAGING_PROFILE_ID" ) ``` ```javascript Node import Telnyx from 'telnyx'; const telnyx = new Telnyx('YOUR_API_KEY'); const message = await telnyx.messages.create({ from: 'YourBrand', to: '+447700900123', text: 'Your order has shipped!', messaging_profile_id: 'YOUR_MESSAGING_PROFILE_ID' }); console.log(message.data); ``` ```ruby Ruby require 'telnyx' Telnyx.api_key = 'YOUR_API_KEY' Telnyx::Message.create( from: 'YourBrand', to: '+447700900123', text: 'Your order has shipped!', messaging_profile_id: 'YOUR_MESSAGING_PROFILE_ID' ) ``` ```php PHP \Telnyx\Telnyx::setApiKey('YOUR_API_KEY'); \Telnyx\Message::Create([ 'from' => 'YourBrand', 'to' => '+447700900123', 'text' => 'Your order has shipped!', 'messaging_profile_id' => 'YOUR_MESSAGING_PROFILE_ID' ]); ``` ```java Java import com.telnyx.sdk.ApiClient; import com.telnyx.sdk.Configuration; import com.telnyx.sdk.auth.HttpBearerAuth; import com.telnyx.sdk.model.CreateMessageRequest; import com.telnyx.sdk.api.MessagesApi; public class SendAlphanumeric { public static void main(String[] args) { ApiClient client = Configuration.getDefaultApiClient(); client.setBasePath("https://api.telnyx.com/v2"); HttpBearerAuth auth = (HttpBearerAuth) client.getAuthentication("bearerAuth"); auth.setBearerToken("YOUR_API_KEY"); MessagesApi api = new MessagesApi(client); CreateMessageRequest request = new CreateMessageRequest() .from("YourBrand") .to("+447700900123") .text("Your order has shipped!"); api.createMessage(request); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey("YOUR_API_KEY"); var service = new MessagingSenderIdService(); var response = await service.CreateAsync(new NewMessagingSenderId { From = "YourBrand", To = "+447700900123", Text = "Your order has shipped!" }); ``` Replace: - `YOUR_API_KEY` with your [API key](https://portal.telnyx.com/#/app/api-keys) - `YourBrand` with your alphanumeric sender ID (1–11 characters) - `YOUR_MESSAGING_PROFILE_ID` with your [messaging profile ID](https://portal.telnyx.com/#/app/messaging) ## US and Canada fallback If you need to reach US or Canadian recipients, configure a **fallback long code** on your messaging profile. Telnyx will automatically use this number instead of the alphanumeric sender for restricted destinations. Configure this in the [Messaging Portal](https://portal.telnyx.com/#/app/messaging) or via the [Messaging Profiles API](/api-reference/profiles/update-a-messaging-profile). ## Limitations | Feature | Supported | |---------|-----------| | SMS | Yes | | MMS | No | | Inbound messages | No | | US/CA/PR destinations | No (use fallback) | ## Rate limits | Account level | Rate limit | |---------------|------------| | Level 1 (unverified) | 6 messages/minute | | Level 2 (verified) | 60 messages/minute | Need higher throughput? Contact [sales@telnyx.com](mailto:sales@telnyx.com). ## Failover behavior Telnyx attempts to deliver your message whenever possible. If alphanumeric delivery fails for a destination: 1. If a US fallback long code is configured, Telnyx uses that number 2. Otherwise, Telnyx may use a generic alphanumeric sender ID to complete delivery ## Common errors | Error | Cause | Solution | |-------|-------|----------| | `InvalidFromAddress` | Invalid sender format | Use 1–11 characters with at least one letter | | `AlphaSenderNotConfigured` | No alphanumeric sender on profile | Configure sender ID on your messaging profile | | `UnsupportedDestination` | Sending to US/CA/PR | Use a long code or configure a fallback number | ## Next steps Create and configure messaging profiles via API Complete messaging quickstart guide Compare alphanumeric, long code, toll-free, and short codes Full send message API documentation --- ### International Compliance > Source: https://developers.telnyx.com/docs/messaging/messages/international-sms-compliance.md Sending SMS internationally requires compliance with country-specific regulations for sender IDs, opt-in consent, content restrictions, and registration requirements. This guide covers the top 10 international messaging destinations and their specific rules. **Quick links:** [Country-by-country reference](#country-by-country-reference) · [Sender ID types](#sender-id-types-by-country) · [Opt-in requirements](#opt-in-requirements-by-region) · [Content restrictions](#content-restrictions) · [Pre-registration](#countries-requiring-pre-registration) Regulations change frequently. This guide reflects requirements as of early 2026. Always verify current requirements with [Telnyx support](https://support.telnyx.com) before launching messaging in a new country. --- ## Sender ID types by country Not every sender type works in every country. Here's what's supported in the top international destinations: | Country | Alphanumeric ID | Long Code | Short Code | Toll-Free | Pre-Registration | |---------|:-:|:-:|:-:|:-:|:-:| | 🇺🇸 United States | ❌ | ✅ (10DLC) | ✅ | ✅ | 10DLC required | | 🇨🇦 Canada | ❌ | ✅ | ✅ | ✅ | Short code approval | | 🇬🇧 United Kingdom | ✅ | ✅ | ✅ | — | Recommended | | 🇩🇪 Germany | ✅ | ✅ | ✅ | — | No | | 🇫🇷 France | ✅ | ✅ | ✅ | — | OACP required | | 🇪🇸 Spain | ✅ | ✅ | ✅ | — | No | | 🇦🇺 Australia | ✅ | ✅ | ✅ | — | Sender ID registration | | 🇮🇳 India | ✅ (registered) | ❌ | ❌ | — | DLT mandatory | | 🇧🇷 Brazil | ✅ | ✅ | ✅ | — | No | | 🇲🇽 Mexico | ✅ | ✅ | ✅ | — | No | **US/Canada note:** Alphanumeric sender IDs are **not supported** for US and Canadian destinations. Use [10DLC](/docs/messaging/10dlc/quickstart/index), [toll-free](/docs/messaging/toll-free-verification/index), or [short codes](/docs/messaging/messages/short-code/index). --- ## Countries requiring pre-registration Several countries require sender ID or entity registration before you can send messages. Failing to register results in blocked traffic or filtered messages. ### Mandatory registration India requires **Distributed Ledger Technology (DLT)** registration for all A2P SMS. This is the most complex international registration requirement. **What you need:** 1. **Entity registration** on a DLT platform (JioConnect, Vodafone DLT, Airtel DLT, or BSNL DLT) 2. **Header (sender ID) registration** — your alphanumeric sender ID must be approved 3. **Template registration** — every message template must be pre-approved 4. **Content category** — transactional, promotional, or service **Registration steps:** 1. Register as a business entity on one of the DLT platforms 2. Submit your sender ID (called "header") for approval 3. Create and submit message templates 4. Provide Telnyx with your DLT Entity ID, registered headers, and template IDs **Message categories:** | Category | Allowed Hours | DND Filtering | Example | |----------|--------------|---------------|---------| | Transactional | 24/7 | Exempt | OTP, order confirmations | | Service (Implicit) | 24/7 | Exempt | Account updates to existing customers | | Promotional | 9 AM – 9 PM IST | Applies | Marketing, offers, discounts | **Promotional messages** to users on the Do Not Disturb (DND) registry will be blocked. Transactional and service messages are exempt from DND filtering. **Template format:** ``` Dear {#var#}, your order {#var#} has been shipped. Track at {#var#}. Delivery by {#var#}. ``` Variables are marked with `{#var#}` and the template must match exactly at delivery time. France requires registration through the **Off-net Application-to-Person (OACP)** framework for commercial SMS. **Requirements:** - Sender ID must be registered with French carriers - Opt-out must include "STOP" at no cost to the recipient - Commercial messages restricted to 8 AM – 8 PM local time - No commercial SMS on Sundays or public holidays - CNIL (French data authority) consent rules apply **Registration process:** 1. Submit sender ID registration through Telnyx support 2. Provide business documentation (SIRET number for French businesses) 3. Allow 5–10 business days for approval Unregistered sender IDs may be silently filtered by French carriers. Australia's ACMA requires sender ID registration for A2P messaging. **Requirements:** - Alphanumeric sender IDs must be registered with carriers - Messages must include opt-out instructions - Commercial messages must comply with the Spam Act 2003 - Sender must have consent (express or inferred) **Registration:** 1. Submit sender ID through Telnyx support 2. Provide Australian Business Number (ABN) or equivalent 3. Typical approval: 3–5 business days Singapore's SMS Sender ID Registry (SSIR) requires all organizations to register sender IDs. **Requirements:** - Mandatory SSIR registration since January 2023 - Unregistered alphanumeric sender IDs display as "Likely-SCAM" - Registration through SGNIC (Singapore Network Information Centre) **Process:** 1. Register on the SSIR portal (sgnic.sg) 2. Submit sender ID with business documentation 3. Link registered sender ID to Telnyx account via support ### Recommended (not mandatory) registration | Country | Registration | Benefit | |---------|-------------|---------| | 🇬🇧 United Kingdom | Sender ID pre-registration | Higher delivery rates, reduced filtering | | 🇩🇪 Germany | None required | — | | 🇪🇸 Spain | None required | — | | 🇧🇷 Brazil | Sender ID registration | Better deliverability | | 🇲🇽 Mexico | None required | — | --- ## Opt-in requirements by region ### Europe (GDPR + ePrivacy) The EU's GDPR and ePrivacy Directive set the baseline for all EU/EEA countries: Recipients must actively opt in to receive messages. Pre-checked boxes are **not valid consent** under GDPR. Consent must specify what types of messages the user will receive. "We may contact you" is too vague. Users must be able to opt out at any time, and the process must be as easy as opting in. Maintain records of when and how consent was obtained. You must be able to prove consent if challenged. **GDPR-compliant consent example:** ``` ☐ I agree to receive appointment reminders and order updates from [Company] via SMS to the phone number provided. Message frequency: up to 4 msg/month. Reply STOP to unsubscribe. Msg & data rates may apply. ``` **Country variations within the EU:** - **Germany:** Requires "double opt-in" (confirmation SMS after initial signup) as best practice - **France:** CNIL requires explicit, separate consent for marketing SMS - **Spain:** AEPD allows "soft opt-in" for existing customers (similar products/services only) - **Italy:** Garante requires clear separation between service and marketing consent ### North America | Requirement | United States | Canada | |-------------|--------------|--------| | Governing law | TCPA + CTIA guidelines | CASL | | Consent type | Express written (marketing) / Express (transactional) | Express or implied | | Opt-out mechanism | STOP keyword mandatory | Unsubscribe mechanism required | | Record retention | Recommended 4+ years | Duration of consent | | Pre-registration | 10DLC / toll-free verification | Short code approval | ### Asia-Pacific | Country | Key requirement | |---------|----------------| | 🇮🇳 India | DLT registration + template approval. DND registry filtering for promotional. | | 🇦🇺 Australia | Express consent required (Spam Act 2003). Include sender identity + opt-out. | | 🇸🇬 Singapore | SSIR registration. PDPA consent rules. No SMS between 9 PM – 9 AM without consent. | | 🇯🇵 Japan | Act on Specified Commercial Transactions. Opt-out link required. Sender identification mandatory. | | 🇰🇷 South Korea | Pre-approved templates only. 080 opt-out number required for commercial messages. | ### Latin America | Country | Key requirement | |---------|----------------| | 🇧🇷 Brazil | LGPD consent required. No messages between 9 PM – 9 AM. Include opt-out. | | 🇲🇽 Mexico | LFPDPPP consent. Include sender identity. Opt-out mechanism required. | | 🇨🇴 Colombia | SIC consent requirements. Habeas Data law. Pre-registration recommended. | | 🇦🇷 Argentina | PDPA consent. National Do Not Call Registry must be checked. | --- ## Content restrictions ### Universally restricted content These content types are restricted or prohibited in most countries: | Content type | Status | Notes | |-------------|--------|-------| | Cannabis / CBD | 🚫 Prohibited in most countries | Even where locally legal, carriers often block | | Gambling | ⚠️ Heavily regulated | Requires specific licensing in most jurisdictions | | Adult content | 🚫 Prohibited | Blocked by most carriers globally | | Phishing / fraud | 🚫 Prohibited | Immediate account termination | | Financial services | ⚠️ Regulated | Must comply with local financial advertising laws | | Healthcare / pharma | ⚠️ Regulated | Prescription drug messaging restricted in many countries | | Political campaigns | ⚠️ Varies by country | Some countries ban political SMS entirely | ### Country-specific restrictions - **Financial promotions:** Must be approved by an FCA-authorized firm - **Age-gated content:** Must use age verification for alcohol, gambling - **Charity messaging:** Regulated by the Fundraising Regulator - **Marketing hours:** No legal restriction, but industry best practice is 8 AM – 9 PM - **UWG (Competition Law):** Strict consent requirements for all commercial messages - **Heilmittelwerbegesetz:** Restricts pharmaceutical advertising - **Glücksspielstaatsvertrag:** Strict gambling advertising rules - **Double opt-in:** Expected best practice for marketing consent - **Loi Hamon:** Right to opt out of all commercial solicitation - **Time restrictions:** No commercial SMS 8 PM – 8 AM, Sundays, or public holidays - **CNIL enforcement:** Active enforcement with significant fines - **Language:** Commercial messages should be in French - **Promotional hours:** 9 AM – 9 PM IST only (mandatory, not best practice) - **DND registry:** Promotional messages blocked to DND-registered numbers - **Template approval:** Every message must match a pre-approved template - **Scrubbing:** Numbers are checked against DND registry before delivery - **LGPD compliance:** Explicit consent with specific purpose - **Quiet hours:** 9 PM – 9 AM (industry standard) - **Consumer code:** Price/offer messages must include full terms - **Language:** Messages should be in Portuguese --- ## Country-by-country reference ### 🇬🇧 United Kingdom | Setting | Value | |---------|-------| | **Sender types** | Alphanumeric (recommended), long code, short code | | **Alphanumeric length** | 3–11 characters | | **Regulation** | PECR + UK GDPR | | **Regulator** | ICO (Information Commissioner's Office) | | **Pre-registration** | Recommended (improves deliverability) | | **Opt-out** | STOP keyword or unsubscribe link | | **Time restrictions** | None (best practice: 8 AM – 9 PM) | **Send with alphanumeric sender ID:** ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "YourBrand", "to": "+447700900123", "text": "Hi! Your delivery is scheduled for tomorrow between 2-4 PM. Reply STOP to opt out.", "messaging_profile_id": "YOUR_MESSAGING_PROFILE_ID" }' ``` ```python Python import telnyx import os telnyx.api_key = os.environ["TELNYX_API_KEY"] message = telnyx.Message.create( from_="YourBrand", to="+447700900123", text="Hi! Your delivery is scheduled for tomorrow between 2-4 PM. Reply STOP to opt out.", messaging_profile_id="YOUR_MESSAGING_PROFILE_ID", ) print(f"Message ID: {message.id}") ``` ```javascript Node import Telnyx from "telnyx"; const telnyx = new Telnyx(process.env.TELNYX_API_KEY); const { data: message } = await telnyx.messages.create({ from: "YourBrand", to: "+447700900123", text: "Hi! Your delivery is scheduled for tomorrow between 2-4 PM. Reply STOP to opt out.", messaging_profile_id: "YOUR_MESSAGING_PROFILE_ID", }); console.log(`Message ID: ${message.id}`); ``` ```ruby Ruby require "telnyx" Telnyx.api_key = ENV["TELNYX_API_KEY"] message = Telnyx::Message.create( from: "YourBrand", to: "+447700900123", text: "Hi! Your delivery is scheduled for tomorrow between 2-4 PM. Reply STOP to opt out.", messaging_profile_id: "YOUR_MESSAGING_PROFILE_ID" ) puts "Message ID: #{message.id}" ``` ```java Java import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageCreateParams; TelnyxClient client = TelnyxOkHttpClient.fromEnv(); var message = client.messages().create(MessageCreateParams.builder() .from("YourBrand") .to("+447700900123") .text("Hi! Your delivery is scheduled for tomorrow between 2-4 PM. Reply STOP to opt out.") .messagingProfileId("YOUR_MESSAGING_PROFILE_ID") .build()); System.out.println("Message ID: " + message.id()); ``` ```csharp .NET using Telnyx; var client = new TelnyxClient(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var message = await client.Messages.CreateAsync(new MessageCreateParams { From = "YourBrand", To = "+447700900123", Text = "Hi! Your delivery is scheduled for tomorrow between 2-4 PM. Reply STOP to opt out.", MessagingProfileId = "YOUR_MESSAGING_PROFILE_ID" }); Console.WriteLine($"Message ID: {message.Id}"); ``` ```php PHP $telnyx = new \Telnyx\TelnyxClient(getenv('TELNYX_API_KEY')); $message = $telnyx->messages->create([ 'from' => 'YourBrand', 'to' => '+447700900123', 'text' => 'Hi! Your delivery is scheduled for tomorrow between 2-4 PM. Reply STOP to opt out.', 'messaging_profile_id' => 'YOUR_MESSAGING_PROFILE_ID', ]); echo "Message ID: " . $message->id . "\n"; ``` ```go Go package main import ( "context" "fmt" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient(option.WithAPIKey("YOUR_API_KEY")) message, err := client.Messages.New(context.TODO(), telnyx.MessageNewParams{ From: telnyx.String("YourBrand"), To: telnyx.String("+447700900123"), Text: telnyx.String("Hi! Your delivery is scheduled for tomorrow between 2-4 PM. Reply STOP to opt out."), MessagingProfileID: telnyx.String("YOUR_MESSAGING_PROFILE_ID"), }) if err != nil { panic(err) } fmt.Printf("Message ID: %s\n", message.ID) } ``` ### 🇮🇳 India | Setting | Value | |---------|-------| | **Sender types** | Alphanumeric only (6 characters, registered header) | | **Regulation** | TRAI + DLT framework | | **Regulator** | TRAI (Telecom Regulatory Authority of India) | | **Pre-registration** | **Mandatory** — DLT entity, header, and template registration | | **Opt-out** | Handled via DLT/DND registry | | **Time restrictions** | Promotional: 9 AM – 9 PM IST only | India requires both **sender ID registration** and **message template approval** before any messages can be sent. Contact [Telnyx support](https://support.telnyx.com) to initiate India DLT registration. ### 🇩🇪 Germany | Setting | Value | |---------|-------| | **Sender types** | Alphanumeric, long code | | **Regulation** | GDPR + UWG (Competition Act) + TTDSG | | **Regulator** | BfDI (Federal Data Protection Commissioner) | | **Pre-registration** | Not required | | **Opt-out** | Must be free and easy (STOP keyword or link) | | **Time restrictions** | None legally, best practice 8 AM – 9 PM | ### 🇫🇷 France | Setting | Value | |---------|-------| | **Sender types** | Alphanumeric (registered via OACP), long code, short code | | **Regulation** | GDPR + Code des postes et des communications | | **Regulator** | CNIL + ARCEP | | **Pre-registration** | **Required** — OACP sender ID registration | | **Opt-out** | "STOP" at no cost to recipient (mandatory) | | **Time restrictions** | **8 AM – 8 PM, no Sundays/holidays** (mandatory for commercial) | ### 🇦🇺 Australia | Setting | Value | |---------|-------| | **Sender types** | Alphanumeric (registered), long code, short code | | **Regulation** | Spam Act 2003 + Privacy Act 1988 | | **Regulator** | ACMA | | **Pre-registration** | Required (sender ID registration) | | **Opt-out** | Functional unsubscribe within 5 business days | | **Time restrictions** | None legally, best practice 9 AM – 8 PM AEST | ### 🇧🇷 Brazil | Setting | Value | |---------|-------| | **Sender types** | Alphanumeric, long code, short code | | **Regulation** | LGPD + Consumer Protection Code | | **Regulator** | ANPD (National Data Protection Authority) | | **Pre-registration** | Recommended | | **Opt-out** | Easy mechanism required | | **Time restrictions** | 9 PM – 9 AM quiet hours (industry standard) | ### 🇲🇽 Mexico | Setting | Value | |---------|-------| | **Sender types** | Alphanumeric, long code, short code | | **Regulation** | LFPDPPP (Federal Data Protection Law) | | **Regulator** | INAI | | **Pre-registration** | Not required | | **Opt-out** | Mechanism required in privacy notice | | **Time restrictions** | None legally | --- ## Best practices for international messaging Review this guide and contact Telnyx support for any country not listed. Requirements vary significantly and change frequently. Alphanumeric sender IDs are preferred in most international markets (except US/Canada). They build brand recognition and improve open rates. Send messages in the recipient's language. Many countries require or strongly recommend this for commercial messaging. Even where not legally required, sending during business hours dramatically reduces complaints and opt-outs. Universal best practice. Use language appropriate to the country (e.g., "STOP" in English-speaking countries, "ARRÊT" in France). Store when and how each recipient consented. GDPR requires you to prove consent if challenged. Keep records for at least 4 years. Use [Message Detail Records](/docs/messaging/messages/message-detail-records/index) to track delivery rates per country. Sudden drops may indicate registration issues or content filtering. --- ## Handling multi-country messaging For platforms sending to multiple countries, implement country-aware routing: ```python Python import telnyx import os telnyx.api_key = os.environ["TELNYX_API_KEY"] # Country-specific configuration COUNTRY_CONFIG = { "US": { "from": "+12025551234", # 10DLC registered number "profile": "us-messaging-profile-id", }, "GB": { "from": "YourBrand", # Alphanumeric sender ID "profile": "intl-messaging-profile-id", }, "IN": { "from": "YRBRAND", # 6-char registered DLT header "profile": "india-messaging-profile-id", }, "DEFAULT": { "from": "YourBrand", "profile": "intl-messaging-profile-id", }, } def send_international_sms(to: str, text: str, country_code: str): """Send an SMS with country-appropriate sender and profile.""" config = COUNTRY_CONFIG.get(country_code, COUNTRY_CONFIG["DEFAULT"]) message = telnyx.Message.create( from_=config["from"], to=to, text=text, messaging_profile_id=config["profile"], ) return message # Usage send_international_sms("+447700900123", "Your order shipped!", "GB") send_international_sms("+12025559876", "Your order shipped!", "US") send_international_sms("+919876543210", "Your order shipped!", "IN") ``` ```javascript Node import Telnyx from "telnyx"; const telnyx = new Telnyx(process.env.TELNYX_API_KEY); const COUNTRY_CONFIG = { US: { from: "+12025551234", profile: "us-messaging-profile-id" }, GB: { from: "YourBrand", profile: "intl-messaging-profile-id" }, IN: { from: "YRBRAND", profile: "india-messaging-profile-id" }, DEFAULT: { from: "YourBrand", profile: "intl-messaging-profile-id" }, }; async function sendInternationalSms(to, text, countryCode) { const config = COUNTRY_CONFIG[countryCode] || COUNTRY_CONFIG.DEFAULT; const { data: message } = await telnyx.messages.create({ from: config.from, to, text, messaging_profile_id: config.profile, }); return message; } // Usage await sendInternationalSms("+447700900123", "Your order shipped!", "GB"); await sendInternationalSms("+12025559876", "Your order shipped!", "US"); ``` --- ## Next steps Set up branded sender IDs for international messaging. Handle character encoding for international scripts (Arabic, Chinese, etc.). Understand delivery errors including country-specific rejections. US-specific registration for A2P messaging. --- ### MMS Converter > Source: https://developers.telnyx.com/docs/messaging/messages/mms-converter.md While your message's source number may support sending MMS, the destination number might not support receiving it. Normally, this will prevent you from sending MMS to this destination. When **MMS converter** is enabled on your messaging profile, however, your MMS will be converted to an SMS message by Telnyx and then sent to the destination. Messages sent as SMS are unaffected by this feature and will be sent as usual. The resultant webhooks for messages sent with this feature enabled will indicate the protocol that was used to send the message. For example: when an MMS message is sent and fallback happens, the webhook will indicate that an SMS message was sent, and when an MMS message is sent and fallback doesn't happen, the webhook will indicate that an MMS message was sent. If fallback happens, the destination will receive an SMS formatted to contain the media URLs specified in the request. Each media URL will appear on its own line, immediately after the message body, if any. ## Examples Note how the media URL(s) appear on the destination exactly as provided in the request. No shortlinking or other transformations are applied on your behalf. ### Message body and one media URL If your request includes: * `"text": "message body that\nis potentially spread across multiple lines"` * `"media_urls": ["https://example.com/image.png"]` The message received on the destination will look like this: ``` message body that is potentially spread across multiple lines https://example.com/image.png ``` ### Message body and two media URLs If your request includes: * `"text": "message body"` * `"media_urls": ["https://example.com/one.png", "https://example.com/two.png"]` The message received on the destination will look like this: ``` message body https://example.com/one.png https://example.com/two.png ``` ### Only one media URL If your request doesn't set `text` and includes: * `"media_urls": ["https://example.com/image.png"]` The message received on the destination will look like this: ``` https://example.com/image.png ``` ## Enable MMS converter This behavior is not enabled by default; if you want to make use of this feature then you must enable it first. This is controlled at the messaging profile level by an optional boolean field called `mms_fall_back_to_sms`, which you can either set at [creation time][create] or [update later][update] if you'd like to change an existing messaging profile. [create]: /api-reference/profiles/create-a-messaging-profile [update]: /api-reference/profiles/update-a-messaging-profile --- ### MMS Media & Transcoding > Source: https://developers.telnyx.com/docs/messaging/messages/mms-transcoding.md MMS messages let you send images, videos, and other media files alongside text. This guide covers supported media types, carrier size limits, and how to use Telnyx's automatic transcoding to ensure delivery. MMS is supported on Long Code, Toll-Free, and Short Code numbers in the US and Canada. International MMS support varies by carrier. ## Send an MMS message Include one or more `media_urls` in your message request to send an MMS: ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "from": "+15551234567", "to": "+15559876543", "text": "Check out this image!", "media_urls": ["https://example.com/image.jpg"] }' ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) response = client.messages.send( from_="+15551234567", to="+15559876543", text="Check out this image!", media_urls=["https://example.com/image.jpg"], ) print(response.data) ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.send({ from: '+15551234567', to: '+15559876543', text: 'Check out this image!', media_urls: ['https://example.com/image.jpg'], }); console.log(response.data); ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_( from: "+15551234567", to: "+15559876543", text: "Check out this image!", media_urls: ["https://example.com/image.jpg"] ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "+15551234567", To: "+15559876543", Text: "Check out this image!", MediaURLs: []string{"https://example.com/image.jpg"}, }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import java.util.List; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); var params = MessageSendParams.builder() .from("+15551234567") .to("+15559876543") .text("Check out this image!") .mediaUrls(List.of("https://example.com/image.jpg")) .build(); var response = client.messages().send(params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = "+15551234567", To = "+15559876543", Text = "Check out this image!", MediaUrls = new List { "https://example.com/image.jpg" } }); Console.WriteLine(response.Data); ``` ```php PHP '+15551234567', 'to' => '+15559876543', 'text' => 'Check out this image!', 'media_urls' => ['https://example.com/image.jpg'] ]); print_r($response); ``` You can include up to **10 media URLs** per MMS message. Each URL must be publicly accessible — Telnyx fetches the media at send time. --- ## Supported media types | Category | Formats | Notes | |----------|---------|-------| | Images | JPEG, PNG, GIF, BMP, TIFF, WebP | Most widely supported across carriers | | Video | MP4, 3GP, MOV | H.264 codec recommended | | Audio | MP3, WAV, AMR, OGG | Limited carrier support | | Documents | PDF, vCard (.vcf), iCal (.ics) | Limited carrier support | **Animated GIFs** are not supported for transcoding. If you send animated GIFs, ensure they are under the carrier size limit — they will not be resized. --- ## Carrier size limits Each US carrier imposes different maximum MMS message sizes based on the sender type. Messages exceeding these limits will be rejected by the carrier. | Carrier | Long Code | Toll-Free | Short Code | |---------|-----------|-----------|------------| | AT&T | 1 MB | 600 KB | 600 KB | | T-Mobile | 1.5 MB | 600 KB | 1 MB | | Verizon | 1 MB | 600 KB | 1.2 MB | The **safe maximum** across all carriers and sender types is **600 KB**. To guarantee delivery to all recipients regardless of carrier, keep your total media under this limit — or enable transcoding. --- ## Automatic transcoding Telnyx can automatically resize media to comply with carrier size limits. When enabled, oversized images and videos are resized before delivery. ### How transcoding works 1. You send an MMS with media that exceeds the destination carrier's size limit 2. Telnyx detects the destination carrier and its size restriction 3. Media is resized to fit within the limit: - **Images** → converted to JPEG - **Videos** → converted to H.264 MP4 4. The resized media is delivered to the recipient Transcoding reduces media quality to meet size constraints. For best results, optimize your media before sending. ### Enable transcoding Enable `mms_transcoding` on your messaging profile to apply it to all MMS sent through that profile. ```bash curl curl -X PATCH https://api.telnyx.com/v2/messaging_profiles/{profile_id} \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "mms_transcoding": true }' ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) response = client.messaging_profiles.update( "your_messaging_profile_id", mms_transcoding=True, ) print(response.data) ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messagingProfiles.update( 'your_messaging_profile_id', { mms_transcoding: true } ); console.log(response.data); ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messaging_profiles.update( "your_messaging_profile_id", mms_transcoding: true ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.MessagingProfiles.Update( context.TODO(), "your_messaging_profile_id", telnyx.MessagingProfileUpdateParams{ MMSTranscoding: telnyx.Bool(true), }, ) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messagingprofiles.MessagingProfileUpdateParams; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); var params = MessagingProfileUpdateParams.builder() .mmsTranscoding(true) .build(); var response = client.messagingProfiles() .update("your_messaging_profile_id", params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessagingProfileService(); var response = await service.UpdateAsync( "your_messaging_profile_id", new MessagingProfileUpdateOptions { MmsTranscoding = true } ); Console.WriteLine(response.Data); ``` ```php PHP true] ); print_r($response); ``` Navigate to [Messaging > Messaging Profiles](https://portal.telnyx.com/#/app/messaging) in the portal. Click on the messaging profile you want to update. Toggle **MMS Transcoding** to enabled. Click **Save** to apply. --- ## Best practices Pre-optimize your media for the best balance of quality and deliverability: - **Images:** Resize to 640×480 or smaller, use JPEG at 80% quality - **Videos:** Compress to H.264, keep under 30 seconds, target 480p - **Target size:** Stay under 600 KB for universal carrier compatibility This gives you control over quality rather than relying on automatic transcoding. Media URLs must be publicly accessible — Telnyx fetches them at send time. Ensure: - URLs don't require authentication - URLs respond quickly (timeouts cause delivery failures) - URLs return the correct `Content-Type` header - URLs use HTTPS for security Inbound MMS media URLs in webhooks are **ephemeral** — they expire after a short period. Always download and store important media to your own storage (e.g., AWS S3) immediately upon receiving the webhook. See the [Send & Receive MMS](/docs/messaging/messages/send-receive-mms/index) tutorial for a complete implementation. If your message doesn't include media, send it as SMS instead of MMS. SMS is: - **Cheaper** — MMS messages cost more than SMS - **Faster** — No media download/processing overhead - **More reliable** — Fewer points of failure Only use MMS when you actually need to include media content. --- ## Related resources Full tutorial for building an MMS application with media storage. Customize the layout of your MMS media with SMIL templates. Get started with the Telnyx Messaging API. API reference for sending messages with media. --- ### Schedule Messages > Source: https://developers.telnyx.com/docs/messaging/messages/schedule-message.md Schedule SMS and MMS messages to send at a specific time in the future. Use scheduled messaging for appointment reminders, marketing campaigns, time-zone-aware notifications, and any scenario where precise delivery timing matters. ## Prerequisites - A [Telnyx account](https://telnyx.com/sign-up) - A [Messaging Profile](/docs/messaging/messages/send-message#2-create-a-messaging-profile) with an assigned phone number - An [API key](https://portal.telnyx.com/#/app/api-keys) ## How scheduled messaging works When you schedule a message, Telnyx stores it and delivers it at the specified time. Here's how it works: 1. You send a request with a `send_at` timestamp set in the future 2. Telnyx validates the request and returns a message resource with `status: "scheduled"` 3. At the scheduled time (accurate to the minute), Telnyx sends the message 4. Standard [webhooks](/docs/messaging/messages/receiving-webhooks) fire as the message is processed and delivered **Scheduling constraints:** - `send_at` must be at least **5 minutes** in the future - `send_at` must be no more than **5 days** in the future - Scheduling accuracy is up to **1 minute** - Maximum of **1 million** scheduled messages at any given time ## Schedule a message You can schedule messages using either endpoint: - **`POST /v2/messages`** — The standard send endpoint, with the `send_at` parameter added - **`POST /v2/messages/schedule`** — A dedicated scheduling endpoint with the same parameters Both endpoints accept identical parameters. The examples below use `/v2/messages` with `send_at`. Export your API key as an environment variable: ```bash export TELNYX_API_KEY="YOUR_API_KEY" ``` The `send_at` field requires an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted datetime string in UTC. For example: - `2026-02-15T14:30:00Z` — February 15, 2026 at 2:30 PM UTC - `2026-02-14T09:00:00-08:00` — February 14, 2026 at 9:00 AM PST **Time zone tip:** Always convert your desired delivery time to UTC, or include the UTC offset. Messages are delivered based on the UTC time you specify, not the recipient's local time zone. ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -d '{ "from": "+15551234567", "to": "+15559876543", "text": "Reminder: Your appointment is tomorrow at 10 AM.", "send_at": "2026-02-15T14:30:00Z" }' ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.send( from_="+15551234567", to="+15559876543", text="Reminder: Your appointment is tomorrow at 10 AM.", send_at="2026-02-15T14:30:00Z" ) print(response.data) ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.send({ from: '+15551234567', to: '+15559876543', text: 'Reminder: Your appointment is tomorrow at 10 AM.', send_at: '2026-02-15T14:30:00Z' }); console.log(response.data); ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_( from: "+15551234567", to: "+15559876543", text: "Reminder: Your appointment is tomorrow at 10 AM.", send_at: "2026-02-15T14:30:00Z" ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "+15551234567", To: "+15559876543", Text: "Reminder: Your appointment is tomorrow at 10 AM.", SendAt: "2026-02-15T14:30:00Z", }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import com.telnyx.sdk.models.messages.MessageSendResponse; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessageSendParams params = MessageSendParams.builder() .from("+15551234567") .to("+15559876543") .text("Reminder: Your appointment is tomorrow at 10 AM.") .sendAt("2026-02-15T14:30:00Z") .build(); MessageSendResponse response = client.messages().send(params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = "+15551234567", To = "+15559876543", Text = "Reminder: Your appointment is tomorrow at 10 AM.", SendAt = "2026-02-15T14:30:00Z" }); Console.WriteLine(response.Data); ``` ```php PHP '+15551234567', 'to' => '+15559876543', 'text' => 'Reminder: Your appointment is tomorrow at 10 AM.', 'send_at' => '2026-02-15T14:30:00Z' ]); print_r($response); ``` ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -d '{ "from": "+15551234567", "to": "+15559876543", "text": "Check out our weekend sale!", "subject": "Weekend Sale", "media_urls": ["https://example.com/sale-banner.jpg"], "send_at": "2026-02-15T14:30:00Z" }' ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.send( from_="+15551234567", to="+15559876543", text="Check out our weekend sale!", subject="Weekend Sale", media_urls=["https://example.com/sale-banner.jpg"], send_at="2026-02-15T14:30:00Z" ) print(response.data) ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.send({ from: '+15551234567', to: '+15559876543', text: 'Check out our weekend sale!', subject: 'Weekend Sale', media_urls: ['https://example.com/sale-banner.jpg'], send_at: '2026-02-15T14:30:00Z' }); console.log(response.data); ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_( from: "+15551234567", to: "+15559876543", text: "Check out our weekend sale!", subject: "Weekend Sale", media_urls: ["https://example.com/sale-banner.jpg"], send_at: "2026-02-15T14:30:00Z" ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "+15551234567", To: "+15559876543", Text: "Check out our weekend sale!", Subject: "Weekend Sale", MediaURLs: []string{"https://example.com/sale-banner.jpg"}, SendAt: "2026-02-15T14:30:00Z", }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import com.telnyx.sdk.models.messages.MessageSendResponse; import java.util.List; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessageSendParams params = MessageSendParams.builder() .from("+15551234567") .to("+15559876543") .text("Check out our weekend sale!") .subject("Weekend Sale") .mediaUrls(List.of("https://example.com/sale-banner.jpg")) .sendAt("2026-02-15T14:30:00Z") .build(); MessageSendResponse response = client.messages().send(params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = "+15551234567", To = "+15559876543", Text = "Check out our weekend sale!", Subject = "Weekend Sale", MediaUrls = new[] { "https://example.com/sale-banner.jpg" }, SendAt = "2026-02-15T14:30:00Z" }); Console.WriteLine(response.Data); ``` ```php PHP '+15551234567', 'to' => '+15559876543', 'text' => 'Check out our weekend sale!', 'subject' => 'Weekend Sale', 'media_urls' => ['https://example.com/sale-banner.jpg'], 'send_at' => '2026-02-15T14:30:00Z' ]); print_r($response); ``` ### Response A successful response returns the message with `status: "scheduled"`: ```json { "data": { "record_type": "message", "direction": "outbound", "id": "b0c7e8cb-6227-4c74-9f32-c7f80c30934b", "type": "SMS", "messaging_profile_id": "16fd2706-8baf-433b-82eb-8c7fada847da", "from": { "phone_number": "+15551234567" }, "to": [ { "phone_number": "+15559876543", "status": "scheduled" } ], "text": "Reminder: Your appointment is tomorrow at 10 AM.", "send_at": "2026-02-15T14:30:00Z" } } ``` Save the `id` — you'll need it to retrieve or cancel the scheduled message. ## Retrieve a scheduled message Check the status of a scheduled message with `GET /v2/messages/{id}`: ```bash curl curl -X GET https://api.telnyx.com/v2/messages/b0c7e8cb-6227-4c74-9f32-c7f80c30934b \ -H "Authorization: Bearer $TELNYX_API_KEY" ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.retrieve("b0c7e8cb-6227-4c74-9f32-c7f80c30934b") print(response.data) ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.retrieve( 'b0c7e8cb-6227-4c74-9f32-c7f80c30934b' ); console.log(response.data); ``` The retrieve endpoint can only access messages created within the last **10 days**. For older messages, generate an [MDR report](https://portal.telnyx.com/#/app/reporting/mdr). ## Cancel a scheduled message Cancel a message that hasn't been sent yet with `DELETE /v2/messages/{id}`: ```bash curl curl -X DELETE https://api.telnyx.com/v2/messages/b0c7e8cb-6227-4c74-9f32-c7f80c30934b \ -H "Authorization: Bearer $TELNYX_API_KEY" ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.delete("b0c7e8cb-6227-4c74-9f32-c7f80c30934b") print(response.data) ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.del( 'b0c7e8cb-6227-4c74-9f32-c7f80c30934b' ); console.log(response.data); ``` **Cancellation rules:** - The message must have `status: "scheduled"` - The `send_at` time must be more than **1 minute** in the future - Once a message begins sending, it cannot be cancelled ## Webhooks Scheduled messages trigger the same [messaging webhooks](/docs/messaging/messages/receiving-webhooks) as immediate messages. The webhook sequence is: 1. **`message.sent`** — Fires when the message is sent at the scheduled time 2. **`message.finalized`** — Fires when delivery is confirmed or fails ```json { "data": { "event_type": "message.sent", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "occurred_at": "2026-02-15T14:30:01Z", "payload": { "id": "b0c7e8cb-6227-4c74-9f32-c7f80c30934b", "direction": "outbound", "type": "SMS", "from": { "phone_number": "+15551234567" }, "to": [ { "phone_number": "+15559876543", "status": "sent" } ], "text": "Reminder: Your appointment is tomorrow at 10 AM." } } } ``` ## Use cases Schedule reminders 24 hours before an appointment: ```python Python from datetime import datetime, timedelta, timezone import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) appointment_time = datetime(2026, 2, 16, 10, 0, tzinfo=timezone.utc) reminder_time = appointment_time - timedelta(hours=24) response = client.messages.send( from_="+15551234567", to="+15559876543", text=f"Reminder: Your appointment is tomorrow at {appointment_time.strftime('%I:%M %p')} UTC.", send_at=reminder_time.isoformat() ) print(f"Reminder scheduled for {reminder_time.isoformat()}, message ID: {response.data.id}") ``` Send marketing messages during business hours in each recipient's time zone: ```python Python from datetime import datetime, timezone, timedelta import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) recipients = [ {"number": "+15551234567", "tz_offset": -5}, # EST {"number": "+15559876543", "tz_offset": -8}, # PST {"number": "+15557654321", "tz_offset": -6}, # CST ] for recipient in recipients: # Send at 10:00 AM in each recipient's local time local_10am = datetime(2026, 2, 16, 10, 0, tzinfo=timezone(timedelta(hours=recipient["tz_offset"]))) utc_time = local_10am.astimezone(timezone.utc) response = client.messages.send( from_="+15550001111", to=recipient["number"], text="Weekend flash sale! 20% off with code WEEKEND20.", send_at=utc_time.isoformat() ) print(f"Scheduled for {recipient['number']} at {utc_time.isoformat()}") ``` ## Limits and rate limiting - **Scheduling window:** 5 minutes to 5 days in the future - **Maximum scheduled messages:** 1 million at any given time - **Accuracy:** Messages are sent within 1 minute of the scheduled time - **Rate limits:** The same [rate limits](/docs/messaging/messages/rate-limiting) apply to scheduled messages as to immediate messages — both when creating the scheduled message and when it's sent ## Comparison with other providers | Feature | Telnyx | Twilio | Vonage | |---------|--------|--------|--------| | Scheduling window | 5 min – 5 days | 15 min – 35 days | Not natively supported | | Cancellation | ✅ Up to 1 min before send time | ✅ Up to 1 hour before send time | N/A | | Dedicated endpoint | ✅ `/v2/messages/schedule` | ❌ Same endpoint only | N/A | | Requires Messaging Service | ❌ Optional | ✅ Required | N/A | | Additional cost | ❌ Free | ❌ Free | N/A | | Accuracy | ~1 minute | ~15 minutes | N/A | ## Next steps New to Telnyx messaging? Start here Handle delivery confirmations and inbound messages Understand messaging throughput limits Full API parameter documentation --- ### Specifying SMIL Template > Source: https://developers.telnyx.com/docs/messaging/messages/smil-template.md SMIL specifies how MMS media files are laid out in MMS. Telnyx generates SMIL automatically based on `media_urls` and `text` fields. Advanced users can add a `smil_template` parameter to the MMS message request. This template will then be used to generate a customized SMIL. Example of SMIL template: ```xml ``` Note that the template has parameters enclosed in double braces `{{ }}`. During the construction of SMIL these parameters will be automatically replaced with locations of text and media contents. In case of media, `{{ 0 }}` will be replaced with the location of the first URL in the `media_urls` list, `{{ 1 }}` will be replaced with the location of the second URL in the `media_urls` list, and so on. `{{ text }}` will be replaced with the location of the `text` content. The SMIL template must be set in `smil_template` parameter of the MMS request. It must be properly JSON-escaped: ```json { "from": "+13125551234", "to": "+18655551234", "text": "Hello from Telnyx", "media_urls": ["https://example.com/media.jpg"], "smil_template": "\n\t\n\t\t Source: https://developers.telnyx.com/docs/messaging/messages/2fa.md Implement SMS-based two-factor authentication (2FA) using the Telnyx Messaging API. This guide covers generating, sending, and verifying one-time passwords (OTPs) with security best practices. **Consider the Verify API first.** Telnyx offers a dedicated [Verify API](/docs/identity/verify/index) that handles OTP generation, delivery, and verification for you — including retry logic, rate limiting, and multi-channel support (SMS, voice, WhatsApp). Use this guide only if you need full control over the 2FA flow. ## How SMS 2FA works ```mermaid sequenceDiagram participant User participant App as Your Application participant Telnyx as Telnyx API participant Phone as User's Phone User->>App: Login / Action requiring 2FA App->>App: Generate OTP + store with expiry App->>Telnyx: Send SMS with OTP Telnyx->>Phone: Deliver SMS Phone->>User: Read OTP User->>App: Submit OTP App->>App: Verify OTP + check expiry App->>User: Access granted / denied ``` --- ## Generate and send an OTP Generate a cryptographically secure OTP and send it via SMS: ```python Python import os import secrets import time from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) # In production, use a database (Redis, PostgreSQL, etc.) otp_store = {} OTP_LENGTH = 6 OTP_EXPIRY_SECONDS = 300 # 5 minutes def generate_otp() -> str: """Generate a cryptographically secure numeric OTP.""" # Use secrets module for secure random generation return "".join(secrets.choice("0123456789") for _ in range(OTP_LENGTH)) def send_otp(phone_number: str) -> dict: """Generate and send an OTP to the given phone number.""" otp = generate_otp() # Store OTP with expiry and attempt counter otp_store[phone_number] = { "otp": otp, "expires_at": time.time() + OTP_EXPIRY_SECONDS, "attempts": 0, "max_attempts": 3, } # Send via Telnyx response = client.messages.send( from_=os.environ.get("TELNYX_FROM_NUMBER"), to=phone_number, text=f"Your verification code is: {otp}. It expires in 5 minutes.", ) return {"message_id": response.data.id, "expires_in": OTP_EXPIRY_SECONDS} def verify_otp(phone_number: str, submitted_otp: str) -> bool: """Verify the submitted OTP. Returns True if valid.""" record = otp_store.get(phone_number) if not record: return False # Check expiry if time.time() > record["expires_at"]: del otp_store[phone_number] return False # Check attempts record["attempts"] += 1 if record["attempts"] > record["max_attempts"]: del otp_store[phone_number] return False # Constant-time comparison to prevent timing attacks if secrets.compare_digest(record["otp"], submitted_otp): del otp_store[phone_number] # One-time use return True return False # Example usage result = send_otp("+15559876543") print(f"OTP sent, message ID: {result['message_id']}") # Later, when user submits the code: # is_valid = verify_otp("+15559876543", "123456") ``` ```javascript Node import Telnyx from 'telnyx'; import crypto from 'crypto'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); // In production, use Redis or a database const otpStore = new Map(); const OTP_LENGTH = 6; const OTP_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes function generateOtp() { // Cryptographically secure numeric OTP const bytes = crypto.randomBytes(OTP_LENGTH); return Array.from(bytes).map(b => b % 10).join(''); } async function sendOtp(phoneNumber) { const otp = generateOtp(); otpStore.set(phoneNumber, { otp, expiresAt: Date.now() + OTP_EXPIRY_MS, attempts: 0, maxAttempts: 3, }); const response = await client.messages.send({ from: process.env.TELNYX_FROM_NUMBER, to: phoneNumber, text: `Your verification code is: ${otp}. It expires in 5 minutes.`, }); return { messageId: response.data.id, expiresIn: OTP_EXPIRY_MS / 1000 }; } function verifyOtp(phoneNumber, submittedOtp) { const record = otpStore.get(phoneNumber); if (!record) return false; if (Date.now() > record.expiresAt) { otpStore.delete(phoneNumber); return false; } record.attempts++; if (record.attempts > record.maxAttempts) { otpStore.delete(phoneNumber); return false; } // Constant-time comparison if (crypto.timingSafeEqual( Buffer.from(record.otp), Buffer.from(submittedOtp) )) { otpStore.delete(phoneNumber); return true; } return false; } // Usage const result = await sendOtp('+15559876543'); console.log(`OTP sent, message ID: ${result.messageId}`); ``` ```ruby Ruby require "telnyx" require "securerandom" Telnyx.api_key = ENV["TELNYX_API_KEY"] # In production, use Redis or a database $otp_store = {} OTP_LENGTH = 6 OTP_EXPIRY_SECONDS = 300 # 5 minutes def generate_otp SecureRandom.random_number(10**OTP_LENGTH).to_s.rjust(OTP_LENGTH, "0") end def send_otp(phone_number) otp = generate_otp $otp_store[phone_number] = { otp: otp, expires_at: Time.now + OTP_EXPIRY_SECONDS, attempts: 0, max_attempts: 3 } response = Telnyx::Message.create( from: ENV["TELNYX_FROM_NUMBER"], to: phone_number, text: "Your verification code is: #{otp}. It expires in 5 minutes." ) { message_id: response.id, expires_in: OTP_EXPIRY_SECONDS } end def verify_otp(phone_number, submitted_otp) record = $otp_store[phone_number] return false unless record return ($otp_store.delete(phone_number); false) if Time.now > record[:expires_at] record[:attempts] += 1 return ($otp_store.delete(phone_number); false) if record[:attempts] > record[:max_attempts] if ActiveSupport::SecurityUtils.secure_compare(record[:otp], submitted_otp) $otp_store.delete(phone_number) true else false end end result = send_otp("+15559876543") puts "OTP sent, message ID: #{result[:message_id]}" ``` ```go Go package main import ( "context" "crypto/rand" "crypto/subtle" "fmt" "math/big" "os" "sync" "time" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) const ( otpLength = 6 otpExpirySeconds = 300 maxAttempts = 3 ) type OTPRecord struct { OTP string ExpiresAt time.Time Attempts int } var ( otpStore = make(map[string]*OTPRecord) mu sync.Mutex ) func generateOTP() string { otp := make([]byte, otpLength) for i := range otp { n, _ := rand.Int(rand.Reader, big.NewInt(10)) otp[i] = byte('0') + byte(n.Int64()) } return string(otp) } func sendOTP(client *telnyx.Client, phone string) (string, error) { otp := generateOTP() mu.Lock() otpStore[phone] = &OTPRecord{ OTP: otp, ExpiresAt: time.Now().Add(otpExpirySeconds * time.Second), } mu.Unlock() resp, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: os.Getenv("TELNYX_FROM_NUMBER"), To: phone, Text: fmt.Sprintf("Your verification code is: %s. It expires in 5 minutes.", otp), }) if err != nil { return "", err } return resp.Data.ID, nil } func verifyOTP(phone, submitted string) bool { mu.Lock() defer mu.Unlock() record, ok := otpStore[phone] if !ok || time.Now().After(record.ExpiresAt) { delete(otpStore, phone) return false } record.Attempts++ if record.Attempts > maxAttempts { delete(otpStore, phone) return false } if subtle.ConstantTimeCompare([]byte(record.OTP), []byte(submitted)) == 1 { delete(otpStore, phone) return true } return false } func main() { client := telnyx.NewClient(option.WithAPIKey(os.Getenv("TELNYX_API_KEY"))) msgID, err := sendOTP(client, "+15559876543") if err != nil { panic(err) } fmt.Printf("OTP sent, message ID: %s\n", msgID) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import java.security.MessageDigest; import java.security.SecureRandom; import java.time.Instant; import java.util.concurrent.ConcurrentHashMap; public final class OtpService { private static final int OTP_LENGTH = 6; private static final int OTP_EXPIRY_SECONDS = 300; private static final int MAX_ATTEMPTS = 3; private static final SecureRandom RANDOM = new SecureRandom(); record OtpRecord(String otp, Instant expiresAt, int attempts) { OtpRecord withAttempt() { return new OtpRecord(otp, expiresAt, attempts + 1); } } private final ConcurrentHashMap store = new ConcurrentHashMap<>(); private final TelnyxClient client; public OtpService() { this.client = TelnyxOkHttpClient.fromEnv(); } public String generateOtp() { StringBuilder sb = new StringBuilder(OTP_LENGTH); for (int i = 0; i < OTP_LENGTH; i++) { sb.append(RANDOM.nextInt(10)); } return sb.toString(); } public String sendOtp(String phoneNumber) { String otp = generateOtp(); store.put(phoneNumber, new OtpRecord( otp, Instant.now().plusSeconds(OTP_EXPIRY_SECONDS), 0 )); var params = MessageSendParams.builder() .from(System.getenv("TELNYX_FROM_NUMBER")) .to(phoneNumber) .text("Your verification code is: " + otp + ". It expires in 5 minutes.") .build(); var response = client.messages().send(params); return response.data().id(); } public boolean verifyOtp(String phoneNumber, String submitted) { OtpRecord record = store.get(phoneNumber); if (record == null || Instant.now().isAfter(record.expiresAt())) { store.remove(phoneNumber); return false; } OtpRecord updated = record.withAttempt(); store.put(phoneNumber, updated); if (updated.attempts() > MAX_ATTEMPTS) { store.remove(phoneNumber); return false; } if (MessageDigest.isEqual( record.otp().getBytes(), submitted.getBytes())) { store.remove(phoneNumber); return true; } return false; } public static void main(String[] args) { OtpService service = new OtpService(); String msgId = service.sendOtp("+15559876543"); System.out.println("OTP sent, message ID: " + msgId); } } ``` ```csharp .NET using System; using System.Collections.Concurrent; using System.Security.Cryptography; using Telnyx; public class OtpService { private const int OtpLength = 6; private static readonly TimeSpan OtpExpiry = TimeSpan.FromMinutes(5); private const int MaxAttempts = 3; private record OtpRecord(string Otp, DateTime ExpiresAt, int Attempts); private readonly ConcurrentDictionary _store = new(); public async Task SendOtpAsync(string phoneNumber) { var otp = GenerateOtp(); _store[phoneNumber] = new OtpRecord(otp, DateTime.UtcNow + OtpExpiry, 0); TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = Environment.GetEnvironmentVariable("TELNYX_FROM_NUMBER"), To = phoneNumber, Text = $"Your verification code is: {otp}. It expires in 5 minutes." }); return response.Data.Id; } public bool VerifyOtp(string phoneNumber, string submitted) { if (!_store.TryGetValue(phoneNumber, out var record)) return false; if (DateTime.UtcNow > record.ExpiresAt) { _store.TryRemove(phoneNumber, out _); return false; } var updated = record with { Attempts = record.Attempts + 1 }; _store[phoneNumber] = updated; if (updated.Attempts > MaxAttempts) { _store.TryRemove(phoneNumber, out _); return false; } if (CryptographicOperations.FixedTimeEquals( System.Text.Encoding.UTF8.GetBytes(record.Otp), System.Text.Encoding.UTF8.GetBytes(submitted))) { _store.TryRemove(phoneNumber, out _); return true; } return false; } private static string GenerateOtp() { var bytes = RandomNumberGenerator.GetBytes(OtpLength); return string.Concat(Array.ConvertAll(bytes, b => (b % 10).ToString())); } } ``` ```php PHP $otp, 'expires_at' => time() + OTP_EXPIRY_SECONDS, 'attempts' => 0, ]; $response = \Telnyx\Message::Create([ 'from' => getenv('TELNYX_FROM_NUMBER'), 'to' => $phoneNumber, 'text' => "Your verification code is: {$otp}. It expires in 5 minutes.", ]); return ['message_id' => $response->id, 'expires_in' => OTP_EXPIRY_SECONDS]; } function verifyOtp(string $phoneNumber, string $submitted): bool { global $otpStore; if (!isset($otpStore[$phoneNumber])) { return false; } $record = &$otpStore[$phoneNumber]; if (time() > $record['expires_at']) { unset($otpStore[$phoneNumber]); return false; } $record['attempts']++; if ($record['attempts'] > MAX_ATTEMPTS) { unset($otpStore[$phoneNumber]); return false; } if (hash_equals($record['otp'], $submitted)) { unset($otpStore[$phoneNumber]); return true; } return false; } // Usage $result = sendOtp('+15559876543'); echo "OTP sent, message ID: {$result['message_id']}\n"; ``` --- ## Security best practices Never use `Math.random()`, `rand()`, or similar non-cryptographic functions for OTP generation. Use: | Language | Secure Function | |----------|----------------| | Python | `secrets.choice()` or `secrets.token_hex()` | | Node | `crypto.randomBytes()` | | Ruby | `SecureRandom.random_number()` | | Go | `crypto/rand.Int()` | | Java | `SecureRandom.nextInt()` | | .NET | `RandomNumberGenerator.GetBytes()` | | PHP | `random_int()` | OTPs should expire after a short window (3-5 minutes is typical). Never allow OTPs to be valid indefinitely. - Store the expiry timestamp alongside the OTP - Check expiry before validating - Delete expired OTPs proactively Allow a maximum of 3 verification attempts per OTP. After exceeding the limit, invalidate the OTP and require the user to request a new one. This prevents brute-force attacks on short numeric codes. Always use constant-time string comparison when verifying OTPs to prevent timing attacks: | Language | Function | |----------|----------| | Python | `secrets.compare_digest()` | | Node | `crypto.timingSafeEqual()` | | Go | `subtle.ConstantTimeCompare()` | | Java | `MessageDigest.isEqual()` | | .NET | `CryptographicOperations.FixedTimeEquals()` | | PHP | `hash_equals()` | Prevent abuse by limiting how frequently a user can request new OTPs: - **Per phone number:** Maximum 1 OTP request per 60 seconds - **Per IP address:** Maximum 10 OTP requests per hour - **Per account:** Maximum 5 OTP requests per hour Return the same "OTP sent" response regardless of whether the number exists in your system to prevent enumeration attacks. Use numeric-only OTPs (e.g., `847291`) rather than alphanumeric codes. They are: - Easier for users to type on mobile - Compatible with SMS autofill on iOS and Android - Sufficient security when combined with attempt limits and expiry A 6-digit code has 1,000,000 possible values — with a 3-attempt limit, the probability of guessing correctly is 0.0003%. iOS and Android can automatically detect and fill OTP codes from SMS. To enable this: **Android (SMS Retriever API):** Include your app's hash at the end of the message: ``` Your verification code is: 847291 FA+9qCX9VSu ``` **iOS:** iOS automatically detects codes from messages containing "code" or "passcode." No special formatting needed, but keeping the OTP on its own line helps. --- ## When to use the Verify API instead The [Telnyx Verify API](/docs/identity/verify/index) is a better choice when: | Requirement | DIY (this guide) | Verify API | |-------------|-------------------|------------| | OTP generation & storage | You build it | Handled for you | | Retry logic | You build it | Built-in | | Rate limiting | You build it | Built-in | | Multi-channel (SMS + Voice + WhatsApp) | Separate implementation | Single API | | Delivery status tracking | Manual webhook handling | Built-in | | Compliance & audit logging | You build it | Built-in | Use this guide's DIY approach only when you need full control over the OTP flow, custom message templates, or integration with existing verification systems. --- ## Related resources Managed OTP verification with built-in rate limiting and multi-channel support. Get started with the Verify API in minutes. Understand message throughput limits for OTP delivery. Get started with the Telnyx Messaging API. --- ### Appointment Reminders > Source: https://developers.telnyx.com/docs/messaging/messages/appointment-reminder.md Reduce no-shows by sending automated SMS appointment reminders with the Telnyx Messaging API. This guide covers scheduling strategies, message templates, opt-out handling, and timing best practices. ## How it works ```mermaid sequenceDiagram participant App as Your Application participant Scheduler as Job Scheduler participant Telnyx as Telnyx API participant Patient as Recipient App->>App: Appointment booked App->>Scheduler: Schedule reminder(s) Note over Scheduler: Wait until reminder time Scheduler->>App: Trigger reminder job App->>Telnyx: Send SMS Telnyx->>Patient: Deliver reminder Patient->>Telnyx: Reply CONFIRM / CANCEL Telnyx->>App: Webhook with reply App->>App: Update appointment status ``` --- ## Send an appointment reminder ```python Python import os from datetime import datetime from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) def send_reminder(to: str, patient_name: str, appointment_time: datetime, location: str): """Send an appointment reminder SMS.""" formatted_time = appointment_time.strftime("%A, %B %d at %I:%M %p") response = client.messages.send( from_=os.environ.get("TELNYX_FROM_NUMBER"), to=to, text=( f"Hi {patient_name}, this is a reminder for your appointment " f"on {formatted_time} at {location}. " f"Reply CONFIRM to confirm or CANCEL to cancel." ), ) return response.data # Example send_reminder( to="+15559876543", patient_name="Jane", appointment_time=datetime(2026, 3, 15, 14, 30), location="123 Main St, Suite 200", ) ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); async function sendReminder(to, patientName, appointmentTime, location) { const formatted = appointmentTime.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', }); const response = await client.messages.send({ from: process.env.TELNYX_FROM_NUMBER, to, text: `Hi ${patientName}, this is a reminder for your appointment on ${formatted} at ${location}. Reply CONFIRM to confirm or CANCEL to cancel.`, }); return response.data; } // Example await sendReminder( '+15559876543', 'Jane', new Date('2026-03-15T14:30:00'), '123 Main St, Suite 200' ); ``` ```ruby Ruby require "telnyx" Telnyx.api_key = ENV["TELNYX_API_KEY"] def send_reminder(to:, patient_name:, appointment_time:, location:) formatted = appointment_time.strftime("%A, %B %d at %I:%M %p") Telnyx::Message.create( from: ENV["TELNYX_FROM_NUMBER"], to: to, text: "Hi #{patient_name}, this is a reminder for your appointment " \ "on #{formatted} at #{location}. " \ "Reply CONFIRM to confirm or CANCEL to cancel." ) end # Example send_reminder( to: "+15559876543", patient_name: "Jane", appointment_time: Time.new(2026, 3, 15, 14, 30), location: "123 Main St, Suite 200" ) ``` ```go Go package main import ( "context" "fmt" "os" "time" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func sendReminder(client *telnyx.Client, to, name, location string, apptTime time.Time) error { formatted := apptTime.Format("Monday, January 2 at 3:04 PM") _, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: os.Getenv("TELNYX_FROM_NUMBER"), To: to, Text: fmt.Sprintf( "Hi %s, this is a reminder for your appointment on %s at %s. "+ "Reply CONFIRM to confirm or CANCEL to cancel.", name, formatted, location, ), }) return err } func main() { client := telnyx.NewClient(option.WithAPIKey(os.Getenv("TELNYX_API_KEY"))) appt := time.Date(2026, 3, 15, 14, 30, 0, 0, time.Local) err := sendReminder(client, "+15559876543", "Jane", "123 Main St, Suite 200", appt) if err != nil { panic(err) } fmt.Println("Reminder sent!") } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public final class AppointmentReminder { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("EEEE, MMMM d 'at' h:mm a"); public static String sendReminder(String to, String name, LocalDateTime apptTime, String location) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); String formatted = apptTime.format(FORMATTER); var params = MessageSendParams.builder() .from(System.getenv("TELNYX_FROM_NUMBER")) .to(to) .text(String.format( "Hi %s, this is a reminder for your appointment on %s at %s. " + "Reply CONFIRM to confirm or CANCEL to cancel.", name, formatted, location)) .build(); var response = client.messages().send(params); return response.data().id(); } public static void main(String[] args) { sendReminder("+15559876543", "Jane", LocalDateTime.of(2026, 3, 15, 14, 30), "123 Main St, Suite 200"); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); async Task SendReminderAsync(string to, string name, DateTime apptTime, string location) { var formatted = apptTime.ToString("dddd, MMMM d 'at' h:mm tt"); var service = new MessageService(); await service.SendAsync(new MessageSendOptions { From = Environment.GetEnvironmentVariable("TELNYX_FROM_NUMBER"), To = to, Text = $"Hi {name}, this is a reminder for your appointment on {formatted} at {location}. " + "Reply CONFIRM to confirm or CANCEL to cancel." }); } await SendReminderAsync("+15559876543", "Jane", new DateTime(2026, 3, 15, 14, 30, 0), "123 Main St, Suite 200"); ``` ```php PHP format('l, F j \a\t g:i A'); \Telnyx\Message::Create([ 'from' => getenv('TELNYX_FROM_NUMBER'), 'to' => $to, 'text' => "Hi {$name}, this is a reminder for your appointment " . "on {$formatted} at {$location}. " . "Reply CONFIRM to confirm or CANCEL to cancel.", ]); } sendReminder('+15559876543', 'Jane', new DateTime('2026-03-15 14:30:00'), '123 Main St, Suite 200'); ``` --- ## Scheduling strategies Choose a scheduling approach based on your application's requirements: The simplest approach — use the Telnyx API's built-in [scheduled messaging](/docs/messaging/messages/schedule-message/index) feature. No external scheduler needed. ```python Python from datetime import datetime, timedelta, timezone # Schedule reminder 24 hours before appointment reminder_time = appointment_time - timedelta(hours=24) response = client.messages.send( from_=os.environ.get("TELNYX_FROM_NUMBER"), to="+15559876543", text="Reminder: You have an appointment tomorrow at 2:30 PM.", send_at=reminder_time.astimezone(timezone.utc).isoformat(), ) ``` **Pros:** No infrastructure needed, simple API call **Cons:** Limited to single scheduled time per API call, max 7 days in advance Run a periodic job (e.g., every hour) that queries your database for upcoming appointments and sends reminders. ```python Python # Example cron job (runs hourly) from datetime import datetime, timedelta def send_pending_reminders(): """Find appointments in the next 24-25 hours and send reminders.""" now = datetime.now() window_start = now + timedelta(hours=23) window_end = now + timedelta(hours=25) # Query your database appointments = db.query( "SELECT * FROM appointments " "WHERE start_time BETWEEN %s AND %s " "AND reminder_sent = FALSE", (window_start, window_end) ) for appt in appointments: send_reminder( to=appt.phone, patient_name=appt.name, appointment_time=appt.start_time, location=appt.location, ) db.execute( "UPDATE appointments SET reminder_sent = TRUE WHERE id = %s", (appt.id,) ) ``` **Pros:** Full control, supports multiple reminder windows, database-driven **Cons:** Requires job scheduler infrastructure (cron, Celery, Bull, etc.) Schedule individual reminder jobs when appointments are created using a task queue like Celery (Python), Bull (Node), or Sidekiq (Ruby). ```python Python from celery import Celery from datetime import timedelta celery_app = Celery('reminders', broker='redis://localhost:6379') @celery_app.task def send_scheduled_reminder(phone, name, time_str, location): appointment_time = datetime.fromisoformat(time_str) send_reminder(to=phone, patient_name=name, appointment_time=appointment_time, location=location) # When appointment is booked, schedule the reminder def on_appointment_created(appointment): reminder_time = appointment.start_time - timedelta(hours=24) send_scheduled_reminder.apply_async( args=[appointment.phone, appointment.name, appointment.start_time.isoformat(), appointment.location], eta=reminder_time, ) ``` **Pros:** Precise timing, scalable, handles cancellations **Cons:** Requires message queue infrastructure (Redis, RabbitMQ) --- ## Handle replies (confirm / cancel) Set up a webhook to receive replies and update appointment status: ```python Python from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/webhooks/messaging", methods=["POST"]) def handle_webhook(): data = request.json["data"] if data["event_type"] != "message.received": return jsonify({"status": "ignored"}), 200 payload = data["payload"] from_number = payload["from"]["phone_number"] text = payload["text"].strip().upper() if text == "CONFIRM": # Update appointment status in your database db.execute( "UPDATE appointments SET status = 'confirmed' WHERE phone = %s " "AND start_time > NOW()", (from_number,) ) # Send confirmation client.messages.send( from_=os.environ.get("TELNYX_FROM_NUMBER"), to=from_number, text="Your appointment has been confirmed. See you then!", ) elif text == "CANCEL": db.execute( "UPDATE appointments SET status = 'cancelled' WHERE phone = %s " "AND start_time > NOW()", (from_number,) ) client.messages.send( from_=os.environ.get("TELNYX_FROM_NUMBER"), to=from_number, text="Your appointment has been cancelled. " "Please call us to reschedule.", ) return jsonify({"status": "ok"}), 200 ``` ```javascript Node import express from 'express'; const app = express(); app.use(express.json()); app.post('/webhooks/messaging', async (req, res) => { const { data } = req.body; if (data.event_type !== 'message.received') { return res.json({ status: 'ignored' }); } const fromNumber = data.payload.from.phone_number; const text = data.payload.text.trim().toUpperCase(); if (text === 'CONFIRM') { await db.query( `UPDATE appointments SET status = 'confirmed' WHERE phone = $1 AND start_time > NOW()`, [fromNumber] ); await client.messages.send({ from: process.env.TELNYX_FROM_NUMBER, to: fromNumber, text: 'Your appointment has been confirmed. See you then!', }); } else if (text === 'CANCEL') { await db.query( `UPDATE appointments SET status = 'cancelled' WHERE phone = $1 AND start_time > NOW()`, [fromNumber] ); await client.messages.send({ from: process.env.TELNYX_FROM_NUMBER, to: fromNumber, text: 'Your appointment has been cancelled. Please call us to reschedule.', }); } res.json({ status: 'ok' }); }); ``` --- ## Opt-out handling You **must** honor opt-out requests. Telnyx automatically handles STOP/UNSTOP keywords for 10DLC and Toll-Free numbers, but you should also track opt-outs in your application. Telnyx automatically handles standard opt-out keywords (`STOP`, `UNSUBSCRIBE`, `CANCEL`, `END`, `QUIT`) for US long codes and toll-free numbers. When a user texts STOP: 1. Telnyx sends an automatic reply confirming the opt-out 2. Future messages to that number are blocked at the carrier level 3. You receive a `message.received` webhook with the STOP keyword See [Advanced Opt-In/Out](/docs/messaging/messages/advanced-opt-in-out/index) for customization options. In addition to Telnyx's automatic handling, track opt-outs in your database to prevent scheduling reminders for opted-out users: ```python def handle_opt_out(phone_number: str): """Mark a phone number as opted out.""" db.execute( "UPDATE patients SET sms_opted_out = TRUE WHERE phone = %s", (phone_number,) ) # Cancel any pending reminders db.execute( "DELETE FROM scheduled_reminders WHERE phone = %s AND sent = FALSE", (phone_number,) ) def can_send_reminder(phone_number: str) -> bool: """Check if we can send a reminder to this number.""" result = db.query( "SELECT sms_opted_out FROM patients WHERE phone = %s", (phone_number,) ) return result and not result.sms_opted_out ``` --- ## Timing best practices - **24 hours before:** Primary reminder — enough time to cancel/reschedule - **2-3 hours before:** Final reminder for same-day appointments - **Avoid late night/early morning:** Only send between 9 AM and 8 PM in the recipient's local time zone For high-value appointments (medical, legal), send two reminders: 1. 48 or 24 hours before — gives time to reschedule 2. 2-3 hours before — final confirmation For routine appointments (salon, auto service), a single reminder 24 hours before is usually sufficient. Always calculate reminder times in the recipient's local time zone. Sending a reminder at 3 AM is worse than not sending one at all. ```python from zoneinfo import ZoneInfo # Store patient timezone in your database patient_tz = ZoneInfo(patient.timezone) # e.g., "America/New_York" local_time = reminder_time.astimezone(patient_tz) # Only send between 9 AM and 8 PM local time if 9 <= local_time.hour < 20: send_reminder(...) else: # Reschedule to 9 AM local time next_9am = local_time.replace(hour=9, minute=0) if next_9am < local_time: next_9am += timedelta(days=1) schedule_reminder_at(next_9am, ...) ``` SMS has character limits. Keep reminders under 160 characters (1 segment) when possible to minimize costs. Include only essential info: - Patient name - Date and time - Location (short form) - Reply instructions --- ## Message templates **Healthcare:** ``` Hi {name}, reminder: your appointment with Dr. {provider} is on {date} at {time}. Reply CONFIRM or CANCEL. Call {phone} to reschedule. ``` **Dental:** ``` {name}, your dental cleaning at {practice} is tomorrow at {time}. Please arrive 10 min early. Reply C to confirm, X to cancel. ``` **Salon / Spa:** ``` Hi {name}! Your {service} appointment is {date} at {time}. Reply YES to confirm or call {phone} to reschedule. ``` **Auto Service:** ``` {name}, your vehicle service at {shop} is scheduled for {date} at {time}. Reply OK to confirm. ``` **Legal / Financial:** ``` Reminder: Your meeting with {advisor} is on {date} at {time} at {location}. Please bring required documents. Reply CONFIRM to confirm. ``` --- ## Related resources Use the Telnyx API to schedule messages for future delivery. Customize opt-in/out behavior for your messaging profile. Receive inbound messages and delivery status updates. Understand throughput limits for bulk reminder sending. --- ### Send & Receive MMS > Source: https://developers.telnyx.com/docs/messaging/messages/send-receive-mms.md Send multimedia messages (images, video, audio, vCards) via the Telnyx API and process inbound MMS attachments from webhooks. ```mermaid sequenceDiagram participant App as Your App participant TLX as Telnyx API participant Carrier participant User as Recipient Note over App,User: Sending MMS App->>TLX: POST /messages + media_urls TLX->>TLX: Download media from URLs TLX->>Carrier: Deliver MMS Carrier->>User: MMS received TLX->>App: Webhook: message.sent TLX->>App: Webhook: message.finalized Note over App,User: Receiving MMS User->>Carrier: Send MMS Carrier->>TLX: Inbound MMS TLX->>App: Webhook: message.received + media[] App->>App: Download & store media ``` ## Prerequisites - A [Telnyx account](https://telnyx.com/sign-up) with [API key](https://portal.telnyx.com/#/app/api-keys) - A [messaging profile](https://portal.telnyx.com/#/app/messaging) with a phone number enabled for MMS - A webhook endpoint to receive inbound messages (see [ngrok setup](/development/development-tools/ngrok-setup/index)) MMS is supported on US/Canada long codes, toll-free, and short codes. For media format details and carrier limits, see [MMS Media & Transcoding](/docs/messaging/messages/mms-transcoding/index). --- ## Send an MMS Include `media_urls` in your message request. You can send up to 10 media files per message. ```python Python import telnyx telnyx.api_key = "YOUR_API_KEY" message = telnyx.Message.create( from_="+18005550100", to="+18005550101", text="Here's the photo you requested!", media_urls=["https://example.com/image.jpg"], messaging_profile_id="YOUR_MESSAGING_PROFILE_ID" ) print(f"Message ID: {message.id}") print(f"Status: {message.to[0]['status']}") ``` ```javascript Node const telnyx = require("telnyx")("YOUR_API_KEY"); const message = await telnyx.messages.create({ from: "+18005550100", to: "+18005550101", text: "Here's the photo you requested!", media_urls: ["https://example.com/image.jpg"], messaging_profile_id: "YOUR_MESSAGING_PROFILE_ID", }); console.log(`Message ID: ${message.data.id}`); console.log(`Status: ${message.data.to[0].status}`); ``` ```ruby Ruby require "telnyx" Telnyx.api_key = "YOUR_API_KEY" message = Telnyx::Message.create( from: "+18005550100", to: "+18005550101", text: "Here's the photo you requested!", media_urls: ["https://example.com/image.jpg"], messaging_profile_id: "YOUR_MESSAGING_PROFILE_ID" ) puts "Message ID: #{message.id}" puts "Status: #{message.to[0]['status']}" ``` ```java Java import com.telnyx.sdk.*; import com.telnyx.sdk.api.MessagesApi; import com.telnyx.sdk.model.*; import java.util.Arrays; ApiClient client = Configuration.getDefaultApiClient(); client.setBearerToken("YOUR_API_KEY"); MessagesApi api = new MessagesApi(client); CreateMessageRequest req = new CreateMessageRequest() .from("+18005550100") .to("+18005550101") .text("Here's the photo you requested!") .mediaUrls(Arrays.asList("https://example.com/image.jpg")) .messagingProfileId("YOUR_MESSAGING_PROFILE_ID"); MessageResponse resp = api.createMessage(req); System.out.println("Message ID: " + resp.getData().getId()); ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey("YOUR_API_KEY"); var service = new MessagingSenderIdService(); var options = new NewMessage { From = "+18005550100", To = "+18005550101", Text = "Here's the photo you requested!", MediaUrls = new List { "https://example.com/image.jpg" }, MessagingProfileId = "YOUR_MESSAGING_PROFILE_ID" }; var message = service.Create(options); Console.WriteLine($"Message ID: {message.Id}"); ``` ```php PHP $telnyx = new \Telnyx\Telnyx("YOUR_API_KEY"); $message = \Telnyx\Message::create([ "from" => "+18005550100", "to" => "+18005550101", "text" => "Here's the photo you requested!", "media_urls" => ["https://example.com/image.jpg"], "messaging_profile_id" => "YOUR_MESSAGING_PROFILE_ID" ]); echo "Message ID: " . $message->id . "\n"; ``` ```go Go package main import ( "context" "fmt" telnyx "github.com/telnyx/telnyx-go" ) func main() { client := telnyx.NewClient("YOUR_API_KEY") message, err := client.Messages.Create(context.Background(), &telnyx.MessageParams{ From: "+18005550100", To: "+18005550101", Text: "Here's the photo you requested!", MediaURLs: []string{"https://example.com/image.jpg"}, MessagingProfileID: "YOUR_MESSAGING_PROFILE_ID", }) if err != nil { panic(err) } fmt.Printf("Message ID: %s\n", message.ID) } ``` ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+18005550100", "to": "+18005550101", "text": "Here'\''s the photo you requested!", "media_urls": ["https://example.com/image.jpg"], "messaging_profile_id": "YOUR_MESSAGING_PROFILE_ID" }' ``` ### Send multiple media files Include multiple URLs in `media_urls`. Total payload must stay under carrier limits. ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+18005550100", "to": "+18005550101", "text": "Product photos attached", "media_urls": [ "https://example.com/photo1.jpg", "https://example.com/photo2.jpg", "https://example.com/photo3.jpg" ], "messaging_profile_id": "YOUR_MESSAGING_PROFILE_ID" }' ``` Media URLs must be publicly accessible. Telnyx downloads the media at send time — if the URL requires authentication or returns an error, the message will fail. --- ## Receive an MMS Inbound MMS messages arrive as webhooks to your messaging profile's webhook URL. The `media` array contains attachment details. ### Webhook payload ```json { "data": { "event_type": "message.received", "payload": { "from": { "phone_number": "+18005550101" }, "to": [{ "phone_number": "+18005550100" }], "text": "Check out this photo!", "media": [ { "url": "https://media.telnyx.com/abc123/image.jpg", "content_type": "image/jpeg", "size": 245760 } ] } } } ``` **Media URLs are ephemeral.** Telnyx-hosted media links expire. Download and store attachments in your own storage immediately upon receipt. ### Process inbound MMS ```python Python from flask import Flask, request, jsonify import requests import os app = Flask(__name__) TELNYX_API_KEY = os.getenv("TELNYX_API_KEY") MEDIA_DIR = "./received_media" os.makedirs(MEDIA_DIR, exist_ok=True) @app.route("/webhooks", methods=["POST"]) def webhooks(): body = request.json event_type = body["data"]["event_type"] if event_type != "message.received": return jsonify({"status": "ignored"}), 200 payload = body["data"]["payload"] from_number = payload["from"]["phone_number"] text = payload.get("text", "") media = payload.get("media", []) print(f"From: {from_number} | Text: {text} | Attachments: {len(media)}") # Download each attachment saved_files = [] for item in media: resp = requests.get(item["url"]) ext = item["content_type"].split("/")[-1] filename = f"{MEDIA_DIR}/{from_number}_{len(saved_files)}.{ext}" with open(filename, "wb") as f: f.write(resp.content) saved_files.append(filename) print(f" Saved: {filename} ({item['size']} bytes)") return jsonify({"status": "ok", "files": len(saved_files)}), 200 if __name__ == "__main__": app.run(port=8000) ``` ```javascript Node const express = require("express"); const axios = require("axios"); const fs = require("fs"); const path = require("path"); const app = express(); app.use(express.json()); const MEDIA_DIR = "./received_media"; fs.mkdirSync(MEDIA_DIR, { recursive: true }); app.post("/webhooks", async (req, res) => { const { event_type, payload } = req.body.data; if (event_type !== "message.received") { return res.json({ status: "ignored" }); } const from = payload.from.phone_number; const text = payload.text || ""; const media = payload.media || []; console.log(`From: ${from} | Text: ${text} | Attachments: ${media.length}`); for (let i = 0; i < media.length; i++) { const resp = await axios.get(media[i].url, { responseType: "arraybuffer" }); const ext = media[i].content_type.split("/").pop(); const filename = path.join(MEDIA_DIR, `${from}_${i}.${ext}`); fs.writeFileSync(filename, resp.data); console.log(` Saved: ${filename} (${media[i].size} bytes)`); } res.json({ status: "ok", files: media.length }); }); app.listen(8000, () => console.log("Listening on port 8000")); ``` ```ruby Ruby require "sinatra" require "net/http" require "json" require "fileutils" MEDIA_DIR = "./received_media" FileUtils.mkdir_p(MEDIA_DIR) post "/webhooks" do body = JSON.parse(request.body.read) event_type = body.dig("data", "event_type") return { status: "ignored" }.to_json unless event_type == "message.received" payload = body.dig("data", "payload") from = payload.dig("from", "phone_number") media = payload.fetch("media", []) puts "From: #{from} | Attachments: #{media.length}" media.each_with_index do |item, i| uri = URI(item["url"]) resp = Net::HTTP.get(uri) ext = item["content_type"].split("/").last filename = "#{MEDIA_DIR}/#{from}_#{i}.#{ext}" File.binwrite(filename, resp) puts " Saved: #{filename}" end { status: "ok" }.to_json end ``` ```go Go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" ) const mediaDir = "./received_media" type Webhook struct { Data struct { EventType string `json:"event_type"` Payload struct { From struct{ PhoneNumber string `json:"phone_number"` } `json:"from"` Text string `json:"text"` Media []struct { URL string `json:"url"` ContentType string `json:"content_type"` Size int `json:"size"` } `json:"media"` } `json:"payload"` } `json:"data"` } func handler(w http.ResponseWriter, r *http.Request) { var wh Webhook json.NewDecoder(r.Body).Decode(&wh) if wh.Data.EventType != "message.received" { json.NewEncoder(w).Encode(map[string]string{"status": "ignored"}) return } from := wh.Data.Payload.From.PhoneNumber fmt.Printf("From: %s | Attachments: %d\n", from, len(wh.Data.Payload.Media)) for i, item := range wh.Data.Payload.Media { resp, _ := http.Get(item.URL) defer resp.Body.Close() ext := strings.Split(item.ContentType, "/")[1] filename := filepath.Join(mediaDir, fmt.Sprintf("%s_%d.%s", from, i, ext)) f, _ := os.Create(filename) io.Copy(f, resp.Body) f.Close() fmt.Printf(" Saved: %s\n", filename) } json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func main() { os.MkdirAll(mediaDir, 0755) http.HandleFunc("/webhooks", handler) fmt.Println("Listening on port 8000") http.ListenAndServe(":8000", nil) } ``` ```bash curl # Simulate receiving — inspect your webhook logs # The webhook POST body contains media URLs you can download: curl -o attachment.jpg "https://media.telnyx.com/abc123/image.jpg" ``` --- ## Reply with media Echo received media back to the sender, or reply with different media: ```python Python import telnyx import os telnyx.api_key = os.getenv("TELNYX_API_KEY") def handle_mms_webhook(payload): """Reply to inbound MMS with the same media + a text response.""" from_number = payload["from"]["phone_number"] to_number = payload["to"][0]["phone_number"] media = payload.get("media", []) # Reply with the same media echoed back media_urls = [item["url"] for item in media] reply = telnyx.Message.create( from_=to_number, to=from_number, text=f"Thanks! Received {len(media)} attachment(s).", media_urls=media_urls if media_urls else None, messaging_profile_id="YOUR_MESSAGING_PROFILE_ID" ) print(f"Reply sent: {reply.id}") ``` ```javascript Node const telnyx = require("telnyx")(process.env.TELNYX_API_KEY); async function handleMmsWebhook(payload) { const from = payload.from.phone_number; const to = payload.to[0].phone_number; const media = payload.media || []; const mediaUrls = media.map((m) => m.url); const reply = await telnyx.messages.create({ from: to, to: from, text: `Thanks! Received ${media.length} attachment(s).`, media_urls: mediaUrls.length ? mediaUrls : undefined, messaging_profile_id: "YOUR_MESSAGING_PROFILE_ID", }); console.log(`Reply sent: ${reply.data.id}`); } ``` --- ## Supported media types | Type | Formats | Max Size | |------|---------|----------| | **Images** | JPEG, PNG, GIF, BMP, WebP | 1 MB (carrier-dependent) | | **Video** | MP4, 3GP | 600 KB (carrier-dependent) | | **Audio** | MP3, AMR, WAV, OGG | 600 KB (carrier-dependent) | | **Files** | vCard (.vcf), PDF | 600 KB | Telnyx automatically transcodes oversized media when possible. For details on carrier-specific limits and transcoding behavior, see [MMS Media & Transcoding](/docs/messaging/messages/mms-transcoding/index). --- ## Store media externally (optional) For production use, store received media in your own cloud storage rather than relying on ephemeral Telnyx URLs. ```python Python import boto3 import requests from urllib.parse import urlparse s3 = boto3.client("s3") BUCKET = "your-mms-bucket" def save_to_s3(media_url, from_number, index): resp = requests.get(media_url) content_type = resp.headers.get("content-type", "application/octet-stream") ext = content_type.split("/")[-1] key = f"mms/{from_number}/{index}.{ext}" s3.put_object( Bucket=BUCKET, Key=key, Body=resp.content, ContentType=content_type ) return f"s3://{BUCKET}/{key}" ``` ```python Python from google.cloud import storage import requests gcs = storage.Client() bucket = gcs.bucket("your-mms-bucket") def save_to_gcs(media_url, from_number, index): resp = requests.get(media_url) content_type = resp.headers.get("content-type", "application/octet-stream") ext = content_type.split("/")[-1] blob = bucket.blob(f"mms/{from_number}/{index}.{ext}") blob.upload_from_string(resp.content, content_type=content_type) return blob.public_url ``` --- ## Troubleshooting **Cause:** The media URL was unreachable, or the recipient's carrier doesn't support MMS. **Fix:** - Verify the media URL is publicly accessible (no auth required) - Check [message detail records](/docs/messaging/messages/message-detail-records/index) for error details - Confirm the recipient's number supports MMS **Cause:** Total media payload exceeds carrier limits. **Fix:** - Compress images before sending (aim for < 600 KB each) - Enable automatic transcoding (on by default) - See [carrier size limits](/docs/messaging/messages/mms-transcoding/index#carrier-size-limits) **Cause:** Telnyx media URLs are temporary. You waited too long to download. **Fix:** Download media immediately in your webhook handler. Store in your own S3/GCS bucket. **Cause:** Some number types (e.g., alphanumeric sender IDs) don't support MMS. **Fix:** Use a US/Canada long code, toll-free, or short code with MMS enabled in your [messaging profile](https://portal.telnyx.com/#/app/messaging). --- ## Next steps Carrier limits, supported formats, and automatic transcoding. Set up and secure your webhook endpoint. SMS sending guide with all SDK examples. Send MMS to multiple recipients. --- ### Zapier Integration > Source: https://developers.telnyx.com/docs/messaging/messages/zapier-integration.md Connect Telnyx SMS messaging to 7,000+ apps using [Zapier](https://zapier.com/apps/telnyx/integrations) — no code required. Build automated workflows that send messages, forward inbound SMS, and respond to triggers from other services. **When to use Zapier vs. the API:** Zapier is ideal for simple automations, prototyping, and connecting to third-party services without code. For high-volume messaging, complex logic, or production-critical workflows, use the [Telnyx Messaging API](/docs/messaging/messages/send-message/index) directly. ## Prerequisites - A [Telnyx account](https://portal.telnyx.com) with a messaging-enabled phone number - A [Messaging Profile](/docs/messaging/messages/messaging-profiles-overview/index) configured with a webhook URL - A [Zapier account](https://zapier.com/) (free tier available) - Your Telnyx [v2 API Key](https://portal.telnyx.com/#/app/auth/v2) --- ## Connect Telnyx to Zapier Go to the [Telnyx Zapier integration page](https://zapier.com/apps/telnyx/integrations) and click **Connect**. Enter your Telnyx v2 API key when prompted. This gives Zapier permission to send and receive messages on your behalf. After authenticating, Zapier displays all phone numbers on your Telnyx account. Select the number you want to use for sending. Zapier does not support selecting a messaging profile directly — it lists individual numbers from your account. The number you choose must be assigned to a messaging profile with a webhook URL configured for inbound triggers to work. --- ## Available triggers and actions | Type | Name | Description | |------|------|-------------| | **Trigger** | Receive a Message | Fires when an SMS/MMS is received on your Telnyx number | | **Action** | Send SMS | Sends an SMS message from your Telnyx number | --- ## Example workflows ### Send SMS alerts from any app Connect any Zapier trigger to Telnyx SMS to send notifications: **Popular combinations:** - **Google Sheets** → new row → send SMS (e.g., new lead notification) - **Shopify** → new order → send SMS confirmation - **Google Calendar** → event starting → send SMS reminder - **Stripe** → payment received → send SMS receipt - **Typeform** → new response → send SMS follow-up **Setup:** 1. Create a new Zap and select your trigger app (e.g., Google Sheets) 2. Configure the trigger event (e.g., "New Spreadsheet Row") 3. Add **Telnyx** as the action and select **Send SMS** 4. Map fields from the trigger into the message: - **Source Number:** Select a Telnyx number from the dropdown — this is a fixed number from your account, not a mapped field from the trigger - **Destination Number:** Use a field from the trigger (e.g., a phone number column from Google Sheets) or enter a fixed number - **Message Content:** Compose your message using trigger data 5. Test and turn on the Zap ### Forward inbound SMS to your phone Route messages received on your Telnyx number to your personal phone — useful for monitoring business numbers. **Setup:** 1. Create a new Zap with **Telnyx → Receive a Message** as the trigger 2. Add **Telnyx → Send SMS** as the action 3. Configure the action: - **Source Number:** Your Telnyx number - **Destination Number:** Your personal number - **Message Content:** Use trigger fields: ``` FWD FROM: {{From Phone Number}} BODY: {{Text}} ``` 4. Test and turn on the Zap You can also forward to email, Slack, or any other Zapier-supported app instead of (or in addition to) SMS. ### Automatically reply to inbound messages Set up automatic replies for inbound messages — useful for business hours notices, keyword responses, or acknowledgments. **Setup:** 1. Create a new Zap with **Telnyx → Receive a Message** as the trigger 2. (Optional) Add a **Filter by Zapier** step to only reply to certain messages — for example, filter where `{{Text}}` contains "HOURS" or "INFO". Note: filtering requires a [Zapier paid plan](https://zapier.com/pricing) (Professional or higher) 3. Add **Telnyx → Send SMS** as the action 4. Configure the action: - **Source Number:** Your Telnyx number - **Destination Number:** Use `{{From Phone Number}}` from the trigger (replies to sender) - **Message Content:** Your auto-reply message 5. Test and turn on the Zap Be careful with auto-responders — they can create infinite loops if two Telnyx numbers with auto-responders message each other. Use filters to prevent this. --- ## Limitations Zapier checks for new triggers on a schedule based on your plan: | Zapier Plan | Polling Interval | |-------------|-----------------| | Free | Every 15 minutes | | Starter | Every 15 minutes | | Professional | Every 2 minutes | | Team / Company | Every 1 minute | This means inbound messages may not trigger actions immediately. For real-time processing, use the [Telnyx Messaging API](/docs/messaging/messages/send-message/index) with [webhooks](/docs/messaging/messages/receiving-webhooks/index) directly. The Telnyx Zapier integration supports **SMS only**. MMS media attachments are not included in the trigger data, and the Send SMS action does not support `media_urls`. For MMS, use the [Messaging API](/docs/messaging/messages/send-message/index) directly. Each Telnyx **Send SMS** action step uses a single sender number selected from your account. To send from different numbers in the same Zap, add multiple Telnyx action steps — each with a different sender number. The Zapier integration does not provide delivery status (sent, delivered, failed). For delivery tracking, use the API with [webhooks](/docs/messaging/messages/receiving-webhooks/index) or [Message Detail Records](/docs/messaging/messages/message-detail-records/index). Standard Telnyx [rate limits](/docs/messaging/messages/rate-limiting/index) apply to messages sent via Zapier. If your Zap sends messages faster than your sender type allows, messages will be queued or rejected. --- ## Troubleshooting Ensure phone numbers include the country code with `+` prefix: - ✅ `+15551234567` - ❌ `5551234567` - ❌ `(555) 123-4567` 1. Verify your Telnyx number is assigned to the correct messaging profile 2. Check that the messaging profile has a webhook URL configured 3. Ensure the Zap is turned **on** (not paused) 4. Check Zapier's [task history](https://zapier.com/app/history) for errors 5. On free plans, polling may take up to 15 minutes 1. Verify your API key is a **v2 key** from the [Telnyx Portal](https://portal.telnyx.com/#/app/auth/v2) 2. Ensure the key has not been revoked or expired 3. Try removing the Telnyx connection in Zapier and re-adding it Check the error in Zapier's task history. Common causes: - **40333:** Spend limit reached — increase your [daily spend limit](/docs/messaging/messages/configurable-spend-limits/index) - **40318:** Queue full — you're sending too fast for your sender type - **40300:** Invalid `from` number — ensure the source number is assigned to a messaging profile in the Telnyx Portal If two numbers with auto-responders message each other, they create an infinite loop. Fix by: 1. Adding a Zapier **Filter** step that checks if the sender is your own number 2. Using a **delay** step to rate-limit responses 3. Checking for duplicate messages within a time window --- ## Related resources Use the Telnyx API directly for full messaging control. Real-time message delivery via webhooks (no polling delay). Browse all Telnyx Zapier integrations and templates. Configure the messaging profile used by your Zapier integration. --- ### Spend Limits > Source: https://developers.telnyx.com/docs/messaging/messages/configurable-spend-limits.md Messaging profiles can be configured with a daily spending limit to prevent unexpected costs from bugs, traffic spikes, or human error. When the limit is reached, outbound messages are rejected until the limit resets at midnight UTC. ## Set up spend limits Enable the `daily_spend_limit_enabled` flag and set a `daily_spend_limit` value (in USD) on your messaging profile: ```bash curl curl -X PATCH https://api.telnyx.com/v2/messaging_profiles/{profile_id} \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "daily_spend_limit_enabled": true, "daily_spend_limit": "10.00" }' ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) response = client.messaging_profiles.update( "your_messaging_profile_id", daily_spend_limit_enabled=True, daily_spend_limit="10.00", ) print(f"Spend limit set: ${response.data.daily_spend_limit}") ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); const response = await client.messagingProfiles.update( 'your_messaging_profile_id', { daily_spend_limit_enabled: true, daily_spend_limit: '10.00', } ); console.log(`Spend limit set: $${response.data.daily_spend_limit}`); ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messaging_profiles.update( "your_messaging_profile_id", daily_spend_limit_enabled: true, daily_spend_limit: "10.00" ) puts "Spend limit set: $#{response.data.daily_spend_limit}" ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.MessagingProfiles.Update( context.TODO(), "your_messaging_profile_id", telnyx.MessagingProfileUpdateParams{ DailySpendLimitEnabled: telnyx.Bool(true), DailySpendLimit: telnyx.String("10.00"), }, ) if err != nil { panic(err.Error()) } fmt.Printf("Spend limit set: $%s\n", response.Data.DailySpendLimit) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messagingprofiles.MessagingProfileUpdateParams; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); var params = MessagingProfileUpdateParams.builder() .dailySpendLimitEnabled(true) .dailySpendLimit("10.00") .build(); var response = client.messagingProfiles() .update("your_messaging_profile_id", params); System.out.println("Spend limit set: $" + response.data().dailySpendLimit()); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessagingProfileService(); var response = await service.UpdateAsync( "your_messaging_profile_id", new MessagingProfileUpdateOptions { DailySpendLimitEnabled = true, DailySpendLimit = "10.00" } ); Console.WriteLine($"Spend limit set: ${response.Data.DailySpendLimit}"); ``` ```php PHP true, 'daily_spend_limit' => '10.00' ] ); echo "Spend limit set: \${$response->daily_spend_limit}\n"; ``` The `daily_spend_limit` value is a string representing USD (e.g., `"0.50"`, `"10.00"`, `"100.00"`). It applies per messaging profile — use separate profiles for different budgets. --- ## When the limit is reached Once spending exceeds the configured limit, Telnyx: 1. **Rejects new messages** with HTTP `429` and error code `40333` 2. **Sends a webhook** to your configured URL 3. **Sends an email** notification to your account ### Error response ```json { "errors": [ { "code": "40333", "title": "Messaging profile spend limit reached", "detail": "The daily spend limit configured on this messaging profile has been reached", "meta": { "url": "https://developers.telnyx.com/docs/overview/errors/40333" } } ] } ``` ### Webhook payload ```json { "data": { "event_type": "messaging-profile.spend-limit-reached", "id": "d21a2887-8007-4bb6-bd7d-f2874829918e", "occurred_at": "2024-08-20T19:17:08.918+00:00", "payload": { "configured_limit": "10.00", "current_cost": "10.02", "profile_id": "be3eb60a-a346-470a-886c-ab4e421711bd" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://example.com/webhook-url" } } ``` There may be a short delay between reaching the limit and enforcement. A small number of additional messages may be sent during this window, causing `current_cost` to slightly exceed `configured_limit`. --- ## Handle the spend limit webhook Process the webhook to alert your team and gracefully handle the blocked state: ```python Python from flask import Flask, request, jsonify import logging app = Flask(__name__) logger = logging.getLogger(__name__) @app.route("/webhooks/messaging", methods=["POST"]) def handle_webhook(): data = request.json["data"] if data["event_type"] == "messaging-profile.spend-limit-reached": payload = data["payload"] logger.warning( "Spend limit reached! Profile: %s, Limit: $%s, Current: $%s", payload["profile_id"], payload["configured_limit"], payload["current_cost"], ) # Alert your team (Slack, PagerDuty, email, etc.) alert_team( title="SMS Spend Limit Reached", message=f"Profile {payload['profile_id']} hit ${payload['configured_limit']} limit. " f"Current spend: ${payload['current_cost']}. " f"Messages are being rejected.", ) # Optionally: queue messages for retry after midnight UTC # or switch to a backup profile return jsonify({"status": "ok"}), 200 ``` ```javascript Node import express from 'express'; const app = express(); app.use(express.json()); app.post('/webhooks/messaging', (req, res) => { const { data } = req.body; if (data.event_type === 'messaging-profile.spend-limit-reached') { const { profile_id, configured_limit, current_cost } = data.payload; console.warn( `Spend limit reached! Profile: ${profile_id}, ` + `Limit: $${configured_limit}, Current: $${current_cost}` ); // Alert your team alertTeam({ title: 'SMS Spend Limit Reached', message: `Profile ${profile_id} hit $${configured_limit} limit. ` + `Current spend: $${current_cost}. Messages are being rejected.`, }); } res.json({ status: 'ok' }); }); ``` --- ## Track spending Monitor your messaging spend to take action before hitting the limit: ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) def check_spend(profile_id: str) -> dict: """Check current spend against the configured limit.""" profile = client.messaging_profiles.retrieve(profile_id) limit = float(profile.data.daily_spend_limit or 0) enabled = profile.data.daily_spend_limit_enabled return { "profile_id": profile_id, "limit_enabled": enabled, "daily_limit": limit, } def send_with_spend_check(profile_id: str, to: str, from_: str, text: str): """Send a message with pre-send spend checking.""" try: response = client.messages.send(from_=from_, to=to, text=text) return {"status": "sent", "message_id": response.data.id} except Exception as e: error_code = getattr(e, "code", None) if error_code == "40333": return {"status": "blocked", "reason": "spend_limit_reached"} raise # Check profile status info = check_spend("your_messaging_profile_id") print(f"Limit enabled: {info['limit_enabled']}, Limit: ${info['daily_limit']}") ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); async function checkSpend(profileId) { const profile = await client.messagingProfiles.retrieve(profileId); return { profileId, limitEnabled: profile.data.daily_spend_limit_enabled, dailyLimit: parseFloat(profile.data.daily_spend_limit || '0'), }; } async function sendWithSpendCheck(profileId, to, from, text) { try { const response = await client.messages.send({ from, to, text }); return { status: 'sent', messageId: response.data.id }; } catch (err) { if (err.rawType === '40333') { return { status: 'blocked', reason: 'spend_limit_reached' }; } throw err; } } ``` --- ## Reset and override The running spend total resets automatically at **midnight UTC** each day. After reset, messages can be sent until the limit is exceeded again. Changing the `daily_spend_limit` or `daily_spend_limit_enabled` values does **not** reset the running total. Only the midnight UTC reset clears accumulated spend. If you've hit the limit but need to send urgent messages, temporarily disable the limit: ```python # 1. Disable the limit client.messaging_profiles.update( profile_id, daily_spend_limit_enabled=False ) # 2. Send urgent messages client.messages.send(from_=from_number, to=to_number, text="Urgent message") # 3. Re-enable the limit client.messaging_profiles.update( profile_id, daily_spend_limit_enabled=True ) ``` Re-enabling does **not** reset the counter. If you were at $10.00 of a $10.00 limit, re-enabling will immediately block again. Either increase the limit or wait for the midnight UTC reset. Raise the `daily_spend_limit` to allow more messages until the new limit is reached: ```python # Increase from $10 to $25 client.messaging_profiles.update( profile_id, daily_spend_limit="25.00" ) ``` This takes effect immediately — if current spend is $10.02 and the new limit is $25.00, messages will flow again until $25.00 is reached. --- ## Best practices Even if you don't expect high spend, a limit prevents runaway costs from application bugs or compromised API keys. Create separate messaging profiles for transactional messages (OTP, alerts) and marketing messages, each with appropriate limits. Always handle the `messaging-profile.spend-limit-reached` webhook to alert your team immediately. Your application should gracefully handle the `40333` error code — queue messages for later delivery or switch to a backup profile. As your messaging volume grows, review and adjust limits to avoid unexpected blocks during peak periods. --- ## Related resources API reference for configuring messaging profile settings. Track message costs and delivery status. Understand message throughput limits. Configure webhooks to receive spend limit notifications. --- ### Message Detail Records > Source: https://developers.telnyx.com/docs/messaging/messages/message-detail-records.md A Message Detail Record (MDR) describes a specific message request—including its current status, cost, and metadata. Telnyx creates an MDR when a message is submitted and updates it as the message progresses through delivery. ## When to Use MDRs Check if a message was delivered, failed, or is still in progress. Investigate delivery issues by examining message status and error codes. Confirm message costs after delivery for billing reconciliation. Retrieve message history for compliance and record-keeping. --- ## Retrieve an MDR Fetch a message record using its UUID. The UUID is returned when you [send a message](/api-reference/messages/send-a-message) and is also included in [webhook events](/docs/messaging/messages/receiving-webhooks). ```bash curl curl -X GET "https://api.telnyx.com/v2/messages/834f3d53-8a3c-4aa0-a733-7f2d682a72df" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); const response = await client.messages.retrieve( '834f3d53-8a3c-4aa0-a733-7f2d682a72df' ); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) response = client.messages.retrieve( "834f3d53-8a3c-4aa0-a733-7f2d682a72df" ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.retrieve( "834f3d53-8a3c-4aa0-a733-7f2d682a72df" ) puts response ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Get( context.TODO(), "834f3d53-8a3c-4aa0-a733-7f2d682a72df", ) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.*; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessageGetResponse response = client.messages() .get("834f3d53-8a3c-4aa0-a733-7f2d682a72df"); System.out.println(response); } } ``` ```csharp .NET using System; using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var message = await service.GetAsync("834f3d53-8a3c-4aa0-a733-7f2d682a72df"); Console.WriteLine(message); ``` ```php PHP queued: Message accepted queued --> sent: Gateway accepts queued --> failed: Gateway rejects sent --> delivered: Carrier confirms sent --> failed: Carrier rejects sent --> dlr_timeout: No carrier response queued --> gw_timeout: No gateway response delivered --> [*] failed --> [*] dlr_timeout --> [*] gw_timeout --> [*] ``` ### Outbound Statuses | Status | Description | Final? | |--------|-------------|--------| | `queued` | Message accepted and queued for sending | No | | `sent` | Delivered to carrier gateway | No | | `delivered` | Carrier confirmed delivery to handset | ✓ Yes | | `failed` | Delivery failed (see `errors` array) | ✓ Yes | | `gw_timeout` | No response from gateway | ✓ Yes | | `dlr_timeout` | No delivery receipt from carrier | ✓ Yes | **Track delivery with webhooks**: Rather than polling for status, configure a [webhook URL](/docs/messaging/messages/receiving-webhooks) to receive real-time status updates as `message.sent`, `message.delivered`, or `message.finalized` events. ### Inbound Statuses | Status | Description | |--------|-------------| | `received` | Message received by Telnyx | | `delivered` | Message delivered to your webhook | --- ## Common Error Codes When a message fails, the `errors` array contains details: ```json { "errors": [ { "code": "40301", "title": "Destination number blocked", "detail": "The recipient has opted out of messages from this sender" } ] } ``` | Error Code | Description | Resolution | |------------|-------------|------------| | `40300` | Invalid destination | Verify the phone number format | | `40301` | Destination blocked | Recipient has opted out—remove from list | | `40310` | Carrier rejected | Message content may have triggered spam filters | | `40311` | Undeliverable | Number is unreachable (landline, disconnected) | | `40400` | Sender not registered | Register for 10DLC or toll-free verification | | `40500` | Rate limit exceeded | Slow down sending or request higher limits | See the [Error Codes Reference](/docs/messaging/messages/error-codes) for the complete list. --- ## Best Practices Polling the MDR endpoint is inefficient and can hit rate limits. Instead, configure webhooks on your [Messaging Profile](/docs/messaging/messages/send-message) to receive real-time updates: - `message.sent` — Message accepted by carrier - `message.delivered` — Confirmed delivery - `message.finalized` — Final status with cost ```json { "webhook_url": "https://your-app.com/webhooks/messaging", "webhook_failover_url": "https://your-app.com/webhooks/messaging-backup" } ``` Save the `id` returned when you send a message. This UUID is required to retrieve the MDR later: ```javascript const response = await client.messages.send({ from: '+15551234567', to: '+15559876543', text: 'Hello!' }); // Store this for later tracking const messageId = response.data.id; ``` The `cost` field is populated asynchronously. For accurate billing: 1. Wait for the `message.finalized` webhook, OR 2. Retrieve the MDR after a few seconds ```javascript // Cost may not be available immediately if (message.cost === null) { // Wait for message.finalized webhook or retry later } ``` --- ## Troubleshooting **Possible causes:** - Invalid message ID format - Message ID from a different account - Message was never created (request was rejected at validation) **Solution**: Verify the UUID format and check that the message send request returned a `201` status. Rejected requests don't create MDRs. **Possible causes:** - Message is rate-limited and waiting in queue - Gateway connection issue **Solution**: Wait a few minutes. If still queued after 5 minutes, check [system status](https://status.telnyx.com) for outages. Messages stuck beyond `valid_until` will fail. **Cause**: Cost is calculated asynchronously after the message is sent. **Solution**: Either wait for the `message.finalized` webhook, or retrieve the MDR again after 5-10 seconds. --- ## Next Steps Get real-time delivery updates Complete sending quickstart Full error code reference Complete Messages API docs --- ## Messaging Profiles ### Overview > Source: https://developers.telnyx.com/docs/messaging/messages/messaging-profiles-overview.md A **messaging profile** is the central configuration object for your Telnyx messaging setup. It groups your phone numbers, defines webhook URLs, and controls features like number pooling, smart encoding, and spend limits. Every phone number you use for messaging must be assigned to a messaging profile. ## What a messaging profile controls | Setting | Description | Default | |---------|-------------|---------| | **Webhook URL** | Where inbound messages and delivery status events are sent | None (required) | | **Number Pool** | Distribute messages across multiple numbers automatically | Disabled | | **Sticky Sender** | Keep the same sender number for each recipient | Disabled | | **Geomatch** | Select sender numbers based on geographic proximity | Disabled | | **Smart Encoding** | Replace Unicode characters with GSM-7 equivalents | Disabled | | **MMS Transcoding** | Automatically resize media for carrier limits | Disabled | | **Spend Limit** | Daily spend cap to prevent unexpected costs | Disabled | | **URL Shortening** | Shorten URLs in outbound messages | Disabled | --- ## Create a messaging profile ```bash curl curl -X POST https://api.telnyx.com/v2/messaging_profiles \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "name": "My Messaging Profile", "webhook_url": "https://example.com/webhooks/messaging", "webhook_failover_url": "https://example.com/webhooks/messaging/failover" }' ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) profile = client.messaging_profiles.create( name="My Messaging Profile", webhook_url="https://example.com/webhooks/messaging", webhook_failover_url="https://example.com/webhooks/messaging/failover", ) print(f"Profile created: {profile.data.id}") ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); const profile = await client.messagingProfiles.create({ name: 'My Messaging Profile', webhook_url: 'https://example.com/webhooks/messaging', webhook_failover_url: 'https://example.com/webhooks/messaging/failover', }); console.log(`Profile created: ${profile.data.id}`); ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) profile = client.messaging_profiles.create( name: "My Messaging Profile", webhook_url: "https://example.com/webhooks/messaging", webhook_failover_url: "https://example.com/webhooks/messaging/failover" ) puts "Profile created: #{profile.data.id}" ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) profile, err := client.MessagingProfiles.Create( context.TODO(), telnyx.MessagingProfileCreateParams{ Name: "My Messaging Profile", WebhookURL: "https://example.com/webhooks/messaging", WebhookFailoverURL: telnyx.String("https://example.com/webhooks/messaging/failover"), }, ) if err != nil { panic(err.Error()) } fmt.Printf("Profile created: %s\n", profile.Data.ID) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messagingprofiles.MessagingProfileCreateParams; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); var params = MessagingProfileCreateParams.builder() .name("My Messaging Profile") .webhookUrl("https://example.com/webhooks/messaging") .webhookFailoverUrl("https://example.com/webhooks/messaging/failover") .build(); var profile = client.messagingProfiles().create(params); System.out.println("Profile created: " + profile.data().id()); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessagingProfileService(); var profile = await service.CreateAsync(new MessagingProfileCreateOptions { Name = "My Messaging Profile", WebhookUrl = "https://example.com/webhooks/messaging", WebhookFailoverUrl = "https://example.com/webhooks/messaging/failover" }); Console.WriteLine($"Profile created: {profile.Data.Id}"); ``` ```php PHP 'My Messaging Profile', 'webhook_url' => 'https://example.com/webhooks/messaging', 'webhook_failover_url' => 'https://example.com/webhooks/messaging/failover' ]); echo "Profile created: {$profile->id}\n"; ``` You can also create messaging profiles in the [Telnyx Portal](https://portal.telnyx.com/#/app/messaging) under **Messaging > Messaging Profiles**. --- ## Configure profile features Update an existing profile to enable features: ```bash curl curl -X PATCH https://api.telnyx.com/v2/messaging_profiles/{profile_id} \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "number_pool_settings": { "geomatch": true, "sticky_sender": true, "skip_unhealthy": true }, "smart_encoding": true, "mms_transcoding": true, "daily_spend_limit_enabled": true, "daily_spend_limit": "50.00" }' ``` ```python Python response = client.messaging_profiles.update( "your_messaging_profile_id", number_pool_settings={ "geomatch": True, "sticky_sender": True, "skip_unhealthy": True, }, smart_encoding=True, mms_transcoding=True, daily_spend_limit_enabled=True, daily_spend_limit="50.00", ) print(f"Profile updated: {response.data.id}") ``` ```javascript Node const response = await client.messagingProfiles.update( 'your_messaging_profile_id', { number_pool_settings: { geomatch: true, sticky_sender: true, skip_unhealthy: true, }, smart_encoding: true, mms_transcoding: true, daily_spend_limit_enabled: true, daily_spend_limit: '50.00', } ); console.log(`Profile updated: ${response.data.id}`); ``` --- ## Profile features explained Every messaging profile needs a **webhook URL** to receive: - **Inbound messages** — SMS/MMS received on your numbers - **Delivery status updates** — sent, delivered, failed, etc. - **Spend limit notifications** — when daily limits are reached Configure a **failover URL** as a backup in case your primary webhook is unreachable. | Setting | Description | |---------|-------------| | `webhook_url` | Primary URL for all messaging events | | `webhook_failover_url` | Backup URL if primary fails | | `webhook_api_version` | API version for webhook payloads (`1` or `2`) | See [Webhooks](/docs/messaging/messages/receiving-webhooks/index) for implementation details. When enabled, messages sent from the profile automatically distribute across all assigned numbers. This increases throughput and helps avoid carrier filtering. **Settings:** | Setting | Description | |---------|-------------| | `geomatch` | Select sender number closest to recipient's area code | | `sticky_sender` | Reuse the same sender number for each recipient | | `skip_unhealthy` | Skip numbers with delivery issues | | `long_code_weight` | Weight for long code selection (default: 1) | | `toll_free_weight` | Weight for toll-free selection (default: 1) | See [Number Pool](/docs/messaging/messages/number-pool/index) for details. Automatically replaces Unicode characters (curly quotes, em dashes, etc.) with GSM-7 equivalents to keep messages in the more efficient encoding and reduce segment counts. A single curly quote can switch an entire message from GSM-7 (160 chars/segment) to UTF-16 (70 chars/segment), more than doubling costs. See [Smart Encoding](/docs/messaging/messages/smart-encoding/index) for the full character substitution reference. Automatically resizes images and videos to meet carrier size limits before delivery. When enabled: - Images are converted to JPEG - Videos are converted to H.264 MP4 - Animated GIFs are not resized See [MMS Media & Transcoding](/docs/messaging/messages/mms-transcoding/index) for carrier size limits. Set a daily spending cap to prevent unexpected costs. When the limit is reached: - New messages are rejected with error `40333` - A webhook notification is sent - An email alert is sent to your account The limit resets at midnight UTC daily. See [Spend Limits](/docs/messaging/messages/configurable-spend-limits/index) for configuration and webhook handling. Automatically shorten URLs in outbound messages. Shortened URLs use your configured domain and track click-through rates. | Setting | Description | |---------|-------------| | `url_shortener_settings.domain` | Custom domain for shortened URLs | | `url_shortener_settings.prefix` | URL prefix | | `url_shortener_settings.replace_blacklist_only` | Only replace blacklisted URLs | | `url_shortener_settings.send_webhooks` | Send click-tracking webhooks | --- ## Assign phone numbers After creating a profile, assign phone numbers to it: ```bash curl curl -X POST https://api.telnyx.com/v2/messaging_profiles/{profile_id}/phone_numbers \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{"phone_number_id": "your_phone_number_id"}' ``` ```python Python # List numbers on a profile numbers = client.messaging_profiles.list_phone_numbers( "your_messaging_profile_id" ) for number in numbers.data: print(f"{number.phone_number} ({number.type})") ``` ```javascript Node // List numbers on a profile const numbers = await client.messagingProfiles.listPhoneNumbers( 'your_messaging_profile_id' ); numbers.data.forEach(n => { console.log(`${n.phone_number} (${n.type})`); }); ``` You can also assign numbers to profiles in the [Telnyx Portal](https://portal.telnyx.com/#/app/messaging) by editing a messaging profile and selecting numbers. --- ## Common configurations ```json { "name": "Transactional Messages", "webhook_url": "https://api.example.com/webhooks/sms", "smart_encoding": true, "daily_spend_limit_enabled": true, "daily_spend_limit": "100.00" } ``` **Best for:** OTP codes, account alerts, order confirmations. Low volume, high priority. Smart encoding reduces costs. ```json { "name": "Marketing Campaigns", "webhook_url": "https://api.example.com/webhooks/marketing", "number_pool_settings": { "geomatch": true, "sticky_sender": true, "skip_unhealthy": true }, "smart_encoding": true, "mms_transcoding": true, "daily_spend_limit_enabled": true, "daily_spend_limit": "500.00" } ``` **Best for:** Promotional messages, newsletters. Number pool for throughput. Higher spend limit for volume. ```json { "name": "Customer Support", "webhook_url": "https://api.example.com/webhooks/support", "number_pool_settings": { "sticky_sender": true }, "smart_encoding": true } ``` **Best for:** Two-way conversations. Sticky sender ensures customers always see the same number. --- ## Related resources Distribute messages across multiple numbers for higher throughput. Reduce SMS costs by replacing Unicode with GSM-7 characters. Set daily spend caps to prevent unexpected costs. Receive inbound messages and delivery status updates. --- ### Number Pool > Source: https://developers.telnyx.com/docs/messaging/messages/number-pool.md Number Pool automatically distributes your outbound messages across multiple phone numbers, helping you scale messaging campaigns while maintaining deliverability. Instead of specifying a single "from" number, Telnyx selects the optimal sender from your pool based on availability, health, and configured weights. ## When to Use Number Pool Distribute traffic across numbers to avoid per-number rate limits and increase total throughput. Automatically skip unhealthy numbers and spread reputation across multiple senders. Balance between long codes and toll-free numbers with configurable weights. No need to track which number to use—Telnyx handles sender selection. ## How It Works 1. **Pool creation**: All long code and toll-free numbers assigned to your Messaging Profile form the pool 2. **Sender selection**: When you send a message, Telnyx picks an available number from the pool 3. **Weight distribution**: Control the ratio of long code vs. toll-free usage with weights 4. **Health monitoring**: Optionally skip numbers with poor delivery rates ```mermaid flowchart LR A[Send Message] --> B{Number Pool} B --> C[Long Code 1] B --> D[Long Code 2] B --> E[Toll-Free] C --> F[Recipient] D --> F E --> F ``` ## Prerequisites - A [Messaging Profile](/docs/messaging/messages/send-message) with at least one phone number assigned - Multiple numbers recommended for effective load distribution --- ## Configure Number Pool Enable Number Pool on your Messaging Profile by setting the `number_pool_settings`. The weights control which number types are preferred. ```bash curl curl -X PATCH "https://api.telnyx.com/v2/messaging_profiles/YOUR_PROFILE_ID" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "number_pool_settings": { "long_code_weight": 5, "toll_free_weight": 1, "skip_unhealthy": true } }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); const response = await client.messagingProfiles.update( 'YOUR_PROFILE_ID', { number_pool_settings: { long_code_weight: 5, toll_free_weight: 1, skip_unhealthy: true } } ); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) response = client.messaging_profiles.update( "YOUR_PROFILE_ID", number_pool_settings={ "long_code_weight": 5, "toll_free_weight": 1, "skip_unhealthy": True } ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messaging_profiles.update( "YOUR_PROFILE_ID", number_pool_settings: { long_code_weight: 5, toll_free_weight: 1, skip_unhealthy: true } ) puts response ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.MessagingProfiles.Update( context.TODO(), "YOUR_PROFILE_ID", telnyx.MessagingProfileUpdateParams{ NumberPoolSettings: &telnyx.NumberPoolSettingsParam{ LongCodeWeight: telnyx.Int(5), TollFreeWeight: telnyx.Int(1), SkipUnhealthy: telnyx.Bool(true), }, }, ) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messagingprofiles.*; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); NumberPoolSettings poolSettings = NumberPoolSettings.builder() .longCodeWeight(5) .tollFreeWeight(1) .skipUnhealthy(true) .build(); MessagingProfileUpdateParams params = MessagingProfileUpdateParams.builder() .numberPoolSettings(poolSettings) .build(); MessagingProfileUpdateResponse response = client.messagingProfiles() .update("YOUR_PROFILE_ID", params); System.out.println(response); } } ``` ```csharp .NET using System; using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessagingProfileService(); var options = new MessagingProfileUpdateOptions { NumberPoolSettings = new NumberPoolSettings { LongCodeWeight = 5, TollFreeWeight = 1, SkipUnhealthy = true } }; var profile = service.Update("YOUR_PROFILE_ID", options); Console.WriteLine(profile); ``` ```php PHP [ "long_code_weight" => 5, "toll_free_weight" => 1, "skip_unhealthy" => true ] ]); print_r($profile); ``` 1. Go to [Messaging](https://portal.telnyx.com/#/app/messaging) in the portal 2. Click the edit icon next to your Messaging Profile 3. Under **Outbound**, toggle on **Number Pool** 4. Configure the weights for long codes and toll-free numbers 5. Optionally enable **Skip Unhealthy Numbers** 6. Click **Save** ![Number Pool Portal Settings](/img/mms_number-pool_portal-messaging-profile-settings-number-pool.png) ### Configuration Options | Parameter | Type | Description | |-----------|------|-------------| | `long_code_weight` | integer | Weight for long code selection (0 removes from pool) | | `toll_free_weight` | integer | Weight for toll-free selection (0 removes from pool) | | `skip_unhealthy` | boolean | Skip numbers with poor delivery rates | | `sticky_sender` | boolean | Reuse same number for recipient when possible | | `geomatch` | boolean | Match sender to recipient's geographic area | Weights are ratios, not percentages. With `long_code_weight: 5` and `toll_free_weight: 1`, approximately 5 out of every 6 messages use a long code. --- ## Send Messages with Number Pool When sending with Number Pool, omit the `from` field and specify your `messaging_profile_id` instead. Telnyx automatically selects the optimal sender. ```bash curl curl -X POST "https://api.telnyx.com/v2/messages/number_pool" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "messaging_profile_id": "YOUR_PROFILE_ID", "to": "+15559876543", "text": "Hello from Number Pool!" }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); const response = await client.messages.sendWithNumberPool({ messaging_profile_id: 'YOUR_PROFILE_ID', to: '+15559876543', text: 'Hello from Number Pool!' }); console.log(`Sent from: ${response.data.from.phone_number}`); ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) response = client.messages.send_with_number_pool( messaging_profile_id="YOUR_PROFILE_ID", to="+15559876543", text="Hello from Number Pool!" ) print(f"Sent from: {response.data.from_.phone_number}") ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_with_number_pool( messaging_profile_id: "YOUR_PROFILE_ID", to: "+15559876543", text: "Hello from Number Pool!" ) puts "Sent from: #{response.from.phone_number}" ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.SendWithNumberPool( context.TODO(), telnyx.MessageSendWithNumberPoolParams{ MessagingProfileID: "YOUR_PROFILE_ID", To: "+15559876543", Text: "Hello from Number Pool!", }, ) if err != nil { panic(err.Error()) } fmt.Printf("Sent from: %s\n", response.Data.From.PhoneNumber) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.*; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); MessageSendWithNumberPoolParams params = MessageSendWithNumberPoolParams.builder() .messagingProfileId("YOUR_PROFILE_ID") .to("+15559876543") .text("Hello from Number Pool!") .build(); MessageSendResponse response = client.messages().sendWithNumberPool(params); System.out.println("Sent from: " + response.data().from().phoneNumber()); } } ``` ```csharp .NET using System; using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessagingService(); var options = new MessageSendWithNumberPoolOptions { MessagingProfileId = "YOUR_PROFILE_ID", To = "+15559876543", Text = "Hello from Number Pool!" }; var message = service.SendWithNumberPool(options); Console.WriteLine($"Sent from: {message.From.PhoneNumber}"); ``` ```php PHP "YOUR_PROFILE_ID", "to" => "+15559876543", "text" => "Hello from Number Pool!" ], null, "/v2/messages/number_pool"); echo "Sent from: " . $message->from->phone_number . "\n"; ``` The response includes the actual `from` number that was selected: ```json { "data": { "id": "b0c7e8cb-6227-4c74-9f32-c7f80c30934b", "type": "SMS", "from": { "phone_number": "+15551234567", "carrier": "Telnyx", "line_type": "long_code" }, "to": [ { "phone_number": "+15559876543", "status": "queued" } ], "text": "Hello from Number Pool!" } } ``` --- ## Disable Number Pool To disable Number Pool, set `number_pool_settings` to an empty object: ```bash curl curl -X PATCH "https://api.telnyx.com/v2/messaging_profiles/YOUR_PROFILE_ID" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{"number_pool_settings": {}}' ``` ```javascript Node await client.messagingProfiles.update('YOUR_PROFILE_ID', { number_pool_settings: {} }); ``` ```python Python client.messaging_profiles.update( "YOUR_PROFILE_ID", number_pool_settings={} ) ``` --- ## Related Features Number Pool works alongside these Messaging Profile features: Maintains consistency by using the same number for a recipient across messages. When enabled, if you've previously messaged a recipient, the same number is reused when available. Enable with: ```json { "number_pool_settings": { "long_code_weight": 1, "sticky_sender": true } } ``` See [Sticky Sender](/docs/messaging/messages/sticky-sender) for details. Selects a sender number matching the recipient's geographic area, improving deliverability and user trust by showing a local number. Enable with: ```json { "number_pool_settings": { "long_code_weight": 1, "geomatch": true } } ``` See [Geomatch](/docs/messaging/messages/geomatch) for details. Monitors delivery success rates and automatically excludes numbers performing poorly. This helps maintain overall campaign deliverability. If all numbers in the pool are unhealthy, message sending will fail rather than use an unhealthy number. --- ## Troubleshooting **Cause**: All numbers are flagged as unhealthy and `skip_unhealthy` is enabled. **Solutions**: 1. Temporarily disable `skip_unhealthy` to allow sending 2. Add more numbers to your Messaging Profile 3. Investigate delivery issues on your existing numbers **Cause**: Weight of one type set to 0, or only one number type assigned. **Solutions**: 1. Verify weights are non-zero for both types 2. Ensure you have both long codes and toll-free numbers assigned to the profile **Cause**: Using the standard `/v2/messages` endpoint instead of `/v2/messages/number_pool`. **Solution**: Use the [Number Pool send endpoint](/api-reference/messages/send-a-message-using-number-pool) which requires `messaging_profile_id` instead of `from`. --- ## Next Steps Maintain sender consistency for recipients Match sender to recipient geography Understand messaging throughput limits View full Number Pool API details --- ### Sticky Sender > Source: https://developers.telnyx.com/docs/messaging/messages/sticky-sender.md Sticky Sender ensures the same phone number is used every time your application messages a particular recipient. This consistency builds familiarity—your customers see the same number each time, making your messages more recognizable and trustworthy. Sticky Sender is part of **Number Pool** settings. You must have [Number Pool](/docs/messaging/messages/number-pool) enabled to use Sticky Sender. ## When to Use Sticky Sender Maintain consistent sender identity throughout multi-message conversations. Appointment reminders, delivery updates, and alerts from a familiar number. Customers can save your number knowing future messages will come from the same sender. Build trust by ensuring customers recognize your number over time. ## How It Works 1. **First message**: Telnyx selects a number from your pool (using weights, geomatch, or availability) 2. **Mapping created**: The recipient-to-sender pairing is stored 3. **Future messages**: The same sender is automatically used for that recipient 4. **Mapping expires**: After 8 days of no messages, the mapping resets ```mermaid sequenceDiagram participant App as Your App participant TLX as Telnyx participant User as +1(555) 123-4567 Note over App,TLX: First message to user App->>TLX: Send to +15551234567 Note over TLX: Pool selects +14155550000 TLX->>User: From: +14155550000 Note over TLX: Mapping stored: User → 4155550000 Note over App,TLX: Second message (3 days later) App->>TLX: Send to +15551234567 Note over TLX: Mapping found! TLX->>User: From: +14155550000 ✓ Same number Note over App,TLX: Third message (10 days later) App->>TLX: Send to +15551234567 Note over TLX: Mapping expired (>8 days) Note over TLX: Pool selects new number TLX->>User: From: +14155551111 Note over TLX: New mapping stored ``` ### Mapping Behavior | Scenario | Behavior | |----------|----------| | Message sent within 8 days | Same sender reused, timer resets | | No messages for 8+ days | Mapping expires, new sender assigned | | Sticky Sender disabled | All mappings cleared immediately | | Number removed from profile | Mappings to that number cleared | | Sticky number unavailable | New number selected, new mapping created | **Compare with Twilio**: Sticky Sender works similarly to Twilio's Messaging Services sticky sender feature. If you're migrating, the concept is the same—just configure it on your Messaging Profile instead. ## Prerequisites - A [Messaging Profile](/docs/messaging/messages/send-message) with [Number Pool](/docs/messaging/messages/number-pool) enabled - At least one phone number assigned to the profile --- ## Configure Sticky Sender Enable Sticky Sender by updating your Messaging Profile's `number_pool_settings`. The `PATCH` endpoint merges with your existing configuration—only the fields you include are updated. Your current weights and other Number Pool settings are preserved. ```bash curl curl -X PATCH "https://api.telnyx.com/v2/messaging_profiles/YOUR_PROFILE_ID" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "number_pool_settings": { "sticky_sender": true } }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); const response = await client.messagingProfiles.update( 'YOUR_PROFILE_ID', { number_pool_settings: { sticky_sender: true } } ); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) response = client.messaging_profiles.update( "YOUR_PROFILE_ID", number_pool_settings={ "sticky_sender": True } ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messaging_profiles.update( "YOUR_PROFILE_ID", number_pool_settings: { sticky_sender: true } ) puts response ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.MessagingProfiles.Update( context.TODO(), "YOUR_PROFILE_ID", telnyx.MessagingProfileUpdateParams{ NumberPoolSettings: &telnyx.NumberPoolSettingsParam{ StickySender: telnyx.Bool(true), }, }, ) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messagingprofiles.*; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); NumberPoolSettings poolSettings = NumberPoolSettings.builder() .stickySender(true) .build(); MessagingProfileUpdateParams params = MessagingProfileUpdateParams.builder() .numberPoolSettings(poolSettings) .build(); MessagingProfileUpdateResponse response = client.messagingProfiles() .update("YOUR_PROFILE_ID", params); System.out.println(response); } } ``` ```csharp .NET using System; using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessagingProfileService(); var options = new MessagingProfileUpdateOptions { NumberPoolSettings = new NumberPoolSettings { StickySender = true } }; var profile = service.Update("YOUR_PROFILE_ID", options); Console.WriteLine(profile); ``` ```php PHP [ "sticky_sender" => true ] ]); print_r($profile); ``` The response confirms your settings: ```json { "data": { "id": "YOUR_PROFILE_ID", "record_type": "messaging_profile", "name": "My Profile", "number_pool_settings": { "sticky_sender": true, "geomatch": false, "long_code_weight": 1, "toll_free_weight": 0, "skip_unhealthy": false } } } ``` 1. Go to [Messaging](https://portal.telnyx.com/#/app/messaging) in the portal 2. Click the edit icon next to your Messaging Profile 3. Under **Outbound**, toggle on **Number Pool** 4. Check the **Sticky Sender** checkbox 5. Click **Save** ![Sticky Sender Portal Settings](/img/mms_sticky-sender_portal-messaging-profile-settings-sticky-sender.png) --- ## Disable Sticky Sender To disable Sticky Sender, set the `sticky_sender` field to `false`. This immediately clears all existing mappings. ```bash curl curl -X PATCH "https://api.telnyx.com/v2/messaging_profiles/YOUR_PROFILE_ID" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "number_pool_settings": { "sticky_sender": false } }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); await client.messagingProfiles.update('YOUR_PROFILE_ID', { number_pool_settings: { sticky_sender: false } }); ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) client.messaging_profiles.update( "YOUR_PROFILE_ID", number_pool_settings={ "sticky_sender": False } ) ``` Disabling Sticky Sender clears all recipient-to-sender mappings. Re-enabling it starts fresh—previous mappings are not restored. --- ## Combining with Other Features Sticky Sender works alongside other Number Pool settings. The priority order for number selection is: 1. **Sticky Sender** (if enabled and mapping exists) 2. **Geomatch** (if enabled and matching area code available) 3. **Weight distribution** (long code vs. toll-free preference) 4. **Skip unhealthy** (exclude poor-performing numbers) When both are enabled: - **First message**: Geomatch selects a number matching the recipient's area code - **Future messages**: Sticky Sender reuses that same geomatched number This combination provides both local presence and consistency. ```json { "number_pool_settings": { "sticky_sender": true, "geomatch": true } } ``` If a sticky sender mapping points to a number that becomes unhealthy: - The mapping is preserved - Messages still route through that number (skip_unhealthy doesn't override sticky mappings) To force re-selection, temporarily disable and re-enable Sticky Sender to clear mappings. --- ## Troubleshooting **Possible causes:** - Sticky Sender not enabled on the Messaging Profile - Previous mapping expired (8+ days since last message) - A phone number was removed from the profile **Solutions:** 1. Verify Sticky Sender is enabled in your profile settings 2. Send messages more frequently to prevent mapping expiration 3. Check that all expected numbers are still assigned to the profile **Cause**: Sticky Sender preserves existing mappings—adding new numbers doesn't affect recipients who already have mappings. **Solution**: If you want recipients to potentially use new numbers, temporarily disable Sticky Sender to clear mappings, then re-enable it. New messages will be distributed across all available numbers. Retrieve your Messaging Profile to see current settings: ```bash curl "https://api.telnyx.com/v2/messaging_profiles/YOUR_PROFILE_ID" \ -H "Authorization: Bearer YOUR_API_KEY" | jq '.data.number_pool_settings' ``` Response: ```json { "sticky_sender": true, "geomatch": false, "long_code_weight": 1, "toll_free_weight": 0, "skip_unhealthy": false } ``` --- ## Next Steps Learn about number pool configuration and weights Match sender to recipient geography Send your first message with Number Pool View full Messaging Profile API details --- ### Geomatch > Source: https://developers.telnyx.com/docs/messaging/messages/geomatch.md Geomatch automatically selects sender numbers that share the same area code as your recipients. When enabled, Telnyx matches your outbound messages with locally-recognized numbers—boosting trust and engagement with your customers. Geomatch is part of **Number Pool** settings. You must have [Number Pool](/docs/messaging/messages/number-pool) enabled to use Geomatch. ## When to Use Geomatch Appear local to recipients—messages from familiar area codes are more likely to be read and trusted. Local numbers typically see better response rates than out-of-area or toll-free numbers. Automatically match senders across different geographic areas without manual routing logic. Build rapport with customers who prefer communicating with local business numbers. **NANP only**: Geomatch currently supports only North American numbers (US, Canada, Caribbean). International numbers don't participate in geomatching. ## How It Works 1. **Message sent**: Your app sends a message without specifying a `from` number (using Number Pool) 2. **Area code lookup**: Telnyx identifies the recipient's area code 3. **Pool search**: Telnyx searches your number pool for a matching area code 4. **Selection**: If found, that number is used; otherwise, a number with a different area code is selected ```mermaid sequenceDiagram participant App as Your App participant TLX as Telnyx participant R1 as +1(415) 555-1234 participant R2 as +1(212) 555-6789 Note over App: Send to SF recipient App->>TLX: Send to +14155551234 Note over TLX: Pool has +14155550000 (415) TLX->>R1: From: +14155550000 ✓ Match! Note over App: Send to NYC recipient App->>TLX: Send to +12125556789 Note over TLX: No 212 number in pool Note over TLX: Fallback to available number TLX->>R2: From: +14155550000 (no match) ``` ## Prerequisites - A [Messaging Profile](/docs/messaging/messages/send-message) with [Number Pool](/docs/messaging/messages/number-pool) enabled - Phone numbers from multiple area codes assigned to the profile (for effective geomatching) --- ## Configure Geomatch Enable Geomatch by updating your Messaging Profile's `number_pool_settings`. ```bash curl curl -X PATCH "https://api.telnyx.com/v2/messaging_profiles/YOUR_PROFILE_ID" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "number_pool_settings": { "long_code_weight": 1, "geomatch": true } }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); const response = await client.messagingProfiles.update( 'YOUR_PROFILE_ID', { number_pool_settings: { long_code_weight: 1, geomatch: true } } ); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) response = client.messaging_profiles.update( "YOUR_PROFILE_ID", number_pool_settings={ "long_code_weight": 1, "geomatch": True } ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messaging_profiles.update( "YOUR_PROFILE_ID", number_pool_settings: { long_code_weight: 1, geomatch: true } ) puts response ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.MessagingProfiles.Update( context.TODO(), "YOUR_PROFILE_ID", telnyx.MessagingProfileUpdateParams{ NumberPoolSettings: &telnyx.NumberPoolSettingsParam{ LongCodeWeight: telnyx.Int(1), Geomatch: telnyx.Bool(true), }, }, ) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messagingprofiles.*; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); NumberPoolSettings poolSettings = NumberPoolSettings.builder() .longCodeWeight(1) .geomatch(true) .build(); MessagingProfileUpdateParams params = MessagingProfileUpdateParams.builder() .numberPoolSettings(poolSettings) .build(); MessagingProfileUpdateResponse response = client.messagingProfiles() .update("YOUR_PROFILE_ID", params); System.out.println(response); } } ``` ```csharp .NET using System; using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessagingProfileService(); var options = new MessagingProfileUpdateOptions { NumberPoolSettings = new NumberPoolSettings { LongCodeWeight = 1, Geomatch = true } }; var profile = service.Update("YOUR_PROFILE_ID", options); Console.WriteLine(profile); ``` ```php PHP [ "long_code_weight" => 1, "geomatch" => true ] ]); print_r($profile); ``` The examples include `long_code_weight` to ensure Number Pool is active. If you already have Number Pool configured, you can omit weight fields—PATCH requests merge with existing settings. 1. Go to [Messaging](https://portal.telnyx.com/#/app/messaging) in the portal 2. Click the edit icon next to your Messaging Profile 3. Under **Outbound**, toggle on **Number Pool** 4. Check the **Geomatch** option 5. Click **Save** ![Geomatch Portal Settings](/img/mms_geomatch_portal-messsaging-profile-settings-geomatch.png) --- ## Selection Behavior Understanding how Geomatch selects senders helps you optimize your number pool coverage. ### Selection Priority When both Geomatch and [Sticky Sender](/docs/messaging/messages/sticky-sender) are enabled: | Priority | Condition | Behavior | |----------|-----------|----------| | 1 | Sticky mapping exists | Use the mapped sender (geomatch ignored) | | 2 | No mapping + matching area code available | Use geomatched number, create sticky mapping | | 3 | No mapping + no matching area code | Use any available number, create sticky mapping | Sticky Sender takes precedence over Geomatch. Once a recipient is mapped to a sender, that sender is used regardless of area code matching. ### Coverage Planning For effective geomatching, ensure your number pool covers the area codes where your recipients are located: | Coverage Level | Description | Example | |----------------|-------------|---------| | **Full coverage** | Numbers in all target area codes | Pool: 415, 212, 312, 305 for SF, NYC, Chicago, Miami campaigns | | **Regional coverage** | Numbers per region, not every area code | Pool: 415 for CA, 212 for NY, 312 for IL | | **Minimal coverage** | Single area code | Pool: Only 415 numbers (geomatch rarely activates) | Use the [Phone Numbers API](/api-reference/numbers/list-phone-numbers) to audit your pool's area code coverage. Search for numbers in missing area codes to improve geomatch rates. --- ## Disable Geomatch To disable Geomatch while keeping Number Pool active: ```bash curl curl -X PATCH "https://api.telnyx.com/v2/messaging_profiles/YOUR_PROFILE_ID" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "number_pool_settings": { "geomatch": false } }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); await client.messagingProfiles.update('YOUR_PROFILE_ID', { number_pool_settings: { geomatch: false } }); ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) client.messaging_profiles.update( "YOUR_PROFILE_ID", number_pool_settings={ "geomatch": False } ) ``` --- ## Related Features Geomatch works best combined with other Number Pool features: Maintain the same sender for each recipient. When combined with Geomatch: - First message uses geomatch selection - Subsequent messages use the "stuck" sender (even if area code no longer matches) See [Sticky Sender](/docs/messaging/messages/sticky-sender) for details. Exclude poorly performing numbers from the pool. A geomatched number will be skipped if: - `skip_unhealthy: true` is enabled - The number's deliverability rate is below 25% OR spam ratio exceeds 75% The next best geomatch candidate is used instead. Control the ratio of long codes to toll-free numbers. Geomatch respects these weights—if you weight toll-free higher, toll-free numbers are preferred even when long codes have matching area codes. See [Number Pool](/docs/messaging/messages/number-pool) for configuration details. --- ## Troubleshooting **Possible causes**: - Geomatch not enabled on the profile - No numbers with matching area code in your pool - Sticky Sender is enabled and recipient already has a mapping to a different number - Matching number is unhealthy and `skip_unhealthy` is enabled **Solution**: Verify Geomatch is enabled, then check your pool's area code coverage. Use the portal or API to list numbers assigned to your messaging profile. **Cause**: Geomatch only supports NANP (North American Numbering Plan) numbers—US, Canada, and Caribbean. **Solution**: For international messaging, use explicit `from` numbers or rely on weight-based Number Pool selection. **Possible causes**: - Number Pool is not enabled (Geomatch requires Number Pool) - Only one number in the pool (nothing to match against) - Sending with explicit `from` number (bypasses Number Pool entirely) **Solution**: Ensure Number Pool is enabled with at least one weight > 0, verify multiple numbers are assigned to the profile, and omit `from` when sending to use the pool. --- ## Next Steps Learn about multi-number distribution and weights Maintain consistent sender numbers per recipient Complete messaging quickstart Full Messaging Profiles API details --- ## Short Code ### Short Codes > Source: https://developers.telnyx.com/docs/messaging/messages/short-code.md Short codes are 5- or 6-digit phone numbers designed for high-volume, application-to-person (A2P) messaging. They offer the highest throughput of any sender type — up to **1,000 messages per second** — and are recognized by consumers as legitimate business numbers. ## When to use short codes - **High-volume alerts:** Time-sensitive notifications to large audiences - **Two-factor authentication:** OTP delivery at scale with high deliverability - **Marketing campaigns:** Promotional messages with keyword opt-in (e.g., "Text JOIN to 12345") - **Voting and polling:** Interactive SMS campaigns - **Emergency notifications:** Critical alerts requiring maximum throughput | Need | Better option | |------|--------------| | Low volume (< 1,000 msgs/day) | [10DLC Long Code](/docs/messaging/10dlc/quickstart/index) | | Quick setup (no 8-12 week wait) | [Toll-Free](/docs/messaging/toll-free-verification/index) | | Two-way conversations | Long Code with [Sticky Sender](/docs/messaging/messages/sticky-sender/index) | | International messaging | [Alphanumeric Sender ID](/docs/messaging/messages/alphanumeric-sender-id/index) | | Budget-conscious | 10DLC (lower per-message cost) | --- ## Short code vs. other sender types | Feature | Short Code | Toll-Free | 10DLC Long Code | |---------|-----------|-----------|-----------------| | **Throughput** | Up to 1,000 MPS | 20 MPS | Varies by campaign | | **Setup time** | 8-12 weeks | 1-2 weeks | Days | | **Cost** | Higher (monthly lease + per-message) | Moderate | Lowest | | **Carrier trust** | Highest | High | Varies by vetting score | | **MMS support** | Yes | Yes | Yes | | **Two-way messaging** | Yes (keyword-based) | Yes | Yes | | **Vanity numbers** | Available | No | No | --- ## Provisioning process Navigate to [Short Code](https://portal.telnyx.com/#/messaging-short-code) in Mission Control and submit your request. **Choose your type:** | Type | Description | Timeline | |------|-------------|----------| | **Random** | Carrier-assigned number | 8-10 weeks | | **Vanity** | Memorable number you choose (e.g., 46835 = "GOULD") | 10-12 weeks | Vanity codes are subject to availability. Request early — popular combinations may already be taken. Provide details about your messaging program: - **Company information** — Legal name, address, contact - **Use case description** — What messages you'll send - **Message content samples** — Representative examples - **Volume estimates** — Expected daily/monthly message volume - **Opt-in flow** — How users subscribe (web form, keyword, etc.) - **Opt-out handling** — STOP keyword support - **Help response** — HELP keyword response content Your application is submitted to each major US carrier for review and approval: | Carrier | Review process | |---------|---------------| | AT&T | Reviews content, opt-in flow, and compliance | | T-Mobile | Reviews content and message flow | | Verizon | Reviews content and use case | | US Cellular | Reviews content | **This is the longest step.** Carrier certification typically takes **8-12 weeks**. Each carrier reviews independently — you may get approved by some carriers before others. Plan your launch timeline accordingly. Once approved by all target carriers, your short code appears under your messaging profile. Send messages using the same [Messaging API](/docs/messaging/messages/send-message/index) as any other number type. --- ## Sending messages from a short code Once provisioned, send messages the same way as any other sender type: ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "from": "12345", "to": "+15559876543", "text": "Your verification code is 847291. It expires in 5 minutes." }' ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) response = client.messages.send( from_="12345", to="+15559876543", text="Your verification code is 847291. It expires in 5 minutes.", ) print(f"Message sent: {response.data.id}") ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY }); const response = await client.messages.send({ from: '12345', to: '+15559876543', text: 'Your verification code is 847291. It expires in 5 minutes.', }); console.log(`Message sent: ${response.data.id}`); ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.send_( from: "12345", to: "+15559876543", text: "Your verification code is 847291. It expires in 5 minutes." ) puts "Message sent: #{response.id}" ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Send(context.TODO(), telnyx.MessageSendParams{ From: "12345", To: "+15559876543", Text: "Your verification code is 847291. It expires in 5 minutes.", }) if err != nil { panic(err.Error()) } fmt.Printf("Message sent: %s\n", response.Data.ID) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.MessageSendParams; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); var params = MessageSendParams.builder() .from("12345") .to("+15559876543") .text("Your verification code is 847291. It expires in 5 minutes.") .build(); var response = client.messages().send(params); System.out.println("Message sent: " + response.data().id()); } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new MessageService(); var response = await service.SendAsync(new MessageSendOptions { From = "12345", To = "+15559876543", Text = "Your verification code is 847291. It expires in 5 minutes." }); Console.WriteLine($"Message sent: {response.Data.Id}"); ``` ```php PHP '12345', 'to' => '+15559876543', 'text' => 'Your verification code is 847291. It expires in 5 minutes.', ]); echo "Message sent: {$response->id}\n"; ``` Short codes use **ASCII 7-bit** encoding by default (same character limits as GSM-7). See [Message Encoding](/docs/messaging/messages/message-encoding/index) for details. --- ## Automated responses Short codes must handle standard keywords to pass carrier certification. Telnyx manages these automatically. ### Required keywords | Keyword | Purpose | Telnyx handling | |---------|---------|----------------| | **STOP** (and UNSUBSCRIBE, END, QUIT, CANCEL) | Opt-out | Automatic — user is blocked from receiving messages | | **HELP** (and INFO) | Help/support info | Automatic — sends your configured help message | | **Campaign keyword** | Campaign opt-in | Automatic — sends your configured keyword response | ### Customizing responses After carrier certification, you can customize the HELP and campaign keyword responses: Configure a custom help message that includes: - Your business name - What messages the user is subscribed to - How to get support (phone number or email) - How to opt out **Example:** ``` Acme Corp: You're subscribed to order updates. For help, call 1-800-555-0123 or email support@acme.com. Reply STOP to unsubscribe. Msg&data rates may apply. ``` When a user texts your campaign keyword (e.g., "JOIN"), they receive a confirmation message. **Example:** ``` Welcome to Acme Corp alerts! You'll receive order updates and promotions. Reply HELP for help, STOP to cancel. Msg&data rates may apply. ~4 msgs/month. ``` After certification, you can disable automatic responses for HELP and campaign keywords to handle them yourself via webhook. Contact [support@telnyx.com](mailto:support@telnyx.com) to request this change. **STOP responses cannot be disabled.** Opt-out handling is managed at the carrier level. Once a user opts out, they are blocked from receiving messages — you cannot send a custom opt-out confirmation. --- ## Use case examples **Keyword:** N/A (API-triggered) **Flow:** User initiates login → your app sends OTP via short code → user enters code ``` Your Acme verification code is 847291. It expires in 5 minutes. If you didn't request this, ignore this message. ``` **Why short code:** Highest deliverability, fastest delivery, trusted by carriers. **Keyword:** JOIN **Flow:** User texts JOIN to 12345 → receives welcome message → gets promotional messages ``` Welcome msg: Acme Corp: Thanks for joining! Get exclusive deals and updates. Reply HELP for help, STOP to cancel. Msg&data rates may apply. ~4 msgs/month. Promo msg: Acme Flash Sale! 30% off all items today only. Shop now: https://acme.com/sale Reply STOP to opt out. ``` **Keyword:** ALERT **Flow:** User opts in → receives critical alerts (weather, school closings, etc.) ``` ALERT: Severe thunderstorm warning for Springfield County until 8 PM. Seek shelter immediately. Updates at https://acme.com/alerts ``` **Why short code:** Maximum throughput (1,000 MPS) for time-critical mass notifications. --- ## Carrier certification requirements Failing to meet these requirements will delay or prevent certification. Prepare these items before submitting your application. | Requirement | Details | |-------------|---------| | **Opt-in mechanism** | Clear, documented method for users to subscribe (web form, keyword, paper form) | | **Opt-in disclosure** | Users must know what they're subscribing to, message frequency, and that rates may apply | | **Opt-out support** | Must honor STOP and similar keywords immediately | | **Help support** | Must respond to HELP with business name, support contact, and opt-out instructions | | **Message frequency** | Must disclose expected message frequency at opt-in | | **Content compliance** | Message content must match the registered use case | | **Privacy policy** | Published privacy policy covering SMS data collection | | **Terms of service** | Published terms covering messaging program | --- ## Related resources Compare short codes, toll-free, long codes, and alphanumeric sender IDs. Short code throughput limits and queuing behavior. Customize opt-in and opt-out behavior. Understand ASCII 7-bit encoding used by short codes. --- ## Hosted Numbers ### Hosted SMS > Source: https://developers.telnyx.com/docs/messaging/messages/hosted-sms.md Hosted SMS lets you add messaging capabilities to phone numbers that stay with your current voice provider. Your existing voice service continues uninterrupted — Telnyx handles only the SMS and MMS routing. This is ideal for: - **Landline numbers** that need texting capabilities - **Business numbers** where you want to keep your voice provider but add programmable messaging - **Gradual migration** — test Telnyx messaging before a full port Hosting a number is **not** the same as porting. Your voice service stays with your current provider. Only SMS/MMS traffic routes through Telnyx. ## How it works ```mermaid flowchart LR A[Check Eligibility] --> B[Create Order] B --> C[Verify Ownership] C --> D[Upload LOA + Bill] D --> E[Telnyx Review] E --> F[Number Active] ``` | Step | What happens | Timeline | |------|-------------|----------| | **1. Eligibility check** | Verify your numbers can be hosted | Instant | | **2. Create order** | Submit a hosted SMS order | Instant | | **3. Verify ownership** | Confirm you own the numbers via SMS code | 5 minutes | | **4. Upload documents** | Submit LOA and recent provider bill | Instant | | **5. Telnyx review** | Our team reviews and activates | 1-3 business days | --- ## Step 1: Check number eligibility Not all numbers can be hosted. Check eligibility before creating an order. ```bash curl curl -X POST https://api.telnyx.com/v2/messaging_hosted_number_orders/eligibility_numbers_check \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "phone_numbers": ["+13125550001", "+13125550002"] }' ``` ```python Python import os import requests API_KEY = os.environ.get("TELNYX_API_KEY") headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } response = requests.post( "https://api.telnyx.com/v2/messaging_hosted_number_orders/eligibility_numbers_check", headers=headers, json={"phone_numbers": ["+13125550001", "+13125550002"]}, ) results = response.json()["phone_numbers"] for num in results: status = "✓" if num["eligible"] else "✗" print(f"{status} {num['phone_number']}: {num['detail']}") ``` ```javascript Node const axios = require('axios'); const headers = { Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, 'Content-Type': 'application/json', }; const response = await axios.post( 'https://api.telnyx.com/v2/messaging_hosted_number_orders/eligibility_numbers_check', { phone_numbers: ['+13125550001', '+13125550002'] }, { headers } ); response.data.phone_numbers.forEach((num) => { const status = num.eligible ? '✓' : '✗'; console.log(`${status} ${num.phone_number}: ${num.detail}`); }); ``` ```ruby Ruby require "net/http" require "json" require "uri" uri = URI("https://api.telnyx.com/v2/messaging_hosted_number_orders/eligibility_numbers_check") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" request["Content-Type"] = "application/json" request.body = { phone_numbers: ["+13125550001", "+13125550002"] }.to_json response = http.request(request) results = JSON.parse(response.body)["phone_numbers"] results.each do |num| status = num["eligible"] ? "✓" : "✗" puts "#{status} #{num['phone_number']}: #{num['detail']}" end ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) func main() { data := map[string][]string{ "phone_numbers": {"+13125550001", "+13125550002"}, } body, _ := json.Marshal(data) req, _ := http.NewRequest("POST", "https://api.telnyx.com/v2/messaging_hosted_number_orders/eligibility_numbers_check", bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) numbers := result["phone_numbers"].([]interface{}) for _, n := range numbers { num := n.(map[string]interface{}) eligible := num["eligible"].(bool) symbol := "✗" if eligible { symbol = "✓" } fmt.Printf("%s %s: %s\n", symbol, num["phone_number"], num["detail"]) } } ``` ```php PHP ['+13125550001', '+13125550002']]; $ch = curl_init('https://api.telnyx.com/v2/messaging_hosted_number_orders/eligibility_numbers_check'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$apiKey}", 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($data), ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); foreach ($response['phone_numbers'] as $num) { $status = $num['eligible'] ? '✓' : '✗'; echo "{$status} {$num['phone_number']}: {$num['detail']}\n"; } ``` ```csharp .NET using System.Net.Http.Headers; using System.Text; using System.Text.Json; var apiKey = Environment.GetEnvironmentVariable("TELNYX_API_KEY"); var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var data = new { phone_numbers = new[] { "+13125550001", "+13125550002" } }; var json = JsonSerializer.Serialize(data); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await client.PostAsync( "https://api.telnyx.com/v2/messaging_hosted_number_orders/eligibility_numbers_check", content); var result = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(result); foreach (var num in doc.RootElement.GetProperty("phone_numbers").EnumerateArray()) { var eligible = num.GetProperty("eligible").GetBoolean(); var symbol = eligible ? "✓" : "✗"; Console.WriteLine($"{symbol} {num.GetProperty("phone_number")}: {num.GetProperty("detail")}"); } ``` The Portal automatically checks eligibility when you enter numbers during order creation (Step 2). ### Eligibility statuses | Status | Description | Action | |--------|-------------|--------| | `eligible` | Number can be hosted | Proceed with order | | `number_is_not_a_us_number` | Only US numbers supported | Use a US number | | `number_can_not_be_wireless` | Wireless numbers not supported | Use a landline or VoIP number | | `number_can_not_be_in_telnyx` | Already on Telnyx platform | No hosting needed — number already works | | `number_can_not_hosted_with_a_telnyx_subscriber` | Already hosted by another Telnyx user | Contact support | | `number_can_not_be_active_in_your_account` | Active in your account already | Check your number inventory | | `number_is_not_a_valid_routing_number` | Invalid routing number | Verify the number with your provider | | `number_is_not_in_e164_format` | Wrong format | Use E.164: `+1` followed by 10 digits | | `billing_account_check_failed` | Billing issue | Check your account billing status | | `billing_account_is_abolished` | Account closed | Contact support | --- ## Step 2: Create a hosted SMS order ```bash curl curl -X POST https://api.telnyx.com/v2/messaging_hosted_number_orders \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "messaging_profile_id": "YOUR_MESSAGING_PROFILE_ID", "phone_numbers": ["+13125550001"] }' ``` ```python Python response = requests.post( "https://api.telnyx.com/v2/messaging_hosted_number_orders", headers=headers, json={ "messaging_profile_id": "YOUR_MESSAGING_PROFILE_ID", "phone_numbers": ["+13125550001"], }, ) order = response.json() print(f"Order ID: {order['id']}") print(f"Status: {order['status']}") for num in order["phone_numbers"]: print(f" {num['phone_number']}: {num['status']}") ``` ```javascript Node const response = await axios.post( 'https://api.telnyx.com/v2/messaging_hosted_number_orders', { messaging_profile_id: 'YOUR_MESSAGING_PROFILE_ID', phone_numbers: ['+13125550001'], }, { headers } ); const order = response.data; console.log(`Order ID: ${order.id}`); console.log(`Status: ${order.status}`); order.phone_numbers.forEach((n) => { console.log(` ${n.phone_number}: ${n.status}`); }); ``` ```ruby Ruby request = Net::HTTP::Post.new(URI("https://api.telnyx.com/v2/messaging_hosted_number_orders")) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" request["Content-Type"] = "application/json" request.body = { messaging_profile_id: "YOUR_MESSAGING_PROFILE_ID", phone_numbers: ["+13125550001"] }.to_json response = Net::HTTP.start("api.telnyx.com", 443, use_ssl: true) { |http| http.request(request) } order = JSON.parse(response.body) puts "Order ID: #{order['id']}" puts "Status: #{order['status']}" ``` Numbers are created in `pending` status. They stay pending until you complete verification and document upload. Go to [Numbers → Hosted SMS](https://portal.telnyx.com/#/app/numbers/hosted-sms) in Mission Control. Click [Add New Order](https://portal.telnyx.com/#/app/numbers/hosted-sms/new). Enter the phone number(s) you want to host in E.164 format (e.g., `+13125550001`). The system automatically checks eligibility. Choose the [messaging profile](/docs/messaging/messages/messaging-profiles-overview/index) for these numbers. Click **Create Order** to proceed to document upload. --- ## Step 3: Verify phone number ownership Prove you own the numbers by receiving and entering SMS verification codes. ### 3a. Request verification codes ```bash curl curl -X POST \ "https://api.telnyx.com/v2/messaging_hosted_number_orders/{order_id}/verification_codes" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "verification_method": "sms", "phone_numbers": ["+13125550001"] }' ``` ```python Python response = requests.post( f"https://api.telnyx.com/v2/messaging_hosted_number_orders/{order_id}/verification_codes", headers=headers, json={ "verification_method": "sms", "phone_numbers": ["+13125550001"], }, ) # 201 = codes sent successfully print(f"Codes sent: {response.status_code == 201}") ``` ```javascript Node const response = await axios.post( `https://api.telnyx.com/v2/messaging_hosted_number_orders/${orderId}/verification_codes`, { verification_method: 'sms', phone_numbers: ['+13125550001'], }, { headers } ); console.log(`Codes sent: ${response.status === 201}`); ``` ### 3b. Submit verification codes ```bash curl curl -X POST \ "https://api.telnyx.com/v2/messaging_hosted_number_orders/{order_id}/validation_codes" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "verification_codes": [ {"phone_number": "+13125550001", "code": "87643"} ] }' ``` ```python Python response = requests.post( f"https://api.telnyx.com/v2/messaging_hosted_number_orders/{order_id}/validation_codes", headers=headers, json={ "verification_codes": [ {"phone_number": "+13125550001", "code": "87643"}, ], }, ) result = response.json() for num in result["phone_numbers"]: print(f"{num['phone_number']}: {num['status']}") ``` ```javascript Node const response = await axios.post( `https://api.telnyx.com/v2/messaging_hosted_number_orders/${orderId}/validation_codes`, { verification_codes: [ { phone_number: '+13125550001', code: '87643' }, ], }, { headers } ); response.data.phone_numbers.forEach((n) => { console.log(`${n.phone_number}: ${n.status}`); }); ``` A successful verification returns `verified` status. The number status then changes to `ownership_successful`. --- ## Step 4: Upload authorization documents After verification, upload two PDF documents: 1. **Letter of Authorization (LOA)** — signed authorization to host the number 2. **Recent bill** — from your current voice provider showing the number ```bash curl curl -X POST \ "https://api.telnyx.com/v2/messaging_hosted_number_orders/{order_id}/actions/file_upload" \ -H "Authorization: Bearer YOUR_API_KEY" \ --form "loa=@/path/to/loa.pdf" \ --form "bill=@/path/to/bill.pdf" ``` ```python Python import requests with open("loa.pdf", "rb") as loa, open("bill.pdf", "rb") as bill: response = requests.post( f"https://api.telnyx.com/v2/messaging_hosted_number_orders/{order_id}/actions/file_upload", headers={"Authorization": f"Bearer {API_KEY}"}, files={"loa": loa, "bill": bill}, ) print(f"Upload status: {response.json()['status']}") ``` ```javascript Node const FormData = require('form-data'); const fs = require('fs'); const form = new FormData(); form.append('loa', fs.createReadStream('loa.pdf')); form.append('bill', fs.createReadStream('bill.pdf')); const response = await axios.post( `https://api.telnyx.com/v2/messaging_hosted_number_orders/${orderId}/actions/file_upload`, form, { headers: { ...form.getHeaders(), Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, }, } ); console.log(`Upload status: ${response.data.status}`); ``` After upload, the Telnyx team reviews your order and activates the number(s). This typically takes **1-3 business days**. --- ## Check order status ```bash curl curl -s "https://api.telnyx.com/v2/messaging_hosted_number_orders/{order_id}" \ -H "Authorization: Bearer YOUR_API_KEY" | jq '{status, phone_numbers: [.phone_numbers[] | {phone_number, status}]}' ``` ```python Python response = requests.get( f"https://api.telnyx.com/v2/messaging_hosted_number_orders/{order_id}", headers=headers, ) order = response.json() print(f"Order status: {order['status']}") for num in order["phone_numbers"]: print(f" {num['phone_number']}: {num['status']}") ``` ### Order statuses | Status | Meaning | |--------|---------| | `pending` | Order created, awaiting verification and documents | | `loa_file_successful` | Documents uploaded successfully | | `successful` | Order complete — numbers are active | | `failed` | Activation failed (see [failure statuses](#troubleshooting-failed-orders)) | | `deleted` | Order was cancelled | --- ## Webhook notifications Hosted SMS orders trigger webhooks to your [messaging profile's](https://portal.telnyx.com/#/programmable-messaging/profiles) configured webhook URL. Set up a handler to track order progress in real time. ```python Python from flask import Flask, request app = Flask(__name__) @app.route("/webhooks/hosted-sms", methods=["POST"]) def hosted_sms_webhook(): event = request.json["data"] event_type = event["event_type"] payload = event["payload"] order_id = payload["order_id"] order_status = payload["order_status"] print(f"Event: {event_type}") print(f"Order {order_id}: {order_status}") for num in payload.get("numbers", []): print(f" {num['value']}: {num['status']}") # Handle specific events if order_status == "successful": print("Numbers are active! Ready to send messages.") elif order_status == "failed": print("Order failed. Check number statuses for details.") return "", 200 ``` ```javascript Node const express = require('express'); const app = express(); app.use(express.json()); app.post('/webhooks/hosted-sms', (req, res) => { const event = req.body.data; const { event_type, payload } = event; console.log(`Event: ${event_type}`); console.log(`Order ${payload.order_id}: ${payload.order_status}`); for (const num of payload.numbers || []) { console.log(` ${num.value}: ${num.status}`); } if (payload.order_status === 'successful') { console.log('Numbers are active! Ready to send messages.'); } else if (payload.order_status === 'failed') { console.log('Order failed. Check number statuses for details.'); } res.sendStatus(200); }); ``` ### Webhook event types | Event | Triggered when | |-------|---------------| | `messaging_hosted_numbers_orders.created` | Order is created | | `messaging_hosted_numbers_orders.updated` | Status changes (verification, LOA upload, activation, failure) | | `messaging_hosted_numbers_orders.deleted` | Order is deleted | | `messaging_hosted_numbers_orders.internal_transfer_detected` | Order is classified as an [internal transfer](/docs/messaging/messages/hosted-sms/internal-transfer) | | `messaging_hosted_numbers_orders.internal_transfer_approval_requested` | Internal transfer approval requested from the losing account (72h window) | | `messaging_hosted_numbers_orders.internal_transfer_approved` | Losing account approved the internal transfer | | `messaging_hosted_numbers_orders.internal_transfer_rejected` | Losing account rejected the internal transfer, or the receiving account cancelled the order | | `messaging_hosted_numbers_orders.internal_transfer_auto_approved` | Internal transfer auto-approved (72h window elapsed or bypass enabled) | ### Email and Portal notifications You can also receive email notifications: Go to [Advanced Features → Notifications](https://portal.telnyx.com/#/advanced-features/notifications) and click **New Profile**. Click **New Channel**, select your profile, choose **Email**, and enter your address. Click **New Settings**, select **Messaging Hosted SMS Activity**, and link to your profile. --- ## Troubleshooting failed orders **Cause:** The losing carrier (your current voice provider) rejected the hosting request. **Common reasons:** - Number is under contract with restrictions on SMS routing changes - Provider doesn't support hosted SMS arrangements - Account information mismatch between LOA and provider records **Fix:** - Contact your voice provider to understand the rejection - Verify your LOA matches the account holder name exactly - Some carriers require you to call and authorize the SMS routing change **Cause:** The number's carrier does not support hosted SMS with Telnyx. **Fix:** - Consider porting the number fully to Telnyx instead - Contact Telnyx support to check if the carrier has been added since your last attempt **Cause:** Specific number was rejected by the losing carrier while other numbers in the order may have succeeded. **Fix:** - Check if this specific number has different account ownership - Create a separate order for this number after resolving with your provider **Cause:** The number is already hosted on Telnyx by another account. **Fix:** - If you own both accounts, remove the hosting from the other account first - If not, contact Telnyx support to resolve the conflict **Cause:** The activation process timed out waiting for carrier response. **Fix:** - Create a new order for the same number - If it fails again, contact Telnyx support — the carrier may need manual intervention **Cause:** The SMS verification code wasn't delivered to the phone number. **Fix:** - Ensure the number can receive SMS (landlines may need the current provider to enable it) - Check if the number has any SMS blocking enabled - Request the code again — you can retry multiple times - If the number truly cannot receive SMS, contact Telnyx support for alternative verification **Cause:** The Letter of Authorization didn't meet requirements. **Common issues:** - LOA not signed - Name on LOA doesn't match the voice provider account - LOA template is outdated **Fix:** - Download the latest LOA template from Telnyx support - Ensure the authorized signer matches the account holder - Upload a new LOA via the API or Portal **Known limitation:** Hosted numbers may not appear in the Portal number inventory. They are accessible via the API. ```bash # List your hosted numbers curl -s "https://api.telnyx.com/v2/messaging_hosted_number_orders" \ -H "Authorization: Bearer YOUR_API_KEY" | jq '.data[] | select(.status == "successful")' ``` --- ## Failed order and number statuses reference ### Order statuses | Status | Description | |--------|-------------| | `carrier_rejected` | Losing carrier rejected the porting request | | `failed` | Order closed — contact support | | `ineligible_carrier` | Number's carrier doesn't support hosted SMS | ### Number statuses | Status | Description | |--------|-------------| | `failed` | Number closed — contact support | | `failed_carrier_rejected` | Losing carrier rejected this number | | `failed_ineligible_carrier` | Number's carrier doesn't support hosted SMS | | `failed_number_already_hosted` | Already hosted by another Telnyx user | | `failed_number_not_found` | Number not found in routing database | | `failed_timeout` | Activation timed out | --- ## Next steps Transfer hosted numbers between Telnyx accounts. Configure webhooks, opt-out settings, and number assignment. Start sending SMS and MMS once your hosted numbers are active. Compare hosted SMS with other number types (10DLC, toll-free, short code). --- ### Internal Transfer > Source: https://developers.telnyx.com/docs/messaging/messages/hosted-sms/internal-transfer.md ## Overview Internal Hosted SMS Transfer allows you to move messaging-enabled numbers between two Telnyx accounts without going through the standard carrier porting process. This is useful when: - You manage multiple Telnyx accounts (e.g., separate voice and messaging organizations) - You need to consolidate numbers under a single account - You're migrating messaging services from one Telnyx organization to another Internal transfers are automatically detected when the number(s) in your hosted SMS order already belong to another Telnyx account. No additional configuration is needed — the system handles this automatically. ## How it works Internal transfers follow a modified version of the standard [Hosted SMS](/docs/messaging/messages/hosted-sms) flow with an additional approval step to protect the current number owner. The receiving account creates a standard hosted SMS order with the number(s) to transfer. The system automatically detects that the number belongs to another Telnyx account and flags the order as an internal transfer. The account that currently owns the number receives an email and portal notification with the transfer request details, including an approval link. The current owner has **72 hours** to approve or reject the transfer. If no action is taken, the transfer is **automatically approved** after the window expires. After approval, the receiving account must complete phone number ownership verification — the same [verification code process](/docs/messaging/messages/hosted-sms#validate-phone-numbers) used in standard hosted SMS orders. Upload the Letter of Authorization (LOA) and the most recent bill, just like a standard hosted SMS order. Once approved, verified, and documents are submitted, the Telnyx team reviews and activates the transfer. The number's `user_id` is updated to the new account, and any existing 10DLC campaign associations on the number are removed. When a number is internally transferred, any **10DLC campaign registrations** associated with that number are automatically deleted. The receiving account must re-register the number with a campaign after the transfer completes. ## Create an internal transfer order Create a hosted SMS order using the same endpoint as a standard order. The system automatically detects if the number belongs to another Telnyx account. ```bash cURL curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ -d '{ "messaging_profile_id": "16fd2706-8baf-433b-82eb-8c7fada847da", "phone_numbers": ["+13125550001"] }' \ "https://api.telnyx.com/v2/messaging_hosted_number_orders" ``` ```python Python import telnyx telnyx.api_key = "YOUR_API_KEY" order = telnyx.MessagingHostedNumberOrder.create( messaging_profile_id="16fd2706-8baf-433b-82eb-8c7fada847da", phone_numbers=["+13125550001"] ) print(order) ``` ```javascript Node.js const telnyx = require('telnyx')('YOUR_API_KEY'); const order = await telnyx.messagingHostedNumberOrders.create({ messaging_profile_id: '16fd2706-8baf-433b-82eb-8c7fada847da', phone_numbers: ['+13125550001'] }); console.log(order.data); ``` ```ruby Ruby require 'telnyx' Telnyx.api_key = 'YOUR_API_KEY' order = Telnyx::MessagingHostedNumberOrder.create( messaging_profile_id: '16fd2706-8baf-433b-82eb-8c7fada847da', phone_numbers: ['+13125550001'] ) puts order ``` ```java Java import com.telnyx.sdk.*; import com.telnyx.sdk.api.HostedNumbersApi; ApiClient client = new ApiClient(); client.setApiKey("YOUR_API_KEY"); HostedNumbersApi api = new HostedNumbersApi(client); CreateHostedNumberOrderRequest request = new CreateHostedNumberOrderRequest() .messagingProfileId("16fd2706-8baf-433b-82eb-8c7fada847da") .addPhoneNumbersItem("+13125550001"); HostedNumberOrderResponse response = api.createHostedNumberOrder(request); System.out.println(response); ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey("YOUR_API_KEY"); var service = new MessagingHostedNumberOrderService(); var options = new NewMessagingHostedNumberOrder { MessagingProfileId = "16fd2706-8baf-433b-82eb-8c7fada847da", PhoneNumbers = new List { "+13125550001" } }; var order = await service.CreateAsync(options); Console.WriteLine(order); ``` ```php PHP require_once 'vendor/autoload.php'; Telnyx\Telnyx::setApiKey('YOUR_API_KEY'); $order = Telnyx\MessagingHostedNumberOrder::create([ 'messaging_profile_id' => '16fd2706-8baf-433b-82eb-8c7fada847da', 'phone_numbers' => ['+13125550001'] ]); echo $order; ``` ```go Go package main import ( "context" "fmt" telnyx "github.com/telnyx/telnyx-go" ) func main() { client := telnyx.NewClient("YOUR_API_KEY") order, err := client.MessagingHostedNumberOrders.Create( context.Background(), &telnyx.MessagingHostedNumberOrderParams{ MessagingProfileID: "16fd2706-8baf-433b-82eb-8c7fada847da", PhoneNumbers: []string{"+13125550001"}, }, ) if err != nil { panic(err) } fmt.Println(order) } ``` ### Example response ```json { "record_type": "messaging_hosted_number_order", "id": "7d9b9fdc-d073-4c3d-9c74-bf0622b3830c", "messaging_profile_id": "16fd2706-8baf-433b-82eb-8c7fada847da", "status": "pending", "phone_numbers": [ { "record_type": "messaging_hosted_number", "id": "bda67701-2c08-47ba-8242-f6e6b235cca8", "phone_number": "+13125550001", "status": "pending" } ] } ``` The response looks identical to a standard hosted SMS order. The internal transfer detection happens server-side — the current owner of the number will receive a notification automatically. ## Approve or reject a transfer The current number owner can approve or reject the transfer using the link in their notification email, or via the API. ### Approve ```bash curl -X POST \ --header "Authorization: Bearer CURRENT_OWNER_API_KEY" \ "https://api.telnyx.com/v2/messaging_hosted_number_orders/{order_id}/approve?token=APPROVAL_TOKEN" ``` ### Reject ```bash curl -X POST \ --header "Authorization: Bearer CURRENT_OWNER_API_KEY" \ "https://api.telnyx.com/v2/messaging_hosted_number_orders/{order_id}/reject?token=APPROVAL_TOKEN" ``` The `token` parameter is included in the notification sent to the current owner. It is a one-time use token that expires after **72 hours**. If the token expires without action, the transfer is **automatically approved**. | Decision | Result | |----------|--------| | **Approved** | Transfer proceeds. Receiving account must complete 2FA verification and document upload. | | **Rejected** | Order is marked as `failed`. The receiving account is notified. | | **No action (72h)** | Transfer is **auto-approved**. Receiving account must still complete verification. | ## Complete the transfer After the transfer is approved (either explicitly or via auto-approval), the receiving account must: 1. **Verify ownership** — Send and validate verification codes for the number(s), identical to the [standard verification process](/docs/messaging/messages/hosted-sms#validate-phone-numbers). 2. **Upload documents** — Submit the LOA and bill via the [file upload endpoint](/docs/messaging/messages/hosted-sms#uploading-authorization-documents). 3. **Wait for activation** — The Telnyx team reviews and activates the transfer. ## Webhook notifications Internal transfers generate the same webhook events as standard hosted SMS orders (`.created`, `.updated`, `.deleted`), plus five lifecycle-specific events that let you track classification, approval, and auto-approval separately from generic order updates. ### Lifecycle event reference | Event | Fired when | |-------|-----------| | `messaging_hosted_numbers_orders.internal_transfer_detected` | Order is classified as an internal transfer (fires on order creation, immediately after `.created`) | | `messaging_hosted_numbers_orders.internal_transfer_approval_requested` | Approval was requested from the losing account and the 72h window has started. Includes `approval_deadline` | | `messaging_hosted_numbers_orders.internal_transfer_approved` | Losing account clicked **Approve** | | `messaging_hosted_numbers_orders.internal_transfer_rejected` | Losing account clicked **Reject**, or the receiving account cancelled/deleted the order | | `messaging_hosted_numbers_orders.internal_transfer_auto_approved` | 72h window elapsed with no response (background auto-approval), or the receiving account was on the bypass-approval allowlist | ### Detected (fires after `.created`) ```json { "data": { "event_type": "messaging_hosted_numbers_orders.internal_transfer_detected", "payload": { "order_status": "pending", "numbers": [{"status": "pending", "value": "+13125550001"}] } } } ``` ### Approval requested ```json { "data": { "event_type": "messaging_hosted_numbers_orders.internal_transfer_approval_requested", "payload": { "order_status": "pending", "numbers": [{"status": "pending", "value": "+13125550001"}], "decision": "pending", "approval_deadline": 1714521600 } } } ``` ### Approved ```json { "data": { "event_type": "messaging_hosted_numbers_orders.internal_transfer_approved", "payload": { "order_status": "pending", "decision": "approved" } } } ``` ### Rejected ```json { "data": { "event_type": "messaging_hosted_numbers_orders.internal_transfer_rejected", "payload": { "order_status": "failed", "decision": "rejected" } } } ``` ### Auto-approved ```json { "data": { "event_type": "messaging_hosted_numbers_orders.internal_transfer_auto_approved", "payload": { "order_status": "pending", "decision": "approved" } } } ``` ### Transfer activated Once approved, activation emits the standard `.updated` event when numbers become successful: ```json { "data": { "event_type": "messaging_hosted_numbers_orders.updated", "payload": { "order_status": "successful", "numbers": [{"status": "successful", "value": "+13125550001"}] } } } ``` ## Key differences from standard Hosted SMS | Aspect | Standard Hosted SMS | Internal Transfer | |--------|-------------------|------------------| | **Source** | External carrier | Another Telnyx account | | **Detection** | Manual | Automatic (system detects Telnyx-owned numbers) | | **Approval** | Not required | Required from current owner (72h window) | | **Auto-approval** | N/A | Yes, after 72 hours with no response | | **Campaign cleanup** | N/A | 10DLC campaigns automatically removed | | **Carrier porting** | Yes (NNID override) | No (direct `user_id` update) | | **2FA verification** | Required | Required (after approval) | | **LOA + Bill** | Required | Required | ## Troubleshooting If the 72-hour window passed without action, the transfer is automatically approved. Contact [Telnyx Support](https://support.telnyx.com) immediately — the transfer may be reversible if activation hasn't completed. Ensure the number can receive SMS messages. For internal transfers, the verification code is sent to the number being transferred. If the number is a landline or doesn't have SMS capabilities on the current account, contact [Telnyx Support](https://support.telnyx.com) for assistance. Check the order status for specific error details. Common causes include: - The number was deleted or deactivated on the source account before activation - The number is already hosted with another Telnyx subscriber - Billing issues on the receiving account Use the [Get Order](/api-reference/hosted-numbers/get-hosted-number-order) endpoint to check the current status. This is expected behavior. When a number transfers between accounts, existing campaign associations are automatically cleaned up. Register the number with a new campaign on the receiving account after the transfer completes. See [10DLC Campaign Registration](/docs/messaging/10dlc/quickstart) for details. --- ## Opt Outs ### Opt-In/Out Management > Source: https://developers.telnyx.com/docs/messaging/messages/advanced-opt-in-out.md Advanced Opt-In/Out lets you customize keyword triggers and auto-responses on your messaging profile. Configure country-specific responses, custom keywords, and track opt-out behavior via webhooks — all while maintaining CTIA & TCPA compliance. ## Default behavior Without custom configuration, Telnyx handles standard opt-in/out keywords automatically: These keywords create a **block rule** preventing further messages to the recipient: | Keyword | Action | |---------|--------| | `STOP` | Block messages | | `STOPALL` | Block messages | | `STOP ALL` | Block messages | | `UNSUBSCRIBE` | Block messages | | `CANCEL` | Block messages | | `END` | Block messages | | `QUIT` | Block messages | These keywords **remove an existing block rule**, allowing messages to resume: | Keyword | Action | |---------|--------| | `START` | Remove block | | `UNSTOP` | Remove block | Block rules operate at the **messaging profile level**. If a user opts out from one number on your profile, they're opted out from all numbers on that profile. When you attempt to message a blocked recipient, the API returns: ```json { "errors": [ { "code": "40300", "title": "Blocked due to STOP message", "detail": "Messages cannot be sent from '{from}' to '{to}' due to an existing block rule." } ] } ``` --- ## Create custom auto-responses Configure custom keyword responses for opt-in, opt-out, and help messages: ```bash curl # Create a custom opt-in auto-response curl -X POST \ https://api.telnyx.com/v2/messaging_profiles/{profile_id}/autoresp_configs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "op": "start", "keywords": ["JOIN", "SUBSCRIBE", "YES"], "resp_text": "Welcome to Acme alerts! You'\''ll receive up to 4 msgs/month. Reply HELP for help, STOP to cancel. Msg&data rates may apply.", "country_code": "US" }' # Create a custom HELP response curl -X POST \ https://api.telnyx.com/v2/messaging_profiles/{profile_id}/autoresp_configs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "op": "help", "keywords": ["HELP", "INFO"], "resp_text": "Acme Corp: For support call 1-800-555-0123 or email help@acme.com. Reply STOP to opt out. Msg&data rates may apply.", "country_code": "US" }' # Create a custom opt-out response curl -X POST \ https://api.telnyx.com/v2/messaging_profiles/{profile_id}/autoresp_configs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "op": "stop", "keywords": ["STOP", "QUIT", "CANCEL"], "resp_text": "You have been unsubscribed from Acme alerts. Reply START to resubscribe.", "country_code": "US" }' ``` ```python Python import os import requests API_KEY = os.environ.get("TELNYX_API_KEY") PROFILE_ID = "your_messaging_profile_id" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } # Custom opt-in response response = requests.post( f"https://api.telnyx.com/v2/messaging_profiles/{PROFILE_ID}/autoresp_configs", headers=headers, json={ "op": "start", "keywords": ["JOIN", "SUBSCRIBE", "YES"], "resp_text": ( "Welcome to Acme alerts! You'll receive up to 4 msgs/month. " "Reply HELP for help, STOP to cancel. Msg&data rates may apply." ), "country_code": "US", }, ) print(f"Opt-in config created: {response.json()['data']['id']}") # Custom HELP response response = requests.post( f"https://api.telnyx.com/v2/messaging_profiles/{PROFILE_ID}/autoresp_configs", headers=headers, json={ "op": "help", "keywords": ["HELP", "INFO"], "resp_text": ( "Acme Corp: For support call 1-800-555-0123 or email help@acme.com. " "Reply STOP to opt out. Msg&data rates may apply." ), "country_code": "US", }, ) print(f"Help config created: {response.json()['data']['id']}") ``` ```javascript Node const axios = require('axios'); const headers = { Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, 'Content-Type': 'application/json', }; const profileId = 'your_messaging_profile_id'; const baseUrl = `https://api.telnyx.com/v2/messaging_profiles/${profileId}/autoresp_configs`; // Custom opt-in response const optIn = await axios.post(baseUrl, { op: 'start', keywords: ['JOIN', 'SUBSCRIBE', 'YES'], resp_text: "Welcome to Acme alerts! You'll receive up to 4 msgs/month. Reply HELP for help, STOP to cancel.", country_code: 'US', }, { headers }); console.log(`Opt-in config: ${optIn.data.data.id}`); // Custom HELP response const help = await axios.post(baseUrl, { op: 'help', keywords: ['HELP', 'INFO'], resp_text: 'Acme Corp: For support call 1-800-555-0123. Reply STOP to opt out.', country_code: 'US', }, { headers }); console.log(`Help config: ${help.data.data.id}`); ``` ```ruby Ruby require "net/http" require "json" require "uri" api_key = ENV["TELNYX_API_KEY"] profile_id = "your_messaging_profile_id" base_url = "https://api.telnyx.com/v2/messaging_profiles/#{profile_id}/autoresp_configs" def create_config(url, api_key, data) uri = URI(url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{api_key}" request["Content-Type"] = "application/json" request.body = data.to_json response = http.request(request) JSON.parse(response.body) end # Opt-in result = create_config(base_url, api_key, { op: "start", keywords: ["JOIN", "SUBSCRIBE", "YES"], resp_text: "Welcome to Acme alerts! Reply HELP for help, STOP to cancel.", country_code: "US" }) puts "Opt-in config: #{result['data']['id']}" ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) func createAutoResponse(profileID string, data map[string]interface{}) { url := fmt.Sprintf("https://api.telnyx.com/v2/messaging_profiles/%s/autoresp_configs", profileID) body, _ := json.Marshal(data) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) data2 := result["data"].(map[string]interface{}) fmt.Printf("Config created: %s\n", data2["id"]) } func main() { profileID := "your_messaging_profile_id" createAutoResponse(profileID, map[string]interface{}{ "op": "start", "keywords": []string{"JOIN", "SUBSCRIBE", "YES"}, "resp_text": "Welcome to Acme alerts! Reply HELP for help, STOP to cancel.", "country_code": "US", }) } ``` ```php PHP true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$apiKey}", 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($data), ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); return $response; } // Opt-in $result = createAutoResponse($url, $apiKey, [ 'op' => 'start', 'keywords' => ['JOIN', 'SUBSCRIBE', 'YES'], 'resp_text' => "Welcome to Acme alerts! Reply HELP for help, STOP to cancel.", 'country_code' => 'US', ]); echo "Opt-in config: {$result['data']['id']}\n"; ``` ### Operation types | Operation (`op`) | Purpose | Default keywords | |-----------------|---------|-----------------| | `start` | Opt-in — removes block rule | START, UNSTOP | | `stop` | Opt-out — creates block rule | STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT | | `help` | Help — sends info response | HELP | | Custom | Any custom keyword response | (none) | --- ## Country-specific auto-responses Configure different responses per country using ISO 3166-1 alpha-2 codes. This enables localized language support: ```bash curl # English (US) curl -X POST \ https://api.telnyx.com/v2/messaging_profiles/{profile_id}/autoresp_configs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "op": "stop", "keywords": ["STOP"], "resp_text": "You have been unsubscribed. Reply START to resubscribe.", "country_code": "US" }' # Spanish (Mexico) curl -X POST \ https://api.telnyx.com/v2/messaging_profiles/{profile_id}/autoresp_configs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "op": "stop", "keywords": ["PARAR", "DETENER"], "resp_text": "Te has dado de baja. Responde INICIO para volver a suscribirte.", "country_code": "MX" }' # French (Canada) curl -X POST \ https://api.telnyx.com/v2/messaging_profiles/{profile_id}/autoresp_configs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "op": "stop", "keywords": ["ARRET", "ARRETER"], "resp_text": "Vous êtes désabonné. Répondez DEBUT pour vous réabonner.", "country_code": "CA" }' # UK English curl -X POST \ https://api.telnyx.com/v2/messaging_profiles/{profile_id}/autoresp_configs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "op": "start", "keywords": ["START"], "resp_text": "You are now subscribed to receive messages from Acme. Reply STOP to opt out.", "country_code": "GB" }' ``` The feature is **language agnostic** — you can use keywords and responses in any language. The `country_code` field determines which auto-response applies based on the sender's number origin. --- ## Track opt-out behavior via webhooks When a user sends an opt-in, opt-out, or help keyword, the inbound message webhook includes an `autoresponse_type` field: ```json { "data": { "event_type": "message.received", "payload": { "autoresponse_type": "STOP", "from": { "phone_number": "+15559876543" }, "text": "STOP", "to": [ { "phone_number": "+15551234567" } ] } } } ``` ### Handle opt-out webhooks ```python Python from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/webhooks/messaging", methods=["POST"]) def handle_webhook(): data = request.json["data"] if data["event_type"] != "message.received": return jsonify({"status": "ignored"}), 200 payload = data["payload"] autoresponse_type = payload.get("autoresponse_type") phone = payload["from"]["phone_number"] if autoresponse_type == "STOP": # User opted out — update your database db.execute( "UPDATE subscribers SET opted_out = TRUE, opted_out_at = NOW() " "WHERE phone = %s", (phone,), ) print(f"User {phone} opted out") elif autoresponse_type == "START": # User opted back in db.execute( "UPDATE subscribers SET opted_out = FALSE " "WHERE phone = %s", (phone,), ) print(f"User {phone} opted back in") elif autoresponse_type == "HELP": # User requested help — log for support print(f"User {phone} requested help") return jsonify({"status": "ok"}), 200 ``` ```javascript Node import express from 'express'; const app = express(); app.use(express.json()); app.post('/webhooks/messaging', async (req, res) => { const { data } = req.body; if (data.event_type !== 'message.received') { return res.json({ status: 'ignored' }); } const { autoresponse_type } = data.payload; const phone = data.payload.from.phone_number; switch (autoresponse_type) { case 'STOP': await db.query( 'UPDATE subscribers SET opted_out = TRUE WHERE phone = $1', [phone] ); console.log(`User ${phone} opted out`); break; case 'START': await db.query( 'UPDATE subscribers SET opted_out = FALSE WHERE phone = $1', [phone] ); console.log(`User ${phone} opted back in`); break; case 'HELP': console.log(`User ${phone} requested help`); break; } res.json({ status: 'ok' }); }); ``` The `autoresponse_type` field is also available in your [SMS Logs](/docs/messaging/messages/message-detail-records/index) via Detail Record Search reporting. --- ## Limitations **START**, **STOP**, and **HELP** are reserved keywords for their respective operations and **cannot be reassigned** to different operations. You can add additional keywords to each operation, but the defaults always remain active. Default operations (start, stop, help) require a **minimum 20 characters** for the auto-response message. This ensures compliance with carrier requirements. Each auto-response configuration supports a maximum of **20 trigger keywords**. Toll-free numbers have a **separate carrier-level opt-out system** that Telnyx cannot customize or remove. When a user texts STOP to a toll-free number: 1. The carrier sends its own auto-reply: > *NETWORK MSG: You replied with the word "stop" which blocks all texts sent from this number. Text back "unstop" to receive messages again.* 2. Your custom STOP response is **also sent** (if configured) 3. The carrier block is applied independently of Telnyx's block rule When a user texts START or UNSTOP: > *NETWORK MSG: You have replied "unstop" and will begin receiving messages again from this number.* You cannot prevent the carrier's NETWORK MSG responses on toll-free numbers. Design your custom responses to complement (not contradict) these messages. Block rules apply at the **messaging profile level**, not the individual number level. If a user opts out from any number on your profile, they're blocked from all numbers on that profile. To manage separate opt-out lists for different programs, use separate messaging profiles. --- ## Related resources Configure the messaging profile that manages your opt-in/out rules. Set up webhooks to receive opt-in/out events. Short code keyword handling and carrier certification. Toll-free number verification and compliance. --- ## Phone Numbers ### Number Configuration > Source: https://developers.telnyx.com/docs/messaging/messages/phone-number-configuration.md Before a phone number can send or receive messages through Telnyx, it must be **assigned to a messaging profile** and have messaging enabled. This guide covers the complete workflow — from purchasing a number to configuring it for messaging, assigning it to profiles, and troubleshooting common issues. ## Prerequisites - A [Telnyx account](https://telnyx.com/sign-up) with API access - Your [API key](https://portal.telnyx.com/#/app/api-keys) - At least one [messaging profile](/docs/messaging/messages/messaging-profiles-overview) created --- ## Overview ```mermaid flowchart LR A[Purchase Number] --> B[Create Messaging Profile] B --> C[Assign Number to Profile] C --> D[Configure Features] D --> E[Start Messaging] ``` Every phone number used for messaging needs: 1. **A messaging profile** — Controls webhook URLs, inbound settings, and features like [number pool](/docs/messaging/messages/number-pool) or [sticky sender](/docs/messaging/messages/sticky-sender) 2. **Messaging enabled** — The number must have messaging capabilities activated 3. **Regulatory compliance** — Depending on the number type, you may need [10DLC registration](/docs/messaging/10dlc/quickstart) or [toll-free verification](/docs/messaging/toll-free-verification) --- ## Step 1: List messaging-capable numbers Find numbers on your account that support messaging: ```bash curl curl -X GET "https://api.telnyx.com/v2/messaging_phone_numbers?page[size]=25" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python import os import requests API_KEY = os.environ.get("TELNYX_API_KEY") headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } response = requests.get( "https://api.telnyx.com/v2/messaging_phone_numbers", headers=headers, params={"page[size]": 25}, ) numbers = response.json() for num in numbers.get("data", []): profile = num.get("messaging_profile_id") or "unassigned" print(f"{num['phone_number']} — profile: {profile}, features: {num.get('features', {})}") ``` ```javascript Node const axios = require('axios'); const headers = { Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, 'Content-Type': 'application/json', }; const { data: numbers } = await axios.get( 'https://api.telnyx.com/v2/messaging_phone_numbers', { headers, params: { 'page[size]': 25 } } ); numbers.data.forEach(num => { const profile = num.messaging_profile_id || 'unassigned'; console.log(`${num.phone_number} — profile: ${profile}`); }); ``` ```ruby Ruby require "net/http" require "json" require "uri" uri = URI("https://api.telnyx.com/v2/messaging_phone_numbers?page[size]=25") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" response = http.request(request) numbers = JSON.parse(response.body) numbers["data"].each do |num| profile = num["messaging_profile_id"] || "unassigned" puts "#{num['phone_number']} — profile: #{profile}" end ``` ```go Go package main import ( "encoding/json" "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.telnyx.com/v2/messaging_phone_numbers?page[size]=25", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("Error:", err) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result map[string]interface{} json.Unmarshal(body, &result) for _, num := range result["data"].([]interface{}) { n := num.(map[string]interface{}) profile := n["messaging_profile_id"] if profile == nil { profile = "unassigned" } fmt.Printf("%s — profile: %v\n", n["phone_number"], profile) } } ``` ```php PHP true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$apiKey}", 'Content-Type: application/json', ], ]); $response = curl_exec($ch); curl_close($ch); $numbers = json_decode($response, true); foreach ($numbers['data'] as $num) { $profile = $num['messaging_profile_id'] ?? 'unassigned'; echo "{$num['phone_number']} — profile: {$profile}\n"; } ``` ```csharp .NET using System.Net.Http.Headers; using System.Text.Json; var apiKey = Environment.GetEnvironmentVariable("TELNYX_API_KEY"); var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var response = await client.GetAsync( "https://api.telnyx.com/v2/messaging_phone_numbers?page%5Bsize%5D=25" ); var body = await response.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(body); foreach (var num in doc.RootElement.GetProperty("data").EnumerateArray()) { var phone = num.GetProperty("phone_number").GetString(); var profile = num.TryGetProperty("messaging_profile_id", out var p) && p.ValueKind != JsonValueKind.Null ? p.GetString() : "unassigned"; Console.WriteLine($"{phone} — profile: {profile}"); } ``` ```java Java import java.net.http.*; import java.net.URI; String apiKey = System.getenv("TELNYX_API_KEY"); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.telnyx.com/v2/messaging_phone_numbers?page%5Bsize%5D=25")) .header("Authorization", "Bearer " + apiKey) .GET() .build(); HttpClient client = HttpClient.newHttpClient(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- ## Step 2: Assign a number to a messaging profile Link a phone number to a messaging profile to configure its webhook URLs and messaging behavior. ```bash curl curl -X PATCH "https://api.telnyx.com/v2/messaging_phone_numbers/+15551234567" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "messaging_profile_id": "400174af-0a13-4e28-b4f5-example12345" }' ``` ```python Python phone_number = "+15551234567" profile_id = "400174af-0a13-4e28-b4f5-example12345" response = requests.patch( f"https://api.telnyx.com/v2/messaging_phone_numbers/{phone_number}", headers=headers, json={"messaging_profile_id": profile_id}, ) result = response.json() print(f"Assigned {phone_number} to profile {result['data']['messaging_profile_id']}") ``` ```javascript Node const phoneNumber = '+15551234567'; const profileId = '400174af-0a13-4e28-b4f5-example12345'; const { data: result } = await axios.patch( `https://api.telnyx.com/v2/messaging_phone_numbers/${encodeURIComponent(phoneNumber)}`, { messaging_profile_id: profileId }, { headers } ); console.log(`Assigned ${phoneNumber} to profile ${result.data.messaging_profile_id}`); ``` ```ruby Ruby phone_number = "+15551234567" profile_id = "400174af-0a13-4e28-b4f5-example12345" uri = URI("https://api.telnyx.com/v2/messaging_phone_numbers/#{CGI.escape(phone_number)}") request = Net::HTTP::Patch.new(uri) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" request["Content-Type"] = "application/json" request.body = { messaging_profile_id: profile_id }.to_json response = http.request(request) result = JSON.parse(response.body) puts "Assigned #{phone_number} to profile #{result['data']['messaging_profile_id']}" ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "os" ) func main() { phoneNumber := "+15551234567" profileID := "400174af-0a13-4e28-b4f5-example12345" body, _ := json.Marshal(map[string]string{ "messaging_profile_id": profileID, }) encoded := url.PathEscape(phoneNumber) req, _ := http.NewRequest("PATCH", "https://api.telnyx.com/v2/messaging_phone_numbers/"+encoded, bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("Error:", err) return } defer resp.Body.Close() result, _ := io.ReadAll(resp.Body) fmt.Printf("Result: %s\n", result) } ``` ```php PHP true, CURLOPT_CUSTOMREQUEST => 'PATCH', CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv('TELNYX_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['messaging_profile_id' => $profileId]), ]); $response = curl_exec($ch); curl_close($ch); echo "Result: {$response}\n"; ``` ```csharp .NET var phoneNumber = Uri.EscapeDataString("+15551234567"); var profileId = "400174af-0a13-4e28-b4f5-example12345"; var content = new StringContent( JsonSerializer.Serialize(new { messaging_profile_id = profileId }), Encoding.UTF8, "application/json" ); var request = new HttpRequestMessage(new HttpMethod("PATCH"), $"https://api.telnyx.com/v2/messaging_phone_numbers/{phoneNumber}") { Content = content }; request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var response = await client.SendAsync(request); var result = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Result: {result}"); ``` ```java Java import java.net.http.*; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; String phoneNumber = URLEncoder.encode("+15551234567", StandardCharsets.UTF_8); String profileId = "400174af-0a13-4e28-b4f5-example12345"; String body = String.format("{\"messaging_profile_id\":\"%s\"}", profileId); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.telnyx.com/v2/messaging_phone_numbers/" + phoneNumber)) .header("Authorization", "Bearer " + System.getenv("TELNYX_API_KEY")) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("Result: " + response.body()); ``` --- ## Step 3: Retrieve number configuration Check the current messaging configuration for a specific number: ```bash curl curl -X GET "https://api.telnyx.com/v2/messaging_phone_numbers/+15551234567" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python response = requests.get( f"https://api.telnyx.com/v2/messaging_phone_numbers/{phone_number}", headers=headers, ) config = response.json()["data"] print(f"Number: {config['phone_number']}") print(f"Profile: {config['messaging_profile_id']}") print(f"Features: {config.get('features', {})}") print(f"Health: {config.get('health', {})}") ``` ```javascript Node const { data: config } = await axios.get( `https://api.telnyx.com/v2/messaging_phone_numbers/${encodeURIComponent(phoneNumber)}`, { headers } ); console.log('Number:', config.data.phone_number); console.log('Profile:', config.data.messaging_profile_id); console.log('Features:', config.data.features); ``` ### Response fields | Field | Description | |-------|-------------| | `phone_number` | The E.164 formatted phone number | | `messaging_profile_id` | ID of the assigned messaging profile | | `type` | Number type: `long_code`, `toll_free`, `short_code` | | `country_code` | Two-letter country code | | `features` | Enabled features (SMS, MMS, etc.) | | `health` | Number health indicators (message success rate, etc.) | | `eligible_messaging_products` | Products the number can be used for | --- ## Step 4: Bulk assignment Assign multiple numbers to a messaging profile at once using the messaging profile's phone number assignment endpoint: ```bash curl # Assign multiple numbers to a profile for number in "+15551234567" "+15559876543" "+15551112222"; do curl -X PATCH "https://api.telnyx.com/v2/messaging_phone_numbers/$number" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{"messaging_profile_id": "400174af-0a13-4e28-b4f5-example12345"}' done ``` ```python Python numbers_to_assign = ["+15551234567", "+15559876543", "+15551112222"] profile_id = "400174af-0a13-4e28-b4f5-example12345" for number in numbers_to_assign: response = requests.patch( f"https://api.telnyx.com/v2/messaging_phone_numbers/{number}", headers=headers, json={"messaging_profile_id": profile_id}, ) if response.status_code == 200: print(f"✓ Assigned {number}") else: print(f"✗ Failed {number}: {response.json().get('errors', [])}") ``` ```javascript Node const numbersToAssign = ['+15551234567', '+15559876543', '+15551112222']; const profileId = '400174af-0a13-4e28-b4f5-example12345'; for (const number of numbersToAssign) { try { await axios.patch( `https://api.telnyx.com/v2/messaging_phone_numbers/${encodeURIComponent(number)}`, { messaging_profile_id: profileId }, { headers } ); console.log(`✓ Assigned ${number}`); } catch (error) { console.log(`✗ Failed ${number}: ${error.response?.data?.errors}`); } } ``` --- ## Messaging enablement by number type Different number types have different requirements before they can send messages: | Number Type | Messaging Ready? | Additional Steps Required | |-------------|-----------------|--------------------------| | **Long code (US)** | After 10DLC registration | [Register brand + campaign](/docs/messaging/10dlc/quickstart) | | **Toll-free (US/CA)** | After verification | [Submit toll-free verification](/docs/messaging/toll-free-verification) | | **Short code** | After provisioning | [Short code setup](/docs/messaging/messages/short-code) | | **Long code (non-US)** | Typically immediate | Check country-specific requirements | | **Alphanumeric sender ID** | After registration | [Alphanumeric ID setup](/docs/messaging/messages/alphanumeric-sender-id) | **US long codes without 10DLC registration** will experience carrier filtering and potential message blocking. Always complete 10DLC registration before sending A2P messages on US long codes. --- ## Unassign a number from a profile Remove a number's messaging profile assignment: ```bash curl curl -X PATCH "https://api.telnyx.com/v2/messaging_phone_numbers/+15551234567" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "messaging_profile_id": null }' ``` ```python Python response = requests.patch( f"https://api.telnyx.com/v2/messaging_phone_numbers/{phone_number}", headers=headers, json={"messaging_profile_id": None}, ) print(f"Unassigned {phone_number} from messaging profile") ``` ```javascript Node await axios.patch( `https://api.telnyx.com/v2/messaging_phone_numbers/${encodeURIComponent(phoneNumber)}`, { messaging_profile_id: null }, { headers } ); console.log(`Unassigned ${phoneNumber} from messaging profile`); ``` Unassigning a number from a messaging profile means it will no longer receive inbound message webhooks or be available for outbound messaging through that profile. --- ## Troubleshooting **Possible causes:** - The number doesn't have messaging capabilities. Check your number order — not all numbers support SMS/MMS. - The number hasn't finished provisioning yet. Wait a few minutes after purchase. - The number is on a different Telnyx account. **Fix:** Verify the number's capabilities via `GET /v2/phone_numbers/{id}` and check for `messaging` in the features. **Cause:** The `from` number in your send request isn't assigned to a messaging profile. **Fix:** Assign the number to a profile using the [assignment API](#step-2-assign-a-number-to-a-messaging-profile), or use the messaging profile's number pool to automatically select a number. **Possible causes:** - The number isn't assigned to a messaging profile - The messaging profile doesn't have a webhook URL configured - Your webhook endpoint is returning errors (check [MDR logs](/docs/messaging/messages/message-detail-records)) **Fix:** Verify the number → profile → webhook URL chain. Test with [ngrok](/development/development-tools/ngrok-setup) for local development. **Cause:** For US long codes, messages may be filtered by carriers if 10DLC registration isn't complete. **Fix:** Complete [10DLC brand and campaign registration](/docs/messaging/10dlc/quickstart). For toll-free, complete [verification](/docs/messaging/toll-free-verification). **Possible causes:** - The number is already assigned to a different product (voice connection, etc.) that conflicts - The messaging profile ID is invalid - The number belongs to a different organization **Fix:** Check the profile ID, verify number ownership, and ensure no conflicting product assignments. --- ## Next steps Create and configure messaging profiles with webhooks and features. Send your first SMS/MMS using a configured number. Use multiple numbers in a pool for automatic sender selection. Register your brand and campaign for US long code messaging. --- ## Toll Free Verification ### Toll-Free Verification > Source: https://developers.telnyx.com/docs/messaging/toll-free-verification.md Telnyx toll-free verification now supports Business Registration Number (BRN) fields to strengthen business verification and compliance. Starting **February 17th, 2026**, three BRN fields will be required for all new toll-free verification submissions. The following fields are **required** for all new toll-free verification requests: * `businessRegistrationNumber` * `businessRegistrationType` * `businessRegistrationCountry` Submissions missing these fields will be rejected. ## Why This Matters U.S. wireless carriers require toll-free numbers (800, 888, 877, 866, 855, 844, 833) used for SMS/MMS to complete verification. The new BRN fields provide carriers with verified business identity information, helping to: - Reduce verification processing time - Improve approval rates - Ensure compliance with carrier policies - Prevent fraudulent messaging ## New Business Registration Fields ### Required Fields (February 17th, 2026) #### businessRegistrationNumber The official government-issued business registration identifier. | Property | Value | | -------------- | -------------------------------- | | **Type** | String | | **Max Length** | 500 characters | | **Required** | February 17th, 2026 | | **Nullable** | Currently yes, no after Feb 2026 | **Examples by Country**: | Country | Type | Example Format | | -------------- | --------------- | --------------------------- | | United States | EIN | `12-3456789` or `123456789` | | Canada | Business Number | `123456789RC0001` | | United Kingdom | Companies House | `12345678` | | Australia | ABN | `51824753556` | | Germany | VAT | `DE123456789` | **Where to Find**: - **US**: [IRS EIN Lookup](https://www.irs.gov/) - Check your EIN confirmation letter or SS-4 form - **Canada**: CRA Business Number from your registration documents - **UK**: [Companies House](https://www.gov.uk/get-information-about-a-company) registration certificate - **Australia**: [ABN Lookup](https://abr.business.gov.au/) - **EU**: VAT registration certificate from your national tax authority #### businessRegistrationType The type or classification of your business registration. | Property | Value | | -------------- | -------------------------------- | | **Type** | String | | **Max Length** | 500 characters | | **Required** | February 17, 2026 | | **Nullable** | Currently yes, no after Feb 2026 | **Common Values**: - `EIN` - U.S. Employer Identification Number - `CRA` - Canadian Revenue Agency Business Number - `Companies House` - UK company registration - `ABN` - Australian Business Number - `VAT` - European Union VAT registration - `SSN` - For U.S. sole proprietors without EIN #### businessRegistrationCountry ISO 3166-1 alpha-2 country code of the authority that issued the registration. | Property | Value | | ------------ | -------------------------------- | | **Type** | String | | **Length** | Exactly 2 characters | | **Format** | ISO 3166-1 alpha-2 | | **Required** | February 17, 2026 | | **Nullable** | Currently yes, no after Feb 2026 | **Validation**: - Must be exactly 2 letters - Only alphabetic characters (A-Z) - Automatically converted to uppercase (`"us"` → `"US"`) - Returns HTTP 400 if invalid **Example Values**: `US`, `CA`, `GB`, `AU`, `DE`, `FR`, `JP` See complete list: [ISO 3166-1 country codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) ### Optional Fields These fields are optional but recommended for faster verification processing. #### doingBusinessAs DBA, trade name, or brand name if different from your legal business name. | Property | Value | | -------------- | -------------- | | **Type** | String | | **Max Length** | 500 characters | | **Required** | No | **Example**: Legal name "Acme Corporation Inc.", DBA "Acme Services" #### entityType Legal business entity classification. | Property | Value | | ------------------ | -------------------------------------------------------------------------------- | | **Type** | Enum | | **Required** | No | | **Allowed Values** | `SOLE_PROPRIETOR`, `PRIVATE_PROFIT`, `PUBLIC_PROFIT`, `NON_PROFIT`, `GOVERNMENT` | **Value Descriptions**: | Value | Description | | ----------------- | -------------------------------------------- | | `SOLE_PROPRIETOR` | Individual or sole proprietorship | | `PRIVATE_PROFIT` | Private for-profit corporation (most common) | | `PUBLIC_PROFIT` | Publicly traded for-profit company | | `NON_PROFIT` | 501(c) or charitable organization | | `GOVERNMENT` | Government entity or agency | #### optInConfirmationResponse Message sent to subscribers confirming their opt-in. | Property | Value | | -------------- | -------------- | | **Type** | String | | **Max Length** | 500 characters | | **Required** | No | **Example**: `"You are now subscribed to Acme alerts. Reply STOP to unsubscribe. Msg&data rates may apply."` #### helpMessageResponse Automated response when subscribers text HELP. | Property | Value | | -------------- | -------------- | | **Type** | String | | **Max Length** | 500 characters | | **Required** | No | **Example**: `"Acme Support: Call 1-800-555-0123 or email help@acme.com. Reply STOP to unsubscribe."` #### privacyPolicyURL URL to your business privacy policy. | Property | Value | | -------------- | -------------- | | **Type** | String | | **Max Length** | 500 characters | | **Required** | No | **Example**: `"https://www.acme.com/privacy"` URL format validation is not enforced. Provide a publicly accessible URL. #### termsAndConditionURL URL to your business terms and conditions. | Property | Value | | -------------- | -------------- | | **Type** | String | | **Max Length** | 500 characters | | **Required** | No | **Example**: `"https://www.acme.com/terms"` #### ageGatedContent Indicates if messaging content requires age verification (18+ or 21+). | Property | Value | | ------------ | ------- | | **Type** | Boolean | | **Default** | `false` | | **Required** | No | Set to `true` for alcohol, tobacco, cannabis, or other age-restricted content. #### optInKeywords Keywords subscribers use to opt-in to your messaging program. | Property | Value | | -------------- | -------------- | | **Type** | String | | **Max Length** | 500 characters | | **Required** | No | **Example**: `"START, YES, SUBSCRIBE, JOIN"` ## API Usage ### Create Verification Request with BRN Fields **Endpoint**: `POST /public/api/v2/requests` **Request with BRN Fields**: ```bash curl --request POST \ --url https://api.telnyx.com/v2/messaging_tollfree/verification/requests \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "businessName": "Telnyx LLC", "corporateWebsite": "http://example.com", "businessAddr1": "600 Congress Avenue", "businessAddr2": "14th Floor", "businessCity": "Austin", "businessState": "Texas", "businessZip": "78701", "businessContactFirstName": "John", "businessContactLastName": "Doe", "businessContactEmail": "email@example.com", "businessContactPhone": "+18005550100", "messageVolume": "100,000", "phoneNumbers": [ { "phoneNumber": "+18773554398" }, { "phoneNumber": "+18773554399" } ], "useCase": "2FA", "useCaseSummary": "This is a use case where Telnyx sends out 2FA codes to portal users to verify their identity in order to sign into the portal", "productionMessageContent": "Your Telnyx OTP is XXXX", "optInWorkflow": "User signs into the Telnyx portal, enters a number and is prompted to select whether they want to use 2FA verification for security purposes. If they'\''ve opted in a confirmation message is sent out to the handset", "optInWorkflowImageURLs": [ { "url": "https://telnyx.com/sign-up" }, { "url": "https://telnyx.com/company/data-privacy" } ], "additionalInformation": "", "isvReseller": "Yes", "webhookUrl": "http://example-webhook.com", "businessRegistrationNumber": "12-3456789", "businessRegistrationType": "EIN", "businessRegistrationCountry": "US", "doingBusinessAs": "Acme Services", "entityType": "SOLE_PROPRIETOR", "optInConfirmationResponse": "You have successfully opted in to receive messages from Acme Corp", "helpMessageResponse": "Reply HELP for assistance or STOP to unsubscribe. Contact: support@example.com", "privacyPolicyURL": "https://example.com/privacy", "termsAndConditionURL": "https://example.com/terms", "ageGatedContent": false, "optInKeywords": "START, YES, SUBSCRIBE" }' ``` **Response** (HTTP 201): ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "verificationRequestId": "TFV-ABC123", "verificationStatus": "Pending", "businessName": "Acme Corporation", "businessRegistrationNumber": "12-3456789", "businessRegistrationType": "EIN", "businessRegistrationCountry": "US", "entityType": "PRIVATE_PROFIT", "createdAt": "2025-10-13T12:00:00Z" } ``` ### Retrieve Verification with BRN Fields **Endpoint**: `GET /public/api/v2/requests/{id}` ```bash curl -X GET https://api.telnyx.com/public/api/v2/requests/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** (HTTP 200): ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "verificationRequestId": "TFV-ABC123", "verificationStatus": "Approved", "businessName": "Acme Corporation", "businessRegistrationNumber": "12-3456789", "businessRegistrationType": "EIN", "businessRegistrationCountry": "US", "entityType": "PRIVATE_PROFIT", "phoneNumbers": [{"phoneNumber": "+18773554398"}], "createdAt": "2025-10-13T12:00:00Z", "updatedAt": "2025-10-15T09:30:00Z" } ``` ### Update BRN Fields **Endpoint**: `PATCH /public/api/v2/requests/{id}` ```bash curl -X PATCH https://api.telnyx.com/public/api/v2/requests/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "businessRegistrationNumber": "98-7654321", "businessRegistrationCountry": "US" }' ``` ## Code Examples ### Python ```python import requests # Create verification with BRN fields payload = { "businessName": "Acme Corporation", "corporateWebsite": "https://www.acme.com", "businessAddr1": "123 Main Street", "businessCity": "Chicago", "businessState": "Illinois", "businessZip": "60601", "businessContactFirstName": "John", "businessContactLastName": "Doe", "businessContactEmail": "compliance@acme.com", "businessContactPhone": "+18005551234", "messageVolume": "100,000", "phoneNumbers": [{"phoneNumber": "+18773554398"}], "useCase": "Account Notifications", "useCaseSummary": "Security alerts and account updates", "productionMessageContent": "Your Acme security code is: 123456", "optInWorkflow": "Users opt-in during registration", "optInWorkflowImageURLs": [{"url": "https://www.acme.com/opt-in.png"}], "additionalInformation": "Messages to verified numbers only", "isvReseller": "Yes", # BRN Fields (required Feb 2026) "businessRegistrationNumber": "12-3456789", "businessRegistrationType": "EIN", "businessRegistrationCountry": "US", "entityType": "PRIVATE_PROFIT" } response = requests.post( "https://api.telnyx.com/public/api/v2/requests", json=payload, headers={"Authorization": "Bearer YOUR_API_KEY"} ) if response.status_code == 201: data = response.json() print(f"Verification created: {data['verificationRequestId']}") else: print(f"Error: {response.status_code} - {response.text}") ``` ### JavaScript/TypeScript ```typescript interface TollFreeVerificationRequest { businessName: string; corporateWebsite: string; businessAddr1: string; businessCity: string; businessState: string; businessZip: string; businessContactFirstName: string; businessContactLastName: string; businessContactEmail: string; businessContactPhone: string; messageVolume: string; phoneNumbers: Array<{ phoneNumber: string }>; useCase: string; useCaseSummary: string; productionMessageContent: string; optInWorkflow: string; optInWorkflowImageURLs: Array<{ url: string }>; additionalInformation?: string; isvReseller: string; // BRN Fields (required Feb 2026) businessRegistrationNumber: string; businessRegistrationType: string; businessRegistrationCountry: string; entityType?: 'SOLE_PROPRIETOR' | 'PRIVATE_PROFIT' | 'PUBLIC_PROFIT' | 'NON_PROFIT' | 'GOVERNMENT'; } async function createVerification(request: TollFreeVerificationRequest, apiKey: string) { const response = await fetch('https://api.telnyx.com/public/api/v2/requests', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(request) }); if (!response.ok) { const error = await response.json(); throw new Error(`Verification failed: ${JSON.stringify(error)}`); } return response.json(); } // Example usage const request: TollFreeVerificationRequest = { businessName: "Acme Corporation", corporateWebsite: "https://www.acme.com", businessAddr1: "123 Main Street", businessCity: "Chicago", businessState: "Illinois", businessZip: "60601", businessContactFirstName: "John", businessContactLastName: "Doe", businessContactEmail: "compliance@acme.com", businessContactPhone: "+18005551234", messageVolume: "100,000", phoneNumbers: [{ phoneNumber: "+18773554398" }], useCase: "Account Notifications", useCaseSummary: "Security alerts and account updates", productionMessageContent: "Your code is: 123456", optInWorkflow: "Users opt-in during registration", optInWorkflowImageURLs: [{ url: "https://www.acme.com/opt-in.png" }], additionalInformation: "More context"; isvReseller: "Yes"; businessRegistrationNumber: "12-3456789", businessRegistrationType: "EIN", businessRegistrationCountry: "US", entityType: "PRIVATE_PROFIT" }; createVerification(request, 'YOUR_API_KEY') .then(result => console.log('Created:', result.verificationRequestId)) .catch(error => console.error('Error:', error.message)); ``` ## Error Handling ### Validation Errors (HTTP 400) #### Missing Required Fields (After February 2026) **Response**: ```json { "errors": [ { "field": "businessRegistrationNumber", "message": "This field is required as of February 17th 2026" }, { "field": "businessRegistrationType", "message": "This field is required as of February 17th 2026" }, { "field": "businessRegistrationCountry", "message": "This field is required as of February 17th 2026" } ] } ``` #### Invalid Country Code ```json { "detail": "businessRegistrationCountry must be a 2-character ISO 3166-1 alpha-2 country code" } ``` #### Invalid Entity Type ```json { "detail": "entityType must be one of: SOLE_PROPRIETOR, PRIVATE_PROFIT, PUBLIC_PROFIT, NON_PROFIT, GOVERNMENT" } ``` ### Other Status Codes | Code | Description | | ---- | ------------------------------------- | | 200 | Success (GET, PATCH) | | 201 | Created (POST) | | 204 | Deleted (DELETE) | | 400 | Bad Request - Invalid data | | 401 | Unauthorized - Invalid API key | | 404 | Not Found - Invalid request ID | | 422 | Unprocessable Entity - Invalid format | ## Migration Guide ### Timeline | Period | Status | Action | | ------------------ | --------- | ------------------------------------------- | | **Now - Dec 2025** | Optional | BRN fields can be included but not required | | **Feb 1, 2026** | Mandatory | All 3 required BRN fields must be included | ### Preparation Steps **1. Gather Business Registration Information** Locate your: - Business registration number (EIN, VAT, ABN, etc.) - Registration type identifier - Issuing country code **2. Update Your Integration** Add BRN fields to your API requests: ```diff const request = { businessName: "Acme Corp", corporateWebsite: "https://acme.com", // ... other required fields + businessRegistrationNumber: "12-3456789", + businessRegistrationType: "EIN", + businessRegistrationCountry: "US" }; ``` **3. Test Your Implementation** - Submit test requests with BRN fields - Verify fields are returned in responses - Test validation error handling - Confirm country code uppercase conversion **4. Update Error Handling** Prepare for validation errors after February 2026: ```javascript try { const result = await createVerification(request); } catch (error) { if (error.status === 400) { console.error('Validation failed:', error.errors); // Handle missing BRN fields } } ``` ### Backward Compatibility **Until February 17, 2026**: - Requests without BRN fields continue to work - No breaking changes to existing integrations - BRN fields default to `null` if not provided **After February 17, 2026**: - Requests without 3 required BRN fields will be rejected (HTTP 400) - Update your integration before this date ## Frequently Asked Questions ### When do BRN fields become mandatory? **February 17th, 2026**. All new verification requests must include `businessRegistrationNumber`, `businessRegistrationType`, and `businessRegistrationCountry`. ### Do I need to resubmit existing verifications? No. Approved verifications before February 2026 remain valid. ### Where do I find my business registration number? - **US**: IRS EIN confirmation letter or [IRS.gov](https://www.irs.gov/) - **Canada**: CRA Business Number registration documents - **UK**: [Companies House](https://www.gov.uk/get-information-about-a-company) certificate - **Australia**: [ABN Lookup](https://abr.business.gov.au/) - **EU**: VAT registration certificate ### What if I'm a sole proprietor without an EIN? U.S. sole proprietors can use their Social Security Number as `businessRegistrationNumber` with type `SSN`. Other countries may have similar individual tax identifiers. ### Can I update BRN fields after submission? Yes. Use `PATCH /public/api/v2/requests/{id}` to update BRN fields. ### Why are country codes converted to uppercase? For consistency. Sending `"us"` automatically becomes `"US"` in responses and storage. ### Which entity type should I choose? Choose the type matching your official business registration: - `SOLE_PROPRIETOR` - Individual or sole proprietorship - `PRIVATE_PROFIT` - Private corporation (most common) - `PUBLIC_PROFIT` - Publicly traded company - `NON_PROFIT` - 501(c) or charitable organization - `GOVERNMENT` - Government entity ### How long does toll-free verification take? Verification typically takes **1-2 weeks**, depending on the carrier review queue and the completeness of your submission. Including accurate BRN fields can speed up the process. ### What happens if my verification is rejected? You'll receive a webhook notification with the rejection reason. Common causes: - Incomplete or inaccurate business information - Message samples don't match your declared use case - Missing opt-out language in sample messages - Business couldn't be verified with the provided registration number Fix the issues and resubmit — there's no limit on resubmissions. ### Can I send messages before verification is complete? Unverified toll-free numbers have **limited throughput** and may experience carrier filtering. Complete verification to unlock full sending capabilities (up to 20 MPS). ### What's the difference between toll-free verification and 10DLC registration? | Aspect | Toll-Free Verification | 10DLC Registration | |--------|----------------------|-------------------| | **Number type** | Toll-free (800, 888, etc.) | Local 10-digit numbers | | **Timeline** | 1-2 weeks | Days (plus 1-7 days for vetting) | | **Registry** | Carrier-managed | The Campaign Registry (TCR) | | **Throughput** | Up to 20 MPS | Varies by vetting score | | **Cost** | Per-message | Per-message + campaign fees | ## Related resources API reference for managing toll-free verification requests. Send messages from your verified toll-free numbers. Alternative registration path for local 10-digit numbers. Configure opt-in/out behavior including toll-free specific handling. ## Support Need help with toll-free verification? - **Support Portal**: [support.telnyx.com](https://support.telnyx.com) - **Email**: [support@telnyx.com](mailto:support@telnyx.com) - **Developer Community**: Join discussions and get help from other developers --- ### Troubleshooting > Source: https://developers.telnyx.com/docs/messaging/toll-free-verification/troubleshooting.md This guide covers common toll-free verification failures, rejection reasons, resubmission best practices, and messaging issues with toll-free numbers. Use it to diagnose problems and get your verification approved faster. **Quick links:** [Rejection reasons](#verification-rejection-reasons) · [Resubmission guide](#resubmission-process) · [Delivery issues](#delivery-issues-after-verification) · [Status tracking](#checking-verification-status) · [Diagnostic checklist](#diagnostic-checklist) --- ## Verification lifecycle Understanding where your verification can fail helps target the right fix: ```mermaid graph LR A[Submit] -->|Validation| B{Valid?} B -->|No| C[API Error 400/422] B -->|Yes| D[Under Review] D -->|Carrier Review| E{Approved?} E -->|Yes| F[Verified ✓] E -->|No| G[Rejected] G -->|Fix & Resubmit| A style F fill:#10b981,color:#fff style G fill:#ef4444,color:#fff style C fill:#f59e0b,color:#fff ``` | Stage | Timeline | What happens | |-------|----------|-------------| | **Submission** | Instant | API validates fields, returns 201 or error | | **Under review** | 1–2 weeks | Carriers review business identity and messaging use case | | **Decision** | — | Approved (full throughput) or rejected (with reason) | | **Resubmission** | Instant | Fix issues and resubmit — no limit on attempts | --- ## Verification rejection reasons Rejections come from carrier review. Each has specific causes and fixes. ### Business information issues **Rejection reason:** Business name does not match public records. **Root cause:** The `businessName` you submitted doesn't match what's on file with the Secretary of State, IRS, or similar authority for your registration number. **Fix:** 1. Look up your exact legal name on your state's Secretary of State website 2. Cross-reference with your EIN confirmation letter from the IRS 3. Include suffixes exactly: "Inc.", "LLC", "Corp." — these matter 4. Resubmit with the corrected name ```bash curl curl -X PATCH https://api.telnyx.com/v2/tollFreeVerification/requests/{requestId} \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "businessName": "Acme Corporation Inc." }' ``` **Common mistakes:** - Using a DBA/trade name instead of the legal entity name - Missing "LLC", "Inc.", etc. - Using an old company name after a legal name change **Rejection reason:** Unable to verify business registration information. **Root cause:** The EIN, ABN, VAT number, or other registration number doesn't match the business name or doesn't exist in public records. **Fix:** 1. Verify your EIN at [IRS.gov](https://www.irs.gov/) or on your SS-4 confirmation letter 2. Ensure the `businessRegistrationType` matches the number format (e.g., "EIN" for US tax IDs) 3. Confirm `businessRegistrationCountry` is correct (ISO alpha-2) 4. For sole proprietors using SSN, ensure the name matches exactly ```bash curl curl -X PATCH https://api.telnyx.com/v2/tollFreeVerification/requests/{requestId} \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "businessRegistrationNumber": "12-3456789", "businessRegistrationType": "EIN", "businessRegistrationCountry": "US" }' ``` **Rejection reason:** Corporate website could not be verified or does not match the business. **Root cause:** Carriers check that the website is live, matches the business name, and has content relevant to the declared use case. **Fix:** 1. Ensure the URL is accessible (no authentication required, no redirects to a different domain) 2. The website must show the company name prominently 3. Website content should relate to the messaging use case 4. HTTPS is strongly preferred 5. Under construction / parking pages will cause rejection **Before resubmitting, verify:** ```bash # Check website is accessible curl -sI https://yourbusiness.com | head -5 # Check it resolves to expected domain curl -sL -o /dev/null -w '%{url_effective}' https://yourbusiness.com ``` **Rejection reason:** Contact information could not be verified. **Root cause:** The phone number or email provided doesn't match public records for the business, or isn't reachable. **Fix:** 1. Use a phone number that's publicly associated with the business (Google listing, website, etc.) 2. Use a business email domain (not gmail.com, yahoo.com for corporations) 3. Ensure the contact person is authorized to represent the business **Rejection reason:** Entity type does not match business records. **Root cause:** You selected `PRIVATE_PROFIT` but the business is registered as a nonprofit, or vice versa. **Fix:** Choose the correct entity type based on your actual business registration: | Entity Type | Use for | |-------------|---------| | `SOLE_PROPRIETOR` | Individual / sole proprietorship | | `PRIVATE_PROFIT` | Private corporation (most common) | | `PUBLIC_PROFIT` | Publicly traded company | | `NON_PROFIT` | 501(c)(3) or charitable organization | | `GOVERNMENT` | Government entity at any level | ### Messaging use case issues **Rejection reason:** Message samples are inconsistent with the stated use case. **Root cause:** Your `useCaseSummary` says one thing (e.g., "appointment reminders") but your sample messages show something different (e.g., marketing promotions). **Fix:** 1. Ensure every sample message directly relates to your declared use case 2. Include realistic content — not placeholder text 3. Show the full message including opt-out language 4. If you have multiple use cases, describe all of them in `useCaseSummary` **Good sample:** ``` Hi Sarah, this is Dr. Smith's office. Your appointment is confirmed for March 15 at 2:00 PM. Reply STOP to opt out of reminders. ``` **Bad sample:** ``` Test message for verification purposes. ``` **Rejection reason:** Sample messages must include opt-out instructions. **Root cause:** At least one sample message is missing STOP/opt-out language, or the opt-out mechanism isn't clear. **Fix:** 1. Include "Reply STOP to unsubscribe" (or similar) in every sample 2. The opt-out instruction should be natural, not buried 3. STOP, CANCEL, UNSUBSCRIBE, QUIT, END should all work **Required format examples:** - "Reply STOP to opt out." - "Text STOP to unsubscribe." - "Reply STOP to end messages. Msg & data rates may apply." **Rejection reason:** Declared message volume does not align with the use case. **Root cause:** Claiming a very high volume for a use case that typically doesn't generate it, or vice versa. **Fix:** 1. Be honest about expected volumes — carriers cross-reference similar businesses 2. If volume is high, explain why (large customer base, time-sensitive notifications) 3. Start conservative and increase as your messaging matures **Rejection reason:** Opt-in process is unclear or not documented. **Root cause:** You didn't adequately describe how recipients consent to receive messages. **Fix:** Describe the full opt-in flow in your `messageFlow` field: - Where users sign up (website form, checkout flow, in-app) - What consent language they see - Whether it's single or double opt-in - How consent records are maintained **Good example:** ``` Customers opt in during checkout at acme.com/checkout by checking "I agree to receive order updates and shipping notifications via SMS." Consent is recorded with timestamp and IP address. Customers can opt out at any time by replying STOP. ``` **Bad example:** ``` Users sign up on our website. ``` **Rejection reason:** Message content contains prohibited or restricted material. **Root cause:** Sample messages or use case involves content types that carriers restrict: - Cannabis/CBD - Adult content - Gambling (without proper licensing documentation) - High-risk financial services (payday loans, crypto trading signals) - Third-party lead generation **Fix:** 1. If your content is genuinely prohibited, toll-free may not be the right channel 2. For regulated industries (gambling, financial services), include licensing documentation 3. Remove any references to restricted content from samples 4. Contact [Telnyx support](https://support.telnyx.com) for guidance on restricted use cases --- ## Resubmission process After a rejection, you can fix the issues and resubmit. There's no limit on resubmission attempts. Check your verification status via API or the [Telnyx portal](https://portal.telnyx.com/#/app/messaging/toll-free-verification). The rejection reason tells you exactly what to fix. ```bash curl curl -s https://api.telnyx.com/v2/tollFreeVerification/requests/{requestId} \ -H "Authorization: Bearer $TELNYX_API_KEY" | python3 -m json.tool ``` ```python Python import telnyx import os telnyx.api_key = os.environ["TELNYX_API_KEY"] request = telnyx.TollFreeVerificationRequest.retrieve("REQUEST_ID") print(f"Status: {request.status}") print(f"Rejection reason: {request.rejection_reason}") ``` ```javascript Node import Telnyx from "telnyx"; const telnyx = new Telnyx(process.env.TELNYX_API_KEY); const { data: request } = await telnyx.tollFreeVerification.requests.retrieve("REQUEST_ID"); console.log(`Status: ${request.status}`); console.log(`Rejection reason: ${request.rejectionReason}`); ``` Update only the fields that caused the rejection. Use PATCH to update specific fields: ```bash curl curl -X PATCH https://api.telnyx.com/v2/tollFreeVerification/requests/{requestId} \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "businessName": "Corrected Legal Name LLC", "messageVolume": "10000", "useCaseSummary": "Updated description of our messaging use case...", "sampleMessage1": "Updated sample with opt-out. Reply STOP to unsubscribe.", "sampleMessage2": "Another realistic sample. Reply STOP to opt out." }' ``` ```python Python import telnyx import os telnyx.api_key = os.environ["TELNYX_API_KEY"] request = telnyx.TollFreeVerificationRequest.update( "REQUEST_ID", business_name="Corrected Legal Name LLC", message_volume="10000", use_case_summary="Updated description of our messaging use case...", sample_message1="Updated sample with opt-out. Reply STOP to unsubscribe.", sample_message2="Another realistic sample. Reply STOP to opt out.", ) print(f"Updated status: {request.status}") ``` ```javascript Node import Telnyx from "telnyx"; const telnyx = new Telnyx(process.env.TELNYX_API_KEY); const { data: request } = await telnyx.tollFreeVerification.requests.update("REQUEST_ID", { businessName: "Corrected Legal Name LLC", messageVolume: "10000", useCaseSummary: "Updated description of our messaging use case...", sampleMessage1: "Updated sample with opt-out. Reply STOP to unsubscribe.", sampleMessage2: "Another realistic sample. Reply STOP to opt out.", }); console.log(`Updated status: ${request.status}`); ``` ```ruby Ruby require "telnyx" Telnyx.api_key = ENV["TELNYX_API_KEY"] request = Telnyx::TollFreeVerificationRequest.update( "REQUEST_ID", business_name: "Corrected Legal Name LLC", message_volume: "10000", use_case_summary: "Updated description...", sample_message1: "Updated sample with opt-out. Reply STOP to unsubscribe." ) puts "Updated status: #{request.status}" ``` ```go Go package main import ( "context" "fmt" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient(option.WithAPIKey("YOUR_API_KEY")) request, err := client.TollFreeVerification.Requests.Update( context.TODO(), "REQUEST_ID", telnyx.TollFreeVerificationRequestUpdateParams{ BusinessName: telnyx.String("Corrected Legal Name LLC"), MessageVolume: telnyx.String("10000"), UseCaseSummary: telnyx.String("Updated description..."), }, ) if err != nil { panic(err) } fmt.Printf("Updated status: %s\n", request.Status) } ``` ```java Java import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; TelnyxClient client = TelnyxOkHttpClient.fromEnv(); var request = client.tollFreeVerification().requests().update( "REQUEST_ID", TollFreeVerificationRequestUpdateParams.builder() .businessName("Corrected Legal Name LLC") .messageVolume("10000") .useCaseSummary("Updated description...") .build() ); System.out.println("Updated status: " + request.status()); ``` ```csharp .NET using Telnyx; var client = new TelnyxClient(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var request = await client.TollFreeVerification.Requests.UpdateAsync( "REQUEST_ID", new TollFreeVerificationRequestUpdateParams { BusinessName = "Corrected Legal Name LLC", MessageVolume = "10000", UseCaseSummary = "Updated description...", } ); Console.WriteLine($"Updated status: {request.Status}"); ``` ```php PHP $telnyx = new \Telnyx\TelnyxClient(getenv('TELNYX_API_KEY')); $request = $telnyx->tollFreeVerification->requests->update('REQUEST_ID', [ 'business_name' => 'Corrected Legal Name LLC', 'message_volume' => '10000', 'use_case_summary' => 'Updated description...', ]); echo "Updated status: " . $request->status . "\n"; ``` After updating, the verification automatically enters the review queue again. No separate "submit" action is needed — the PATCH triggers re-review. Set up webhooks to get notified when the review completes: ```python Python from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/webhooks/toll-free", methods=["POST"]) def handle_toll_free_webhook(): event = request.json event_type = event.get("data", {}).get("event_type", "") if event_type == "toll_free_verification.status_update": payload = event["data"]["payload"] status = payload["status"] request_id = payload["id"] if status == "verified": print(f"✅ Verification {request_id} approved!") # Enable full messaging for this number elif status == "rejected": reason = payload.get("rejectionReason", "No reason provided") print(f"❌ Verification {request_id} rejected: {reason}") # Alert team, prepare resubmission return jsonify({"status": "ok"}), 200 ``` ### Resubmission tips | Do | Don't | |----|-------| | Fix **only** the cited rejection reason | Change everything at once | | Use exact legal business name | Use informal names or abbreviations | | Provide realistic sample messages | Use generic placeholder text | | Include opt-out in every sample | Assume opt-out is implied | | Wait for the full review cycle | Submit multiple times in rapid succession | --- ## Delivery issues after verification Even after successful verification, you may experience delivery problems. ### Throughput limitations | Verification status | Throughput | Notes | |--------------------|-----------|-------| | **Unverified** | ~0.25 MPS | Heavy carrier filtering, low reliability | | **Pending review** | ~1 MPS | Some filtering may apply | | **Verified** | Up to 20 MPS | Full throughput, minimal filtering | Sending above your throughput tier results in message queuing and eventual `40014` (Expired in queue) errors. If you need more than 20 MPS from toll-free numbers, consider using [short codes](/docs/messaging/messages/short-code/index) or [10DLC with number pools](/docs/messaging/messages/number-pool/index). ### Common delivery errors **Cause:** Message content triggered carrier-level content filters, independent of verification status. **Fix:** 1. Review message content for spam trigger words (FREE, WINNER, ACT NOW) 2. Avoid URL shorteners (bit.ly, tinyurl) — use full URLs or [Telnyx URL shortening](/docs/messaging/messages/url-shortening/index) 3. Don't send identical messages to many recipients in rapid succession 4. Check if recipient has previously opted out 5. Review the [error code reference](/docs/messaging/messages/error-codes/index) for specific guidance **Cause:** The recipient number is invalid, deactivated, or not SMS-capable. **Fix:** 1. Validate numbers before sending (use [Number Lookup API](/api-reference/number-lookup/look-up-phone-number)) 2. Remove landlines and VoIP numbers that don't support SMS 3. Check for typos in the recipient number **Cause:** Sending faster than your verified throughput allows. **Fix:** 1. Implement client-side rate limiting (see [Rate Limiting guide](/docs/messaging/messages/rate-limiting/index)) 2. Spread traffic across multiple toll-free numbers if needed 3. Use a messaging profile with number pooling for high-volume use cases **Cause:** Message sat in the queue too long, usually due to throughput congestion. **Fix:** 1. Reduce sending rate to stay within throughput limits 2. Check if a carrier outage is causing delivery backlog 3. For time-sensitive messages, set a shorter validity period **Cause:** Some carriers may still filter your toll-free traffic even after verification, especially for: - New verifications (carrier trust builds over time) - Content that resembles spam patterns - High complaint rates from recipients **Fix:** 1. Start with lower volumes and ramp up gradually over 1–2 weeks 2. Monitor delivery rates per carrier using [MDRs](/docs/messaging/messages/message-detail-records/index) 3. Ensure opt-out is working properly (high complaint rates trigger filtering) 4. Contact Telnyx support if specific carriers consistently filter your traffic --- ## Checking verification status ### Via API ```bash curl # Get status of a specific verification curl -s https://api.telnyx.com/v2/tollFreeVerification/requests/{requestId} \ -H "Authorization: Bearer $TELNYX_API_KEY" \ | python3 -c " import sys, json data = json.load(sys.stdin)['data'] print(f\"Status: {data['status']}\") print(f\"Phone numbers: {', '.join(data.get('phoneNumbers', []))}\") if data.get('rejectionReason'): print(f\"Rejection reason: {data['rejectionReason']}\") " # List all verifications curl -s "https://api.telnyx.com/v2/tollFreeVerification/requests?page[size]=25" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ | python3 -c " import sys, json data = json.load(sys.stdin)['data'] for req in data: numbers = ', '.join(req.get('phoneNumbers', [])) print(f\"{req['id']} | {req['status']:12} | {numbers}\") " ``` ```python Python import telnyx import os telnyx.api_key = os.environ["TELNYX_API_KEY"] # List all verifications verifications = telnyx.TollFreeVerificationRequest.list(page={"size": 25}) for v in verifications.data: numbers = ", ".join(v.phone_numbers or []) print(f"{v.id} | {v.status:12} | {numbers}") if v.status == "rejected": print(f" Reason: {v.rejection_reason}") ``` ```javascript Node import Telnyx from "telnyx"; const telnyx = new Telnyx(process.env.TELNYX_API_KEY); // List all verifications const { data: verifications } = await telnyx.tollFreeVerification.requests.list({ page: { size: 25 }, }); for (const v of verifications) { const numbers = (v.phoneNumbers || []).join(", "); console.log(`${v.id} | ${v.status.padEnd(12)} | ${numbers}`); if (v.status === "rejected") { console.log(` Reason: ${v.rejectionReason}`); } } ``` ### Via Portal 1. Log in to the [Telnyx Portal](https://portal.telnyx.com) 2. Navigate to **Messaging** → **Toll-Free Verification** 3. View status, rejection reasons, and submission details for each verification ### Status reference | Status | Meaning | Action needed | |--------|---------|--------------| | `draft` | Created but not yet submitted | Complete required fields and submit | | `pending` | Under carrier review | Wait (1–2 weeks typical) | | `verified` | Approved ✅ | None — full throughput unlocked | | `rejected` | Carrier rejected ❌ | Fix issues and resubmit | --- ## Diagnostic checklist Use this checklist when troubleshooting verification issues: ### Before submitting - [ ] Business name matches exact legal name (including Inc./LLC/etc.) - [ ] EIN/BRN is correct and matches the business name - [ ] Website is live, accessible, and shows the business name - [ ] Contact phone and email are valid and publicly associated with the business - [ ] Entity type matches business registration - [ ] Use case summary clearly describes messaging purpose - [ ] Sample messages are realistic (not placeholder text) - [ ] Every sample includes opt-out language ("Reply STOP to unsubscribe") - [ ] Message flow describes how users consent to receive messages - [ ] Volume estimate is reasonable for the use case ### After rejection - [ ] Read the rejection reason completely - [ ] Cross-reference with the [rejection reasons](#verification-rejection-reasons) above - [ ] Fix only the specific issue cited - [ ] Double-check all information against official business records - [ ] Resubmit via PATCH (don't create a new request) - [ ] Set up webhooks to track the new review ### After verification (delivery issues) - [ ] Verify toll-free number is on an active messaging profile - [ ] Check sending rate isn't exceeding throughput tier - [ ] Review message content for spam trigger words - [ ] Confirm opt-out keywords are being processed - [ ] Check [MDRs](/docs/messaging/messages/message-detail-records/index) for carrier-specific delivery rates - [ ] Monitor [error codes](/docs/messaging/messages/error-codes/index) for patterns --- ## Timeline expectations | Stage | Typical duration | |-------|-----------------| | Initial submission to review start | 1–3 business days | | Carrier review | 5–10 business days | | Total (first submission) | 1–2 weeks | | Resubmission review | 5–10 business days | | Multiple resubmissions | Each adds ~1 week | **Expedited review** is not available for toll-free verification. The review timeline is set by carriers, not Telnyx. Ensure your first submission is complete and accurate to avoid resubmission delays. --- ## Toll-free vs. 10DLC: when to use which | Factor | Toll-Free | 10DLC | |--------|----------|-------| | **Setup time** | 1–2 weeks | Days (brand + campaign registration) | | **Throughput** | Up to 20 MPS | Varies by vetting score (up to 240 MPS with enhanced) | | **Cost** | Per-message only | Per-message + campaign fees ($10/mo) | | **Number appearance** | 800/888/877/etc. | Local area code | | **Registration** | Toll-free verification | TCR brand + campaign | | **MMS** | ✅ Supported | ✅ Supported | | **Best for** | Customer service, national reach | Local presence, high volume A2P | Many businesses use **both**: toll-free for customer service and support lines, 10DLC for marketing and transactional messages with local presence. --- ## Next steps Main verification guide with BRN fields and API reference. Understand delivery error codes and resolution steps. Similar troubleshooting guide for 10DLC registration issues. Implement client-side rate limiting to avoid throughput errors. --- ## 10 DLC ### Quickstart > Source: https://developers.telnyx.com/docs/messaging/10dlc/quickstart.md 10DLC (10-Digit Long Code) is the industry standard for application-to-person (A2P) messaging on US long code numbers. Registering your brand and campaigns provides higher throughput, better deliverability, and reduced carrier filtering. ## Registration overview ```mermaid flowchart LR A[Create Brand] --> B[Vet Brand] B --> C[Create Campaign] C --> D[Assign Phone Numbers] D --> E[Start Messaging] ``` | Step | What happens | Timeline | |------|-------------|----------| | **1. Create Brand** | Register your business identity with The Campaign Registry (TCR) | Instant | | **2. Vet Brand** | Third-party vetting determines your trust score (0-100) | 1-7 business days | | **3. Create Campaign** | Register your messaging use case | Instant (pending carrier approval) | | **4. Assign Numbers** | Link phone numbers to your campaign | Instant | **Vetting is critical.** Your brand's vetting score directly determines your throughput limits — especially on AT&T and T-Mobile. See [10DLC Rate Limits](/docs/messaging/10dlc/10dlc-rate-limits/index) for details. --- ## Step 1: Create a brand A brand represents the business entity sending messages. ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/brand \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "entityType": "PRIVATE_PROFIT", "displayName": "Acme Corp", "companyName": "Acme Corporation", "ein": "12-3456789", "phone": "+15551234567", "street": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US", "email": "admin@acmecorp.com", "website": "https://acmecorp.com", "vertical": "TECHNOLOGY" }' ``` ```python Python import os import requests API_KEY = os.environ.get("TELNYX_API_KEY") headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } brand_data = { "entityType": "PRIVATE_PROFIT", "displayName": "Acme Corp", "companyName": "Acme Corporation", "ein": "12-3456789", "phone": "+15551234567", "street": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US", "email": "admin@acmecorp.com", "website": "https://acmecorp.com", "vertical": "TECHNOLOGY", } response = requests.post( "https://api.telnyx.com/v2/10dlc/brand", headers=headers, json=brand_data, ) brand = response.json() print(f"Brand created: {brand['data']['brandId']}") ``` ```javascript Node const axios = require('axios'); const headers = { Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, 'Content-Type': 'application/json', }; const brandData = { entityType: 'PRIVATE_PROFIT', displayName: 'Acme Corp', companyName: 'Acme Corporation', ein: '12-3456789', phone: '+15551234567', street: '123 Main St', city: 'New York', state: 'NY', postalCode: '10001', country: 'US', email: 'admin@acmecorp.com', website: 'https://acmecorp.com', vertical: 'TECHNOLOGY', }; const response = await axios.post( 'https://api.telnyx.com/v2/10dlc/brand', brandData, { headers } ); console.log(`Brand created: ${response.data.data.brandId}`); ``` ```ruby Ruby require "net/http" require "json" require "uri" uri = URI("https://api.telnyx.com/v2/10dlc/brand") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" request["Content-Type"] = "application/json" request.body = { entityType: "PRIVATE_PROFIT", displayName: "Acme Corp", companyName: "Acme Corporation", ein: "12-3456789", phone: "+15551234567", street: "123 Main St", city: "New York", state: "NY", postalCode: "10001", country: "US", email: "admin@acmecorp.com", website: "https://acmecorp.com", vertical: "TECHNOLOGY" }.to_json response = http.request(request) brand = JSON.parse(response.body) puts "Brand created: #{brand['data']['brandId']}" ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) func main() { brandData := map[string]string{ "entityType": "PRIVATE_PROFIT", "displayName": "Acme Corp", "companyName": "Acme Corporation", "ein": "12-3456789", "phone": "+15551234567", "street": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US", "email": "admin@acmecorp.com", "website": "https://acmecorp.com", "vertical": "TECHNOLOGY", } body, _ := json.Marshal(brandData) req, _ := http.NewRequest("POST", "https://api.telnyx.com/v2/10dlc/brand", bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) data := result["data"].(map[string]interface{}) fmt.Printf("Brand created: %s\n", data["brandId"]) } ``` ```php PHP 'PRIVATE_PROFIT', 'displayName' => 'Acme Corp', 'companyName' => 'Acme Corporation', 'ein' => '12-3456789', 'phone' => '+15551234567', 'street' => '123 Main St', 'city' => 'New York', 'state' => 'NY', 'postalCode' => '10001', 'country' => 'US', 'email' => 'admin@acmecorp.com', 'website' => 'https://acmecorp.com', 'vertical' => 'TECHNOLOGY', ]; $ch = curl_init('https://api.telnyx.com/v2/10dlc/brand'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$apiKey}", 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($brandData), ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); echo "Brand created: {$response['data']['brandId']}\n"; ``` **Required fields:** | Field | Description | Example | |-------|-------------|---------| | `entityType` | Business type | `PRIVATE_PROFIT`, `PUBLIC_PROFIT`, `NON_PROFIT`, `GOVERNMENT` | | `displayName` | Brand display name | `Acme Corp` | | `companyName` | Legal company name | `Acme Corporation` | | `ein` | EIN/Tax ID | `12-3456789` | | `phone` | Business phone | `+15551234567` | | `street`, `city`, `state`, `postalCode`, `country` | Business address | — | | `email` | Contact email | `admin@acmecorp.com` | | `vertical` | Industry vertical | `TECHNOLOGY`, `HEALTHCARE`, `RETAIL`, etc. | Go to [Brands](https://portal.telnyx.com/#/messaging-10dlc/brands) in Mission Control Portal. Click **Create Brand** and enter your business information including legal name, EIN, address, and industry vertical. Click **Save**. Your brand is now registered with The Campaign Registry. --- ## Step 2: Vet your brand Brand vetting determines your trust score (0-100), which directly affects your messaging throughput. Higher scores unlock more messages per minute. ```bash curl # Request vetting for a brand curl -X POST https://api.telnyx.com/v2/10dlc/brand/{brandId}/vetting \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "vettingProvider": "AEGIS", "vettingClass": "STANDARD" }' ``` Check vetting status: ```bash curl -s https://api.telnyx.com/v2/10dlc/brand/{brandId} \ -H "Authorization: Bearer YOUR_API_KEY" | jq '.data.vettingScore' ``` Click on the brand you want to vet on the [Brands](https://portal.telnyx.com/#/messaging-10dlc/brands) page. Under the Vetting Request section, select **Aegis Mobile** as the provider and **Standard** as the vetting class. Click **Apply for Vetting**. Results typically arrive within 1-7 business days. **Timeline:** Standard vetting takes 1-7 business days. Enhanced vetting (for higher scores) may take longer. You can create campaigns before vetting completes, but throughput will be limited until a score is assigned. --- ## Step 3: Create a campaign A campaign defines your messaging use case and is required for each distinct type of messaging you do. ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/campaignBuilder \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "brandId": "your_brand_id", "usecase": "MIXED", "description": "Order confirmations and delivery updates", "sample1": "Your order #12345 has been confirmed. Track at https://acme.com/track/12345", "sample2": "Your package is out for delivery and will arrive by 5 PM today.", "messageFlow": "Customers opt in at checkout by checking a box to receive order updates via SMS.", "helpMessage": "Reply HELP for support. Contact us at support@acmecorp.com or call +15551234567.", "optinKeywords": "START, YES", "optoutKeywords": "STOP, UNSUBSCRIBE", "helpKeywords": "HELP, INFO", "embeddedLink": true, "numberPool": false, "ageGated": false }' ``` ```python Python campaign_data = { "brandId": "your_brand_id", "usecase": "MIXED", "description": "Order confirmations and delivery updates", "sample1": "Your order #12345 has been confirmed.", "sample2": "Your package is out for delivery.", "messageFlow": "Customers opt in at checkout.", "helpMessage": "Reply HELP for support.", "optinKeywords": "START, YES", "optoutKeywords": "STOP, UNSUBSCRIBE", "helpKeywords": "HELP, INFO", "embeddedLink": True, "numberPool": False, "ageGated": False, } response = requests.post( "https://api.telnyx.com/v2/10dlc/campaignBuilder", headers=headers, json=campaign_data, ) campaign = response.json() print(f"Campaign created: {campaign['data']['campaignId']}") ``` ```javascript Node const campaignData = { brandId: 'your_brand_id', usecase: 'MIXED', description: 'Order confirmations and delivery updates', sample1: 'Your order #12345 has been confirmed.', sample2: 'Your package is out for delivery.', messageFlow: 'Customers opt in at checkout.', helpMessage: 'Reply HELP for support.', optinKeywords: 'START, YES', optoutKeywords: 'STOP, UNSUBSCRIBE', helpKeywords: 'HELP, INFO', embeddedLink: true, numberPool: false, ageGated: false, }; const response = await axios.post( 'https://api.telnyx.com/v2/10dlc/campaignBuilder', campaignData, { headers } ); console.log(`Campaign created: ${response.data.data.campaignId}`); ``` **Common use case types:** | Use Case | Description | |----------|-------------| | `MIXED` | Multiple message types (most common) | | `MARKETING` | Promotional messages | | `CUSTOMER_CARE` | Support and service messages | | `DELIVERY_NOTIFICATION` | Order/delivery updates | | `ACCOUNT_NOTIFICATION` | Account alerts | | `2FA` | Two-factor authentication | | `SECURITY_ALERT` | Security notifications | | `POLLING_VOTING` | Surveys and polls | | `CHARITY` | Nonprofit messaging | | `POLITICAL` | Political campaigns | Go to [Campaigns](https://portal.telnyx.com/#/messaging-10dlc/campaigns) and click **Create New Campaign**. Choose the use case that best matches your messaging purpose and click **Next**. Review the carrier terms and your brand score. Click **Next**. Add industry vertical, sample messages, and campaign attributes. Accept the terms and conditions. **Sample messages matter.** Carriers review your sample messages during approval. Make them realistic and representative of your actual messaging. Include opt-out language (e.g., "Reply STOP to unsubscribe"). --- ## Step 4: Assign phone numbers Link your phone numbers to the campaign so they can send messages under that campaign's registration. ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/phoneNumberCampaign \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "phoneNumber": "+15551234567", "campaignId": "your_campaign_id" }' ``` ```python Python response = requests.post( "https://api.telnyx.com/v2/10dlc/phoneNumberCampaign", headers=headers, json={ "phoneNumber": "+15551234567", "campaignId": "your_campaign_id", }, ) print(f"Number assigned: {response.json()['data']['phoneNumber']}") ``` ```javascript Node const response = await axios.post( 'https://api.telnyx.com/v2/10dlc/phoneNumberCampaign', { phoneNumber: '+15551234567', campaignId: 'your_campaign_id', }, { headers } ); console.log(`Number assigned: ${response.data.data.phoneNumber}`); ``` Go to [Campaigns](https://portal.telnyx.com/#/messaging-10dlc/campaigns) and click on your campaign. Navigate to the **Assign Numbers** panel. Select the messaging profile, then enter the phone number(s) to assign. Phone numbers must already be assigned to a [messaging profile](/docs/messaging/messages/messaging-profiles-overview/index) before they can be assigned to a campaign. See the [Send Your First Message](/docs/messaging/messages/send-message/index) guide to set this up. --- ## Troubleshooting Common causes: - **EIN mismatch:** The EIN must match the legal business name exactly as registered with the IRS - **Invalid address:** Use the physical business address, not a P.O. box - **Missing website:** A working website is strongly recommended for higher vetting scores **Fix:** Correct the information and resubmit. Brand registration is free to retry. Vetting scores depend on: - Business age and size - Online presence and reputation - EIN verification - Industry vertical **Options:** - Request **Enhanced Vetting** for a more thorough review (may improve score) - Ensure your website is live, professional, and matches your brand information - Check that your EIN and business name match IRS records exactly See [10DLC Rate Limits](/docs/messaging/10dlc/10dlc-rate-limits/index) for how scores map to throughput. Carriers may reject campaigns for: - Vague or misleading sample messages - Missing opt-out language in samples - Use case doesn't match message content - Prohibited content (cannabis, gambling in some states, etc.) **Fix:** Review and update your sample messages, ensure opt-out language is included, and verify your use case is accurate. Even with 10DLC registration, messages can be filtered if: - Content doesn't match the registered campaign use case - Messages look like spam (identical content to many recipients) - Links are flagged by carrier content filters - Volume exceeds your campaign's throughput allocation **Fix:** Ensure message content matches your campaign description. Personalize messages. Use link shorteners carefully. Monitor [Message Detail Records](/docs/messaging/messages/message-detail-records/index) for delivery issues. --- ## Next steps Understand carrier-specific throughput based on your vetting score. Receive webhooks for brand vetting, campaign approval, and more. Special 10DLC registration for sole proprietors without an EIN. Start sending messages once your 10DLC setup is complete. --- ### Sole Proprietor > Source: https://developers.telnyx.com/docs/messaging/10dlc/sole-proprietor.md Sole Proprietor registration enables individuals and small businesses without a federal Tax ID (EIN) to register for 10DLC messaging. This guide covers the API workflow for creating Sole Proprietor brands, completing OTP verification, and managing campaigns programmatically. ## Overview Sole Proprietor brands have specific constraints compared to standard business brands: | Constraint | Limit | |------------|-------| | Campaigns per brand | 1 | | Phone numbers per campaign | 1 | | Mobile phone reuse | Max 3 SP brands per mobile number | | Throughput | Low-volume (varies by carrier) | Sole Proprietor registration requires identity verification via SMS OTP before campaigns can be created. ## Prerequisites - Telnyx account with API access - At least one US 10DLC phone number - Valid US/CA mobile phone number for OTP verification - Personal information: name, address, date of birth ## Registration Flow ```mermaid sequenceDiagram participant Client participant Telnyx API participant TCR participant Mobile Phone Client->>Telnyx API: POST /10dlc/brand (Sole Proprietor) Telnyx API->>TCR: Create SP brand TCR-->>Telnyx API: Brand created (PENDING) Telnyx API-->>Client: Brand ID returned Client->>Telnyx API: POST /10dlc/brand/{id}/smsOtp Telnyx API->>TCR: Trigger OTP TCR->>Mobile Phone: SMS with PIN TCR-->>Telnyx API: Reference ID Telnyx API-->>Client: OTP triggered Mobile Phone->>TCR: User receives PIN Client->>Telnyx API: PUT /10dlc/brand/{id}/smsOtp Telnyx API->>TCR: Verify PIN TCR-->>Telnyx API: Verified Telnyx API-->>Client: Brand VERIFIED Client->>Telnyx API: POST /10dlc/campaignBuilder Telnyx API-->>Client: Campaign created ``` ## Step 1: Create a Sole Proprietor Brand Create a brand with `entityType` set to `SOLE_PROPRIETOR`: Valid `vertical` values include: `PROFESSIONAL`, `REAL_ESTATE`, `HEALTHCARE`, `RETAIL`, `ENTERTAINMENT`, `EDUCATION`, `NONPROFIT`, `GOVERNMENT`, and others. See the [Brand API Reference](/api-reference/brands/create-brand) for the full list. ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/brand \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "entityType": "SOLE_PROPRIETOR", "firstName": "Jane", "lastName": "Smith", "displayName": "Jane Smith Consulting", "email": "jane@example.com", "phone": "+12025551234", "mobilePhone": "+12025559876", "street": "123 Main St", "city": "Austin", "state": "TX", "postalCode": "78701", "country": "US", "vertical": "PROFESSIONAL" }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const brand = await client.messaging10dlc.brand.create({ entityType: 'SOLE_PROPRIETOR', firstName: 'Jane', lastName: 'Smith', displayName: 'Jane Smith Consulting', email: 'jane@example.com', phone: '+12025551234', mobilePhone: '+12025559876', street: '123 Main St', city: 'Austin', state: 'TX', postalCode: '78701', country: 'US', vertical: 'PROFESSIONAL', }); console.log(brand.brandId); console.log(brand.identityStatus); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) brand = client.messaging_10dlc.brand.create( entity_type="SOLE_PROPRIETOR", first_name="Jane", last_name="Smith", display_name="Jane Smith Consulting", email="jane@example.com", phone="+12025551234", mobile_phone="+12025559876", street="123 Main St", city="Austin", state="TX", postal_code="78701", country="US", vertical="PROFESSIONAL", ) print(brand.brand_id) print(brand.identity_status) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) brand = client.messaging_10dlc.brand.create( entity_type: :SOLE_PROPRIETOR, first_name: "Jane", last_name: "Smith", display_name: "Jane Smith Consulting", email: "jane@example.com", phone: "+12025551234", mobile_phone: "+12025559876", street: "123 Main St", city: "Austin", state: "TX", postal_code: "78701", country: "US", vertical: :PROFESSIONAL ) puts(brand) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) brand, err := client.Messaging10dlc.Brand.New(context.TODO(), telnyx.Messaging10dlcBrandNewParams{ EntityType: telnyx.EntityTypeSoleProprietor, FirstName: "Jane", LastName: "Smith", DisplayName: "Jane Smith Consulting", Email: "jane@example.com", Phone: "+12025551234", MobilePhone: "+12025559876", Street: "123 Main St", City: "Austin", State: "TX", PostalCode: "78701", Country: "US", Vertical: telnyx.VerticalProfessional, }) if err != nil { panic(err.Error()) } fmt.Printf("Brand ID: %s\n", brand.BrandID) fmt.Printf("Identity Status: %s\n", brand.IdentityStatus) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messaging10dlc.brand.BrandCreateParams; import com.telnyx.sdk.models.messaging10dlc.brand.EntityType; import com.telnyx.sdk.models.messaging10dlc.brand.TelnyxBrand; import com.telnyx.sdk.models.messaging10dlc.brand.Vertical; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); BrandCreateParams params = BrandCreateParams.builder() .entityType(EntityType.SOLE_PROPRIETOR) .firstName("Jane") .lastName("Smith") .displayName("Jane Smith Consulting") .email("jane@example.com") .phone("+12025551234") .mobilePhone("+12025559876") .street("123 Main St") .city("Austin") .state("TX") .postalCode("78701") .country("US") .vertical(Vertical.PROFESSIONAL) .build(); TelnyxBrand brand = client.messaging10dlc().brand().create(params); System.out.println("Brand ID: " + brand.brandId()); System.out.println("Identity Status: " + brand.identityStatus()); } } ``` ### Response ```json { "brandId": "f5586561-8ff0-4291-a0ac-84fe544797bd", "entityType": "SOLE_PROPRIETOR", "displayName": "Jane Smith Consulting", "firstName": "Jane", "lastName": "Smith", "identityStatus": "PENDING", "mobilePhone": "+12025559876", "createdAt": "2026-02-03T12:00:00Z" } ``` The brand will remain in `PENDING` identity status until OTP verification is completed. You cannot create campaigns until the brand is `VERIFIED`. ## Step 2: Trigger OTP Verification Send an OTP PIN to the mobile phone associated with the brand: ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/brand/{brand_id}/smsOtp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "pinSms": "Your Telnyx verification code is @OTP_PIN@. This code expires in 24 hours.", "successSms": "Your Telnyx 10DLC brand has been verified successfully." }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messaging10dlc.brand.triggerSMSOtp( 'f5586561-8ff0-4291-a0ac-84fe544797bd', { pinSms: 'Your Telnyx verification code is @OTP_PIN@. This code expires in 24 hours.', successSms: 'Your Telnyx 10DLC brand has been verified successfully.', } ); console.log(response.referenceId); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messaging_10dlc.brand.trigger_sms_otp( brand_id="f5586561-8ff0-4291-a0ac-84fe544797bd", pin_sms="Your Telnyx verification code is @OTP_PIN@. This code expires in 24 hours.", success_sms="Your Telnyx 10DLC brand has been verified successfully.", ) print(response.reference_id) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messaging_10dlc.brand.trigger_sms_otp( "f5586561-8ff0-4291-a0ac-84fe544797bd", pin_sms: "Your Telnyx verification code is @OTP_PIN@. This code expires in 24 hours.", success_sms: "Your Telnyx 10DLC brand has been verified successfully." ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messaging10dlc.Brand.TriggerSMSOtp( context.TODO(), "f5586561-8ff0-4291-a0ac-84fe544797bd", telnyx.Messaging10dlcBrandTriggerSMSOtpParams{ PinSMS: "Your Telnyx verification code is @OTP_PIN@. This code expires in 24 hours.", SuccessSMS: "Your Telnyx 10DLC brand has been verified successfully.", }, ) if err != nil { panic(err.Error()) } fmt.Printf("Reference ID: %s\n", response.ReferenceID) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messaging10dlc.brand.BrandTriggerSmsOtpParams; import com.telnyx.sdk.models.messaging10dlc.brand.BrandTriggerSmsOtpResponse; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); BrandTriggerSmsOtpParams params = BrandTriggerSmsOtpParams.builder() .brandId("f5586561-8ff0-4291-a0ac-84fe544797bd") .pinSms("Your Telnyx verification code is @OTP_PIN@. This code expires in 24 hours.") .successSms("Your Telnyx 10DLC brand has been verified successfully.") .build(); BrandTriggerSmsOtpResponse response = client.messaging10dlc().brand().triggerSmsOtp(params); System.out.println("Reference ID: " + response.referenceId()); } } ``` ### Request Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `pinSms` | string | Yes | SMS message containing `@OTP_PIN@` placeholder (max 500 chars) | | `successSms` | string | Yes | Confirmation SMS sent after successful verification (max 500 chars) | ### Response ```json { "brandId": "f5586561-8ff0-4291-a0ac-84fe544797bd", "referenceId": "OTP8A3B2C" } ``` The `@OTP_PIN@` placeholder in `pinSms` will be replaced with the actual 6-digit PIN when the SMS is sent. ## Step 3: Check OTP Status Poll the OTP status to confirm delivery: ```bash curl curl -X GET https://api.telnyx.com/v2/10dlc/brand/{brand_id}/smsOtp \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const status = await client.messaging10dlc.brand.retrieveSMSOtpStatus( 'f5586561-8ff0-4291-a0ac-84fe544797bd' ); console.log(status.deliveryStatus); console.log(status.verifyDate); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) status = client.messaging_10dlc.brand.retrieve_sms_otp_status( "f5586561-8ff0-4291-a0ac-84fe544797bd", ) print(status.delivery_status) print(status.verify_date) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) status = client.messaging_10dlc.brand.retrieve_sms_otp_status( "f5586561-8ff0-4291-a0ac-84fe544797bd" ) puts(status) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) status, err := client.Messaging10dlc.Brand.RetrieveSMSOtpStatus( context.TODO(), "f5586561-8ff0-4291-a0ac-84fe544797bd", ) if err != nil { panic(err.Error()) } fmt.Printf("Delivery Status: %s\n", status.DeliveryStatus) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messaging10dlc.brand.BrandSmsOtpStatus; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); BrandSmsOtpStatus status = client.messaging10dlc() .brand() .retrieveSmsOtpStatus("f5586561-8ff0-4291-a0ac-84fe544797bd"); System.out.println("Delivery Status: " + status.deliveryStatus()); } } ``` ### Response ```json { "brandId": "f5586561-8ff0-4291-a0ac-84fe544797bd", "referenceId": "OTP8A3B2C", "mobilePhone": "+12025559876", "requestDate": "2026-02-03T12:05:00Z", "verifyDate": null, "deliveryStatus": "DELIVERED_HANDSET", "deliveryStatusDate": "2026-02-03T12:05:02Z", "deliveryStatusDetails": "Delivered to handset" } ``` ### Delivery Status Values | Status | Description | |--------|-------------| | `PENDING` | OTP request submitted, awaiting delivery | | `DELIVERED_HANDSET` | SMS delivered to the mobile device | | `DELIVERY_FAILED` | SMS delivery failed | | `VERIFIED` | OTP PIN successfully verified | | `EXPIRED` | OTP PIN expired (24-hour window) | ## Step 4: Verify OTP PIN After the user receives and provides the PIN, verify it: ```bash curl curl -X PUT https://api.telnyx.com/v2/10dlc/brand/{brand_id}/smsOtp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "otpPin": "123456" }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); await client.messaging10dlc.brand.verifySMSOtp( 'f5586561-8ff0-4291-a0ac-84fe544797bd', { otpPin: '123456' } ); console.log('Brand verified successfully'); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) client.messaging_10dlc.brand.verify_sms_otp( brand_id="f5586561-8ff0-4291-a0ac-84fe544797bd", otp_pin="123456", ) print("Brand verified successfully") ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) client.messaging_10dlc.brand.verify_sms_otp( "f5586561-8ff0-4291-a0ac-84fe544797bd", otp_pin: "123456" ) puts("Brand verified successfully") ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) err := client.Messaging10dlc.Brand.VerifySMSOtp( context.TODO(), "f5586561-8ff0-4291-a0ac-84fe544797bd", telnyx.Messaging10dlcBrandVerifySMSOtpParams{ OtpPin: "123456", }, ) if err != nil { panic(err.Error()) } fmt.Println("Brand verified successfully") } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messaging10dlc.brand.BrandVerifySmsOtpParams; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); BrandVerifySmsOtpParams params = BrandVerifySmsOtpParams.builder() .brandId("f5586561-8ff0-4291-a0ac-84fe544797bd") .otpPin("123456") .build(); client.messaging10dlc().brand().verifySmsOtp(params); System.out.println("Brand verified successfully"); } } ``` Upon successful verification: - The brand `identityStatus` changes to `VERIFIED` - The `successSms` message is sent to the mobile phone - The brand registration fee is charged - You can now create campaigns ## Step 5: Create a Sole Proprietor Campaign With a verified brand, create a campaign using the `SOLE_PROPRIETOR` usecase: Sample messages (`sample1`, `sample2`) are required for campaign approval. Include realistic examples of messages you'll send. ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/campaignBuilder \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "brandId": "f5586561-8ff0-4291-a0ac-84fe544797bd", "usecase": "SOLE_PROPRIETOR", "description": "Customer appointment reminders and booking confirmations for consulting services.", "messageFlow": "Customers opt-in via web form when booking appointments. They receive confirmation and reminder messages.", "helpMessage": "Reply HELP for assistance. Contact support@example.com", "optoutMessage": "Reply STOP to unsubscribe from messages.", "sample1": "Hi Jane, this is a reminder of your appointment tomorrow at 2pm.", "sample2": "Your booking for March 15th has been confirmed. Reply STOP to opt out.", "termsAndConditions": true }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const campaign = await client.messaging10dlc.campaignBuilder.submit({ brandId: 'f5586561-8ff0-4291-a0ac-84fe544797bd', usecase: 'SOLE_PROPRIETOR', description: 'Customer appointment reminders and booking confirmations for consulting services.', messageFlow: 'Customers opt-in via web form when booking appointments. They receive confirmation and reminder messages.', helpMessage: 'Reply HELP for assistance. Contact support@example.com', optoutMessage: 'Reply STOP to unsubscribe from messages.', sample1: 'Hi Jane, this is a reminder of your appointment tomorrow at 2pm.', sample2: 'Your booking for March 15th has been confirmed. Reply STOP to opt out.', termsAndConditions: true, }); console.log(campaign.campaignId); console.log(campaign.status); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) campaign = client.messaging_10dlc.campaign_builder.submit( brand_id="f5586561-8ff0-4291-a0ac-84fe544797bd", usecase="SOLE_PROPRIETOR", description="Customer appointment reminders and booking confirmations for consulting services.", message_flow="Customers opt-in via web form when booking appointments. They receive confirmation and reminder messages.", help_message="Reply HELP for assistance. Contact support@example.com", optout_message="Reply STOP to unsubscribe from messages.", sample1="Hi Jane, this is a reminder of your appointment tomorrow at 2pm.", sample2="Your booking for March 15th has been confirmed. Reply STOP to opt out.", terms_and_conditions=True, ) print(campaign.campaign_id) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) campaign = client.messaging_10dlc.campaign_builder.submit( brand_id: "f5586561-8ff0-4291-a0ac-84fe544797bd", usecase: "SOLE_PROPRIETOR", description: "Customer appointment reminders and booking confirmations for consulting services.", message_flow: "Customers opt-in via web form when booking appointments. They receive confirmation and reminder messages.", help_message: "Reply HELP for assistance. Contact support@example.com", optout_message: "Reply STOP to unsubscribe from messages.", sample1: "Hi Jane, this is a reminder of your appointment tomorrow at 2pm.", sample2: "Your booking for March 15th has been confirmed. Reply STOP to opt out.", terms_and_conditions: true ) puts(campaign) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) campaign, err := client.Messaging10dlc.CampaignBuilder.Submit(context.TODO(), telnyx.Messaging10dlcCampaignBuilderSubmitParams{ BrandID: "f5586561-8ff0-4291-a0ac-84fe544797bd", Usecase: "SOLE_PROPRIETOR", Description: "Customer appointment reminders and booking confirmations for consulting services.", MessageFlow: "Customers opt-in via web form when booking appointments. They receive confirmation and reminder messages.", HelpMessage: "Reply HELP for assistance. Contact support@example.com", OptoutMessage: "Reply STOP to unsubscribe from messages.", Sample1: "Hi Jane, this is a reminder of your appointment tomorrow at 2pm.", Sample2: "Your booking for March 15th has been confirmed. Reply STOP to opt out.", TermsAndConditions: true, }) if err != nil { panic(err.Error()) } fmt.Printf("Campaign ID: %s\n", campaign.CampaignID) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messaging10dlc.campaign.TelnyxCampaignCsp; import com.telnyx.sdk.models.messaging10dlc.campaignbuilder.CampaignBuilderSubmitParams; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); CampaignBuilderSubmitParams params = CampaignBuilderSubmitParams.builder() .brandId("f5586561-8ff0-4291-a0ac-84fe544797bd") .usecase("SOLE_PROPRIETOR") .description("Customer appointment reminders and booking confirmations for consulting services.") .messageFlow("Customers opt-in via web form when booking appointments. They receive confirmation and reminder messages.") .helpMessage("Reply HELP for assistance. Contact support@example.com") .optoutMessage("Reply STOP to unsubscribe from messages.") .sample1("Hi Jane, this is a reminder of your appointment tomorrow at 2pm.") .sample2("Your booking for March 15th has been confirmed. Reply STOP to opt out.") .termsAndConditions(true) .build(); TelnyxCampaignCsp campaign = client.messaging10dlc().campaignBuilder().submit(params); System.out.println("Campaign ID: " + campaign.campaignId()); } } ``` ### Response ```json { "campaignId": "c5e5e598-95b3-4076-bfe2-c7d2c58ec57f", "brandId": "f5586561-8ff0-4291-a0ac-84fe544797bd", "usecase": "SOLE_PROPRIETOR", "status": "ACTIVE", "description": "Customer appointment reminders and booking confirmations for consulting services.", "createdAt": "2026-02-03T12:30:00Z" } ``` Sole Proprietor campaigns are typically auto-approved and become `ACTIVE` immediately. Standard campaigns may require carrier review. ## Step 6: Assign a Phone Number Assign your 10DLC number to the campaign: ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/phone_number_campaigns \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "phoneNumber": "+12025551234", "campaignId": "campaign_id_here" }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const assignment = await client.messaging10dlc.phoneNumberCampaigns.create({ phoneNumber: '+12025551234', campaignId: 'campaign_id_here', }); console.log(assignment.campaignId); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) assignment = client.messaging_10dlc.phone_number_campaigns.create( phone_number="+12025551234", campaign_id="campaign_id_here", ) print(assignment.campaign_id) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) assignment = client.messaging_10dlc.phone_number_campaigns.create( phone_number: "+12025551234", campaign_id: "campaign_id_here" ) puts(assignment) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) assignment, err := client.Messaging10dlc.PhoneNumberCampaigns.New(context.TODO(), telnyx.Messaging10dlcPhoneNumberCampaignNewParams{ PhoneNumberCampaignCreate: telnyx.PhoneNumberCampaignCreateParam{ PhoneNumber: "+12025551234", CampaignID: "campaign_id_here", }, }) if err != nil { panic(err.Error()) } fmt.Printf("Assignment: %+v\n", assignment) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messaging10dlc.phonenumbercampaigns.PhoneNumberCampaign; import com.telnyx.sdk.models.messaging10dlc.phonenumbercampaigns.PhoneNumberCampaignCreate; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); PhoneNumberCampaignCreate params = PhoneNumberCampaignCreate.builder() .phoneNumber("+12025551234") .campaignId("campaign_id_here") .build(); PhoneNumberCampaign assignment = client.messaging10dlc().phoneNumberCampaigns().create(params); System.out.println("Campaign ID: " + assignment.campaignId()); } } ``` Sole Proprietor campaigns can only have **one phone number** assigned. Attempting to assign additional numbers will return an error. ## Error Handling ### Common Errors | Error | Cause | Solution | |-------|-------|----------| | `Sole Proprietor brands can only have one active campaign` | Attempting to create a second campaign | Delete or deactivate existing campaign first | | `Sole Proprietor campaigns can only have one phone number assigned` | Attempting to assign multiple numbers | Use only one number per SP campaign | | `Cannot associate campaign with brand in pending or failed status` | Brand not verified | Complete OTP verification first | | `OTP PIN expired` | 24-hour verification window exceeded | Trigger a new OTP with `POST /10dlc/brand/{id}/smsOtp` | ### Retrying OTP If the OTP expires or the user needs a new PIN, simply call the trigger endpoint again: ```bash curl -X POST https://api.telnyx.com/v2/10dlc/brand/{brand_id}/smsOtp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "pinSms": "Your new Telnyx verification code is @OTP_PIN@. This code expires in 24 hours.", "successSms": "Your Telnyx 10DLC brand has been verified successfully." }' ``` ## Fees | Item | Amount | Frequency | |------|--------|-----------| | Brand Registration | $4.00 | One-time (charged after verification) | | Campaign Vetting | $15.00 | Per submission | | Monthly Maintenance | $2.00 | Monthly | ## Webhooks Subscribe to brand and campaign status updates via webhooks. See [10DLC Event Notifications](/docs/messaging/10dlc/event-notifications/index) for details on: - `10dlc.brand.update` — Brand status and identity verification changes - `10dlc.campaign.update` — Campaign approval/rejection - `10dlc.phone_number.update` — Phone number assignment status ## Next Steps Understand throughput limits for Sole Proprietor campaigns Set up webhooks for status updates Full API documentation for brand management Portal walkthrough for non-API users --- ### Brand Registration > Source: https://developers.telnyx.com/docs/messaging/10dlc/brand-registration.md A **brand** is your registered business identity in the 10DLC ecosystem. Before you can create messaging campaigns or send A2P messages on US long codes, you must register your brand with [The Campaign Registry (TCR)](https://www.campaignregistry.com/) through the Telnyx API. Your brand's **vetting score** directly determines your messaging throughput limits. ## Prerequisites - A [Telnyx account](https://telnyx.com/sign-up) with API access - Your [API key](https://portal.telnyx.com/#/app/api-keys) - Business information: legal name, EIN/Tax ID, address, phone, email, website For **Sole Proprietor** registration (individuals without an EIN), see the [Sole Proprietor guide](/docs/messaging/10dlc/sole-proprietor). This guide covers standard business brand registration. --- ## Brand entity types TCR supports several entity types. Choose the one that matches your business structure: | Entity Type | API Value | Description | Vetting Required | |-------------|-----------|-------------|-----------------| | Private for-profit | `PRIVATE_PROFIT` | Private companies (LLC, Inc, etc.) | Yes | | Public for-profit | `PUBLIC_PROFIT` | Publicly traded companies | Yes | | Non-profit | `NON_PROFIT` | 501(c)(3) or equivalent | Yes | | Government | `GOVERNMENT` | Federal, state, or local government | Yes | | Sole Proprietor | `SOLE_PROPRIETOR` | Individuals without EIN | OTP only | Sole Proprietor brands have significant limitations: 1 campaign, 1 phone number, low throughput. Use standard registration if your business has an EIN. --- ## Registration flow ```mermaid sequenceDiagram participant You participant Telnyx API participant TCR participant Vetting Partner You->>Telnyx API: POST /v2/10dlc/brand Telnyx API->>TCR: Register brand TCR-->>Telnyx API: Brand created (brandId) Telnyx API-->>You: Brand response (status: PENDING) You->>Telnyx API: POST /v2/10dlc/brand/{brandId}/externalVetting Telnyx API->>Vetting Partner: Submit for vetting Note over Vetting Partner: 1-7 business days Vetting Partner-->>TCR: Vetting score (0-100) TCR-->>Telnyx API: Brand updated Telnyx API-->>You: Webhook: brand.vetted ``` --- ## Step 1: Create a brand Register your business identity by providing company details, contact information, and entity type. ### Required fields | Field | Type | Description | |-------|------|-------------| | `entityType` | string | Business entity type (see table above) | | `displayName` | string | Brand display name (shown to carriers) | | `companyName` | string | Legal company name (must match EIN records) | | `ein` | string | Federal Tax ID / EIN (format: `XX-XXXXXXX`) | | `phone` | string | Business phone in E.164 format | | `street` | string | Business street address | | `city` | string | City | | `state` | string | State (2-letter abbreviation) | | `postalCode` | string | ZIP code | | `country` | string | Country code (`US` or `CA`) | | `email` | string | Business contact email | | `website` | string | Business website URL | | `vertical` | string | Industry vertical (see [verticals list](#industry-verticals)) | ### Optional fields | Field | Type | Description | |-------|------|-------------| | `altBusinessId` | string | Alternative business ID (DUNS, GIIN, LEI) | | `altBusinessIdType` | string | Type of alternative ID: `DUNS`, `GIIN`, or `LEI` | | `stockSymbol` | string | Stock ticker (required for `PUBLIC_PROFIT`) | | `stockExchange` | string | Exchange: `NYSE`, `NASDAQ`, `AMEX`, etc. | ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/brand \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "entityType": "PRIVATE_PROFIT", "displayName": "Acme Corp", "companyName": "Acme Corporation", "ein": "12-3456789", "phone": "+15551234567", "street": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US", "email": "admin@acmecorp.com", "website": "https://acmecorp.com", "vertical": "TECHNOLOGY" }' ``` ```python Python import os import requests API_KEY = os.environ.get("TELNYX_API_KEY") headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } brand_data = { "entityType": "PRIVATE_PROFIT", "displayName": "Acme Corp", "companyName": "Acme Corporation", "ein": "12-3456789", "phone": "+15551234567", "street": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US", "email": "admin@acmecorp.com", "website": "https://acmecorp.com", "vertical": "TECHNOLOGY", } response = requests.post( "https://api.telnyx.com/v2/10dlc/brand", headers=headers, json=brand_data, ) brand = response.json() brand_id = brand["data"]["brandId"] print(f"Brand created: {brand_id}") print(f"Status: {brand['data']['identityStatus']}") ``` ```javascript Node const axios = require('axios'); const API_KEY = process.env.TELNYX_API_KEY; const headers = { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }; const brandData = { entityType: 'PRIVATE_PROFIT', displayName: 'Acme Corp', companyName: 'Acme Corporation', ein: '12-3456789', phone: '+15551234567', street: '123 Main St', city: 'New York', state: 'NY', postalCode: '10001', country: 'US', email: 'admin@acmecorp.com', website: 'https://acmecorp.com', vertical: 'TECHNOLOGY', }; const { data: brand } = await axios.post( 'https://api.telnyx.com/v2/10dlc/brand', brandData, { headers } ); console.log(`Brand created: ${brand.data.brandId}`); console.log(`Status: ${brand.data.identityStatus}`); ``` ```ruby Ruby require "net/http" require "json" require "uri" uri = URI("https://api.telnyx.com/v2/10dlc/brand") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" request["Content-Type"] = "application/json" request.body = { entityType: "PRIVATE_PROFIT", displayName: "Acme Corp", companyName: "Acme Corporation", ein: "12-3456789", phone: "+15551234567", street: "123 Main St", city: "New York", state: "NY", postalCode: "10001", country: "US", email: "admin@acmecorp.com", website: "https://acmecorp.com", vertical: "TECHNOLOGY" }.to_json response = http.request(request) brand = JSON.parse(response.body) puts "Brand created: #{brand['data']['brandId']}" puts "Status: #{brand['data']['identityStatus']}" ``` ```java Java import java.net.http.*; import java.net.URI; String apiKey = System.getenv("TELNYX_API_KEY"); String body = """ { "entityType": "PRIVATE_PROFIT", "displayName": "Acme Corp", "companyName": "Acme Corporation", "ein": "12-3456789", "phone": "+15551234567", "street": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US", "email": "admin@acmecorp.com", "website": "https://acmecorp.com", "vertical": "TECHNOLOGY" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.telnyx.com/v2/10dlc/brand")) .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpClient client = HttpClient.newHttpClient(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("Brand response: " + response.body()); ``` ```csharp .NET using System.Net.Http.Headers; using System.Text; using System.Text.Json; var apiKey = Environment.GetEnvironmentVariable("TELNYX_API_KEY"); var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var brandData = new { entityType = "PRIVATE_PROFIT", displayName = "Acme Corp", companyName = "Acme Corporation", ein = "12-3456789", phone = "+15551234567", street = "123 Main St", city = "New York", state = "NY", postalCode = "10001", country = "US", email = "admin@acmecorp.com", website = "https://acmecorp.com", vertical = "TECHNOLOGY" }; var content = new StringContent( JsonSerializer.Serialize(brandData), Encoding.UTF8, "application/json" ); var response = await client.PostAsync( "https://api.telnyx.com/v2/10dlc/brand", content ); var result = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Brand response: {result}"); ``` ```php PHP 'PRIVATE_PROFIT', 'displayName' => 'Acme Corp', 'companyName' => 'Acme Corporation', 'ein' => '12-3456789', 'phone' => '+15551234567', 'street' => '123 Main St', 'city' => 'New York', 'state' => 'NY', 'postalCode' => '10001', 'country' => 'US', 'email' => 'admin@acmecorp.com', 'website' => 'https://acmecorp.com', 'vertical' => 'TECHNOLOGY', ]; $ch = curl_init('https://api.telnyx.com/v2/10dlc/brand'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$apiKey}", 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($brandData), ]); $response = curl_exec($ch); curl_close($ch); $brand = json_decode($response, true); echo "Brand created: " . $brand['data']['brandId'] . "\n"; echo "Status: " . $brand['data']['identityStatus'] . "\n"; ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" ) func main() { brandData := map[string]string{ "entityType": "PRIVATE_PROFIT", "displayName": "Acme Corp", "companyName": "Acme Corporation", "ein": "12-3456789", "phone": "+15551234567", "street": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US", "email": "admin@acmecorp.com", "website": "https://acmecorp.com", "vertical": "TECHNOLOGY", } body, _ := json.Marshal(brandData) req, _ := http.NewRequest("POST", "https://api.telnyx.com/v2/10dlc/brand", bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Printf("Error: %v\n", err) return } defer resp.Body.Close() result, _ := io.ReadAll(resp.Body) fmt.Printf("Brand response: %s\n", result) } ``` ### Brand creation response ```json { "data": { "brandId": "BXXXXXX", "entityType": "PRIVATE_PROFIT", "displayName": "Acme Corp", "companyName": "Acme Corporation", "ein": "12-3456789", "identityStatus": "VERIFIED", "cspId": "TELNYX", "brandRelationship": "BASIC_ACCOUNT", "vertical": "TECHNOLOGY", "phone": "+15551234567", "email": "admin@acmecorp.com", "website": "https://acmecorp.com", "country": "US", "state": "NY", "city": "New York", "street": "123 Main St", "postalCode": "10001" } } ``` --- ## Step 2: Retrieve brand details After creation, check your brand status and details: ```bash curl curl -X GET https://api.telnyx.com/v2/10dlc/brand/BXXXXXX \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python response = requests.get( f"https://api.telnyx.com/v2/10dlc/brand/{brand_id}", headers=headers, ) brand_details = response.json() print(f"Identity status: {brand_details['data']['identityStatus']}") ``` ```javascript Node const { data: details } = await axios.get( `https://api.telnyx.com/v2/10dlc/brand/${brandId}`, { headers } ); console.log(`Identity status: ${details.data.identityStatus}`); ``` ```ruby Ruby uri = URI("https://api.telnyx.com/v2/10dlc/brand/#{brand_id}") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" response = http.request(request) details = JSON.parse(response.body) puts "Identity status: #{details['data']['identityStatus']}" ``` ### Brand statuses | Status | Meaning | |--------|---------| | `SELF_DECLARED` | Brand created, not yet verified | | `VERIFIED` | Identity verified by TCR | | `VETTED_VERIFIED` | Third-party vetting completed | | `UNVERIFIED` | Verification failed — see [troubleshooting](#common-rejection-reasons-and-fixes) | --- ## Step 3: Submit for external vetting Vetting is performed by a third-party partner (e.g., Campaign Verify) and produces a **trust score from 0 to 100** that directly impacts your messaging throughput. ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/brand/BXXXXXX/externalVetting \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "evpId": "AEGIS", "vettingClass": "STANDARD" }' ``` ```python Python vetting_response = requests.post( f"https://api.telnyx.com/v2/10dlc/brand/{brand_id}/externalVetting", headers=headers, json={ "evpId": "AEGIS", "vettingClass": "STANDARD", }, ) print(f"Vetting submitted: {vetting_response.json()}") ``` ```javascript Node const { data: vetting } = await axios.post( `https://api.telnyx.com/v2/10dlc/brand/${brandId}/externalVetting`, { evpId: 'AEGIS', vettingClass: 'STANDARD' }, { headers } ); console.log('Vetting submitted:', vetting); ``` ```ruby Ruby uri = URI("https://api.telnyx.com/v2/10dlc/brand/#{brand_id}/externalVetting") request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" request["Content-Type"] = "application/json" request.body = { evpId: "AEGIS", vettingClass: "STANDARD" }.to_json response = http.request(request) puts "Vetting submitted: #{response.body}" ``` **Idempotency:** Submitting the same `evpId` + `vettingClass` for a brand that already has a successful vetting (within the last 180 days) or one currently being processed returns `400` with error code `10012` ("Duplicate resource"). Failed vettings are excluded from this check, so you can retry immediately after a real failure. To retrieve existing vettings, use `GET /v2/10dlc/brand/{brandId}/externalVetting`. ### Vetting classes | Class | Cost | Use Case | |-------|------|----------| | `STANDARD` | ~$4 | Default for most brands | | `ENHANCED` | ~$40 | Higher trust scores, faster approval | ### Vetting score impact on throughput Your vetting score directly controls your carrier-specific throughput. See the [10DLC Rate Limits guide](/docs/messaging/10dlc/10dlc-rate-limits) for detailed carrier tables. | Score Range | T-Mobile Throughput | AT&T Throughput | Category | |-------------|--------------------|--------------------|----------| | 0–24 | 2,000/day | 1 MPS | Low | | 25–49 | 10,000/day | 4 MPS | Medium-Low | | 50–74 | 50,000/day | 10 MPS | Medium | | 75–89 | 100,000/day | 25 MPS | High | | 90–100 | 200,000+/day | 75 MPS | Highest | Without vetting, brands default to the **lowest throughput tier**. Always submit for vetting before creating campaigns. --- ## Step 4: Check vetting results Vetting typically takes **1–7 business days**. You can poll or use webhooks to check the result. ```bash curl # List external vettings for a brand curl -X GET https://api.telnyx.com/v2/10dlc/brand/BXXXXXX/externalVetting \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python response = requests.get( f"https://api.telnyx.com/v2/10dlc/brand/{brand_id}/externalVetting", headers=headers, ) vettings = response.json() for v in vettings.get("data", []): print(f"Score: {v.get('vettingScore')} | Status: {v.get('vettingStatus')}") ``` ```javascript Node const { data: vettings } = await axios.get( `https://api.telnyx.com/v2/10dlc/brand/${brandId}/externalVetting`, { headers } ); vettings.data.forEach(v => { console.log(`Score: ${v.vettingScore} | Status: ${v.vettingStatus}`); }); ``` --- ## Step 5: Update a brand If your brand details change (address, phone, etc.), update them via the API: ```bash curl curl -X PUT https://api.telnyx.com/v2/10dlc/brand/BXXXXXX \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "phone": "+15559876543", "email": "newemail@acmecorp.com", "website": "https://www.acmecorp.com" }' ``` ```python Python response = requests.put( f"https://api.telnyx.com/v2/10dlc/brand/{brand_id}", headers=headers, json={ "phone": "+15559876543", "email": "newemail@acmecorp.com", "website": "https://www.acmecorp.com", }, ) print(f"Brand updated: {response.json()['data']['brandId']}") ``` ```javascript Node const { data: updated } = await axios.put( `https://api.telnyx.com/v2/10dlc/brand/${brandId}`, { phone: '+15559876543', email: 'newemail@acmecorp.com', website: 'https://www.acmecorp.com', }, { headers } ); console.log(`Brand updated: ${updated.data.brandId}`); ``` Updating certain fields (company name, EIN) may trigger re-verification. Contact support if you need to change core identity fields. --- ## List all brands Retrieve all registered brands on your account: ```bash curl curl -X GET "https://api.telnyx.com/v2/10dlc/brand?page[size]=25" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python response = requests.get( "https://api.telnyx.com/v2/10dlc/brand", headers=headers, params={"page[size]": 25}, ) brands = response.json() for b in brands.get("data", []): print(f"{b['brandId']}: {b['displayName']} ({b['identityStatus']})") ``` ```javascript Node const { data: brands } = await axios.get( 'https://api.telnyx.com/v2/10dlc/brand', { headers, params: { 'page[size]': 25 } } ); brands.data.forEach(b => { console.log(`${b.brandId}: ${b.displayName} (${b.identityStatus})`); }); ``` --- ## Common rejection reasons and fixes **Cause:** The `companyName` field doesn't match what the IRS has on file for that EIN. **Fix:** Use your exact legal name as registered with the IRS. Check your EIN confirmation letter (CP 575) or search the [IRS tax-exempt organization database](https://www.irs.gov/charities-non-profits/tax-exempt-organization-search) for non-profits. **Cause:** EIN must be in `XX-XXXXXXX` format (9 digits with a hyphen after the first 2). **Fix:** Double-check your EIN. It should look like `12-3456789`. Don't include spaces or extra characters. **Cause:** The vetting partner couldn't access your website, or the website content doesn't match the registered business. **Fix:** Ensure your website is live, publicly accessible (no authentication required), and has content that clearly identifies your business. The domain should ideally match your business email domain. **Cause:** The business phone number provided is not a valid, reachable US/CA number. **Fix:** Provide a working business phone number in E.164 format (e.g., `+15551234567`). The number should be answerable during business hours. **Cause:** The provided address doesn't match USPS records or can't be verified. **Fix:** Use your exact business address as registered. Verify it through [USPS address lookup](https://tools.usps.com/zip-code-lookup.htm). **Cause:** A brand with this EIN is already registered (possibly by another CSP or a previous registration). **Fix:** You can only have one brand per EIN. If you previously registered through another provider, you may need to import or transfer the brand. Contact [Telnyx support](https://support.telnyx.com/) for assistance. --- ## Appeal process If your brand is rejected or receives a low vetting score, you can appeal: Check the brand's `identityStatus` and any associated error messages via the API or [Mission Control Portal](https://portal.telnyx.com/#/app/messaging/10dlc/brands). Update your brand information to address the specific rejection reason using the [update brand API](#step-5-update-a-brand). After updating your brand, submit a new external vetting request. Each vetting submission incurs a fee. For complex cases (EIN transfers, duplicate brands, identity disputes), contact [Telnyx support](https://support.telnyx.com/) with your `brandId` and details. Re-vetting after fixing issues often results in a higher score. The most common improvement is ensuring your website, company name, and EIN all align. --- ## Industry verticals When creating a brand, specify your industry using one of these vertical values: | Vertical | API Value | |----------|-----------| | Professional Services | `PROFESSIONAL` | | Real Estate | `REAL_ESTATE` | | Healthcare | `HEALTHCARE` | | Human Resources | `HUMAN_RESOURCES` | | Energy | `ENERGY` | | Entertainment | `ENTERTAINMENT` | | Retail | `RETAIL` | | Transportation | `TRANSPORTATION` | | Agriculture | `AGRICULTURE` | | Insurance | `INSURANCE` | | Postal | `POSTAL` | | Education | `EDUCATION` | | Hospitality | `HOSPITALITY` | | Financial | `FINANCIAL` | | Political | `POLITICAL` | | Gambling | `GAMBLING` | | Legal | `LEGAL` | | Construction | `CONSTRUCTION` | | NGO | `NGO` | | Manufacturing | `MANUFACTURING` | | Government | `GOVERNMENT` | | Technology | `TECHNOLOGY` | | Communication | `COMMUNICATION` | --- ## Webhook notifications Telnyx sends webhooks for brand lifecycle events. Configure your webhook URL on your [messaging profile](/docs/messaging/messages/messaging-profiles-overview) or via the API. | Event | Description | |-------|-------------| | `brand.created` | Brand successfully registered with TCR | | `brand.updated` | Brand details updated | | `brand.vetted` | External vetting completed (includes score) | | `brand.deleted` | Brand deleted | See [10DLC Event Notifications](/docs/messaging/10dlc/event-notifications) for payload examples and webhook handling. --- ## Next steps Register your messaging use case after your brand is vetted. Understand how your vetting score impacts throughput. Registration guide for individuals without an EIN. Track brand and campaign lifecycle via webhooks. --- ### Campaign Registration > Source: https://developers.telnyx.com/docs/messaging/10dlc/campaign-registration.md A 10DLC campaign defines your messaging use case — what you're sending, who you're sending to, and how recipients opted in. Every campaign must be registered with The Campaign Registry (TCR) and approved by mobile carriers before you can send messages at scale. This guide walks you through creating campaigns via API and Portal, choosing the right use case, writing sample messages that pass carrier review, and handling rejections. ```mermaid flowchart LR A[Create Campaign] --> B[TCR Review] B -->|approved| C[MNO Provisioning] B -->|rejected| D[Fix & Resubmit] D --> A C --> E[T-Mobile: instant-24h] C --> F[AT&T: 1-3 days] C --> G[Verizon: 1-3 days] E --> H[ACTIVE] F --> H G --> H H -->|expired| I[EXPIRED] H -->|violation| J[SUSPENDED] ``` **Prerequisites:** You need an approved [brand](/docs/messaging/10dlc/quickstart/index#step-1-create-a-brand) before creating campaigns. Your brand's vetting score affects campaign throughput — see [10DLC Rate Limits](/docs/messaging/10dlc/10dlc-rate-limits/index). ## Campaign use case types Choose the use case that best describes your messaging. Carriers review this against your sample messages, so accuracy matters. ### Standard use cases | Use Case | Description | Example | |----------|-------------|---------| | `CUSTOMER_CARE` | Support and service messages | "Your ticket #4521 has been updated. View details at..." | | `DELIVERY_NOTIFICATION` | Order and shipping updates | "Your package shipped! Tracking: 1Z999..." | | `ACCOUNT_NOTIFICATION` | Account alerts and changes | "Your password was changed. If this wasn't you, call..." | | `MARKETING` | Promotional content | "Summer sale! 30% off all items this weekend..." | | `2FA` | Two-factor authentication codes | "Your verification code is 847291. Expires in 10 min." | | `SECURITY_ALERT` | Security-related notifications | "New login detected from Chrome on Windows..." | | `POLLING_VOTING` | Surveys and polls | "How was your experience? Reply 1-5." | | `CHARITY` | Nonprofit fundraising and awareness | "Thanks for supporting Habitat! Text DONATE for..." | | `POLITICAL` | Political campaigns and advocacy | "Reminder: Vote on Nov 5th. Find your polling place..." | | `MIXED` | Multiple message types (most common) | Combination of the above | ### Special use cases | Use Case | Description | Requirements | |----------|-------------|--------------| | `LOW_VOLUME` | Under 6,000 messages/month | Simplified registration | | `SOLE_PROPRIETOR` | Individual/small business without EIN | See [Sole Proprietor](/docs/messaging/10dlc/sole-proprietor/index) guide | | `EMERGENCY` | Life-threatening alerts | Must demonstrate emergency nature | | `AGENTS_FRANCHISES` | ISVs sending on behalf of clients | Additional compliance requirements | | `SWEEPSTAKES` | Contests and giveaways | Must include rules and terms | **Choose carefully.** Changing a campaign's use case after registration requires creating a new campaign. Carriers reject campaigns where sample messages don't match the declared use case. --- ## Create a campaign ### Required fields | Field | Type | Description | |-------|------|-------------| | `brandId` | string | Your registered brand ID | | `usecase` | string | Use case type (see table above) | | `description` | string | What your campaign does (2-4 sentences) | | `sample1` | string | First sample message | | `sample2` | string | Second sample message | | `messageFlow` | string | How users opt in to receive messages | | `helpMessage` | string | Response to HELP keyword | | `optinKeywords` | string | Comma-separated opt-in keywords | | `optoutKeywords` | string | Comma-separated opt-out keywords | | `helpKeywords` | string | Comma-separated help keywords | ### Optional fields | Field | Type | Default | Description | |-------|------|---------|-------------| | `embeddedLink` | boolean | `false` | Messages contain URLs | | `embeddedPhone` | boolean | `false` | Messages contain phone numbers | | `numberPool` | boolean | `false` | Using number pool for sending | | `ageGated` | boolean | `false` | Content requires age verification | | `directLending` | boolean | `false` | Related to direct lending | | `subscriberOptin` | boolean | `true` | Subscribers have opted in | | `subscriberOptout` | boolean | `true` | Subscribers can opt out | | `subscriberHelp` | boolean | `true` | Help keyword is supported | | `sample3` | string | — | Third sample message (recommended) | | `sample4` | string | — | Fourth sample message | | `sample5` | string | — | Fifth sample message | ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/campaignBuilder \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "brandId": "BRAND_ID", "usecase": "DELIVERY_NOTIFICATION", "description": "Order confirmation and delivery status updates for e-commerce customers who purchase through our website.", "sample1": "Hi {{name}}, your order #{{orderId}} has been confirmed! Estimated delivery: {{date}}. Track at https://acme.com/track/{{orderId}} Reply STOP to opt out.", "sample2": "Great news! Your package is out for delivery and should arrive by 5 PM today. Driver: {{driverName}}. Reply STOP to unsubscribe.", "sample3": "Your order #{{orderId}} has been delivered to your front door. Rate your experience: https://acme.com/review/{{orderId}}", "messageFlow": "Customers opt in at checkout by checking a consent box that reads: I agree to receive order updates via SMS. Frequency varies. Msg & data rates may apply.", "helpMessage": "Acme Corp order updates. For help, visit https://acme.com/support or call +15551234567. Reply STOP to cancel.", "optinKeywords": "START, YES, SUBSCRIBE", "optoutKeywords": "STOP, UNSUBSCRIBE, CANCEL, QUIT", "helpKeywords": "HELP, INFO", "embeddedLink": true, "numberPool": false, "ageGated": false }' ``` ```python Python import os import requests API_KEY = os.environ.get("TELNYX_API_KEY") headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } campaign_data = { "brandId": "BRAND_ID", "usecase": "DELIVERY_NOTIFICATION", "description": ( "Order confirmation and delivery status updates for " "e-commerce customers who purchase through our website." ), "sample1": ( "Hi {{name}}, your order #{{orderId}} has been confirmed! " "Estimated delivery: {{date}}. Track at " "https://acme.com/track/{{orderId}} Reply STOP to opt out." ), "sample2": ( "Great news! Your package is out for delivery and should " "arrive by 5 PM today. Reply STOP to unsubscribe." ), "sample3": ( "Your order #{{orderId}} has been delivered to your front " "door. Rate your experience: https://acme.com/review/{{orderId}}" ), "messageFlow": ( "Customers opt in at checkout by checking a consent box." ), "helpMessage": ( "Acme Corp order updates. For help, visit " "https://acme.com/support or call +15551234567. " "Reply STOP to cancel." ), "optinKeywords": "START, YES, SUBSCRIBE", "optoutKeywords": "STOP, UNSUBSCRIBE, CANCEL, QUIT", "helpKeywords": "HELP, INFO", "embeddedLink": True, "numberPool": False, "ageGated": False, } response = requests.post( "https://api.telnyx.com/v2/10dlc/campaignBuilder", headers=headers, json=campaign_data, ) campaign = response.json() print(f"Campaign ID: {campaign['data']['campaignId']}") print(f"Status: {campaign['data']['status']}") ``` ```javascript Node const axios = require('axios'); const headers = { Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, 'Content-Type': 'application/json', }; const campaignData = { brandId: 'BRAND_ID', usecase: 'DELIVERY_NOTIFICATION', description: 'Order confirmation and delivery status updates for e-commerce customers.', sample1: 'Hi {{name}}, your order #{{orderId}} has been confirmed! Track at https://acme.com/track/{{orderId}} Reply STOP to opt out.', sample2: 'Great news! Your package is out for delivery and should arrive by 5 PM today. Reply STOP to unsubscribe.', sample3: 'Your order #{{orderId}} has been delivered. Rate your experience: https://acme.com/review/{{orderId}}', messageFlow: 'Customers opt in at checkout by checking a consent box.', helpMessage: 'Acme Corp order updates. For help, visit https://acme.com/support. Reply STOP to cancel.', optinKeywords: 'START, YES, SUBSCRIBE', optoutKeywords: 'STOP, UNSUBSCRIBE, CANCEL, QUIT', helpKeywords: 'HELP, INFO', embeddedLink: true, numberPool: false, ageGated: false, }; const response = await axios.post( 'https://api.telnyx.com/v2/10dlc/campaignBuilder', campaignData, { headers } ); console.log(`Campaign ID: ${response.data.data.campaignId}`); console.log(`Status: ${response.data.data.status}`); ``` ```ruby Ruby require "net/http" require "json" require "uri" uri = URI("https://api.telnyx.com/v2/10dlc/campaignBuilder") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" request["Content-Type"] = "application/json" request.body = { brandId: "BRAND_ID", usecase: "DELIVERY_NOTIFICATION", description: "Order confirmation and delivery status updates.", sample1: "Hi {{name}}, your order #{{orderId}} confirmed! Reply STOP to opt out.", sample2: "Your package is out for delivery. Reply STOP to unsubscribe.", messageFlow: "Customers opt in at checkout.", helpMessage: "For help visit https://acme.com/support. Reply STOP to cancel.", optinKeywords: "START, YES, SUBSCRIBE", optoutKeywords: "STOP, UNSUBSCRIBE, CANCEL, QUIT", helpKeywords: "HELP, INFO", embeddedLink: true, numberPool: false, ageGated: false }.to_json response = http.request(request) campaign = JSON.parse(response.body) puts "Campaign ID: #{campaign['data']['campaignId']}" ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) func main() { campaignData := map[string]interface{}{ "brandId": "BRAND_ID", "usecase": "DELIVERY_NOTIFICATION", "description": "Order confirmation and delivery status updates.", "sample1": "Hi {{name}}, your order #{{orderId}} confirmed! Reply STOP to opt out.", "sample2": "Your package is out for delivery. Reply STOP to unsubscribe.", "messageFlow": "Customers opt in at checkout.", "helpMessage": "For help visit https://acme.com/support. Reply STOP to cancel.", "optinKeywords": "START, YES, SUBSCRIBE", "optoutKeywords": "STOP, UNSUBSCRIBE, CANCEL, QUIT", "helpKeywords": "HELP, INFO", "embeddedLink": true, "numberPool": false, "ageGated": false, } body, _ := json.Marshal(campaignData) req, _ := http.NewRequest("POST", "https://api.telnyx.com/v2/10dlc/campaignBuilder", bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) data := result["data"].(map[string]interface{}) fmt.Printf("Campaign ID: %s\n", data["campaignId"]) } ``` ```php PHP 'BRAND_ID', 'usecase' => 'DELIVERY_NOTIFICATION', 'description' => 'Order confirmation and delivery status updates.', 'sample1' => 'Hi {{name}}, your order #{{orderId}} confirmed! Reply STOP to opt out.', 'sample2' => 'Your package is out for delivery. Reply STOP to unsubscribe.', 'messageFlow' => 'Customers opt in at checkout.', 'helpMessage' => 'For help visit https://acme.com/support. Reply STOP to cancel.', 'optinKeywords' => 'START, YES, SUBSCRIBE', 'optoutKeywords' => 'STOP, UNSUBSCRIBE, CANCEL, QUIT', 'helpKeywords' => 'HELP, INFO', 'embeddedLink' => true, 'numberPool' => false, 'ageGated' => false, ]; $ch = curl_init('https://api.telnyx.com/v2/10dlc/campaignBuilder'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$apiKey}", 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($campaignData), ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); echo "Campaign ID: {$response['data']['campaignId']}\n"; echo "Status: {$response['data']['status']}\n"; ``` ```csharp .NET using System.Net.Http.Headers; using System.Text; using System.Text.Json; var apiKey = Environment.GetEnvironmentVariable("TELNYX_API_KEY"); var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var campaignData = new { brandId = "BRAND_ID", usecase = "DELIVERY_NOTIFICATION", description = "Order confirmation and delivery status updates.", sample1 = "Hi {{name}}, your order #{{orderId}} confirmed! Reply STOP to opt out.", sample2 = "Your package is out for delivery. Reply STOP to unsubscribe.", messageFlow = "Customers opt in at checkout.", helpMessage = "For help visit https://acme.com/support. Reply STOP to cancel.", optinKeywords = "START, YES, SUBSCRIBE", optoutKeywords = "STOP, UNSUBSCRIBE, CANCEL, QUIT", helpKeywords = "HELP, INFO", embeddedLink = true, numberPool = false, ageGated = false, }; var json = JsonSerializer.Serialize(campaignData); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await client.PostAsync( "https://api.telnyx.com/v2/10dlc/campaignBuilder", content); var result = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(result); var data = doc.RootElement.GetProperty("data"); Console.WriteLine($"Campaign ID: {data.GetProperty("campaignId")}"); ``` Go to [Campaigns](https://portal.telnyx.com/#/messaging-10dlc/campaigns) in Mission Control and click **Create New Campaign**. Choose the brand this campaign belongs to. If you haven't created one yet, see the [Quickstart](/docs/messaging/10dlc/quickstart/index#step-1-create-a-brand). Select the use case that best describes your messaging purpose. See the [use case table](#campaign-use-case-types) above for guidance. Review the carrier terms and your brand's vetting score. Click **Next** to continue. Fill in your campaign description, sample messages, message flow (opt-in process), and keyword responses (HELP, STOP). Set campaign attributes like embedded links and age gating. Accept the terms and conditions and click **Submit**. Your campaign enters carrier review. --- ## Writing sample messages that pass review Carriers manually review your sample messages. Poorly written samples are the #1 reason campaigns get rejected. Every sample should include opt-out instructions: > "Your order #12345 has shipped! Track at https://acme.com/track/12345. Reply STOP to unsubscribe." Use real-looking content with your actual brand name: > "Hi Sarah, your Acme Corp appointment is confirmed for Tuesday at 2 PM. Reply YES to confirm or HELP for assistance." If your use case is `DELIVERY_NOTIFICATION`, all samples should be about deliveries: > ✅ "Your package has shipped via FedEx. Tracking: 1Z999AA10123456784" > > ❌ "Check out our summer sale! 30% off everything!" (This is marketing, not delivery) Carriers reject vague samples: > ❌ "This is a test message" > > ❌ "Hello, this is a message from our company" Carriers prohibit or restrict: - Cannabis / CBD messaging - Gambling content (varies by state) - Firearms sales - Payday lending - Content targeting minors without age gate The `messageFlow` field should explain exactly how users consent: > "Users sign up on our website at https://acme.com/signup where they enter their phone number and check a box that reads: 'I agree to receive order updates via SMS from Acme Corp. Msg frequency varies. Msg & data rates may apply. Reply STOP to cancel.'" --- ## MNO provisioning timeline After TCR approves your campaign, each carrier (MNO) provisions it on their network independently. This affects when you can send messages on each carrier. | Carrier | Typical Timeline | Notes | |---------|-----------------|-------| | **T-Mobile** | Instant to 24 hours | Usually the fastest | | **AT&T** | 1-3 business days | May require additional review for some use cases | | **Verizon** | 1-3 business days | — | | **US Cellular** | 3-5 business days | Smaller carrier, longer provisioning | You can check provisioning status per carrier via the API: ```bash curl -s https://api.telnyx.com/v2/10dlc/campaignBuilder/{campaignId} \ -H "Authorization: Bearer YOUR_API_KEY" | jq '.data.mnoMetadata' ``` --- ## Check campaign status ```bash curl # Get campaign details curl -s https://api.telnyx.com/v2/10dlc/campaignBuilder/{campaignId} \ -H "Authorization: Bearer YOUR_API_KEY" | jq '{ status: .data.status, usecase: .data.usecase, brandId: .data.brandId, createDate: .data.createDate }' ``` ```python Python response = requests.get( f"https://api.telnyx.com/v2/10dlc/campaignBuilder/{campaign_id}", headers=headers, ) campaign = response.json()["data"] print(f"Status: {campaign['status']}") print(f"Use case: {campaign['usecase']}") ``` ```javascript Node const response = await axios.get( `https://api.telnyx.com/v2/10dlc/campaignBuilder/${campaignId}`, { headers } ); console.log(`Status: ${response.data.data.status}`); console.log(`Use case: ${response.data.data.usecase}`); ``` ### Campaign statuses | Status | Meaning | |--------|---------| | `ACTIVE` | Approved and ready to send | | `EXPIRED` | Campaign expired (renew required) | | `SUSPENDED` | Suspended by carrier — contact support | --- ## List all campaigns ```bash curl curl -s https://api.telnyx.com/v2/10dlc/campaignBuilder \ -H "Authorization: Bearer YOUR_API_KEY" \ -G -d "page[size]=20" ``` ```python Python response = requests.get( "https://api.telnyx.com/v2/10dlc/campaignBuilder", headers=headers, params={"page[size]": 20}, ) campaigns = response.json()["data"] for c in campaigns: print(f"{c['campaignId']} | {c['usecase']} | {c['status']}") ``` ```javascript Node const response = await axios.get( 'https://api.telnyx.com/v2/10dlc/campaignBuilder', { headers, params: { 'page[size]': 20 }, } ); response.data.data.forEach((c) => { console.log(`${c.campaignId} | ${c.usecase} | ${c.status}`); }); ``` --- ## Handle campaign rejections Campaigns can be rejected during carrier review. Common reasons and how to fix them: | Rejection Reason | Fix | |-----------------|-----| | **Samples don't match use case** | Rewrite samples to match your declared use case exactly | | **Missing opt-out language** | Add "Reply STOP to unsubscribe" to every sample | | **Vague message flow** | Describe the exact opt-in mechanism (website form, checkout checkbox, etc.) | | **Prohibited content** | Remove restricted content (cannabis, gambling, etc.) | | **Brand not vetted** | Complete [brand vetting](/docs/messaging/10dlc/quickstart/index#step-2-vet-your-brand) before resubmitting | ### Resubmitting a rejected campaign You cannot edit a rejected campaign. Instead, create a new campaign with corrected information: 1. Review the rejection reason (check [Event Notifications](/docs/messaging/10dlc/event-notifications/index) webhooks) 2. Fix the identified issues in your samples and description 3. Create a new campaign via the API or Portal 4. Reassign your phone numbers to the new campaign Each campaign submission incurs a TCR registration fee. Review your samples carefully before submitting to avoid repeated rejections and fees. --- ## Campaign compliance best practices Only send messages that match your registered campaign use case. Sending marketing from a `CUSTOMER_CARE` campaign risks suspension. Process STOP requests within seconds. Carriers monitor compliance. See [Advanced Opt-In/Out](/docs/messaging/messages/advanced-opt-in-out/index) for implementation. Maintain proof of consent (opt-in records with timestamp, source, and phone number). Carriers may request this during audits. Don't exceed your campaign's allocated throughput. Check [10DLC Rate Limits](/docs/messaging/10dlc/10dlc-rate-limits/index) for your brand score tier. Set up [webhooks for 10DLC events](/docs/messaging/10dlc/event-notifications/index) to catch approval, rejection, and suspension events in real time. --- ## Competitor comparison | Feature | Telnyx | Twilio | Vonage | |---------|--------|--------|--------| | Campaign creation | API + Portal | API + Console | API + Dashboard | | Use case types | 15+ standard types | Similar TCR types | Similar TCR types | | Sample messages | 2 required, up to 5 | 2 required | 2 required | | MNO status visibility | Per-carrier status via API | Per-carrier status | Limited visibility | | Rejection handling | Create new campaign | Create new campaign | Create new campaign | --- ## Next steps Link phone numbers to your campaign to start sending. Understand throughput based on your brand's vetting score. Receive real-time webhooks for campaign status changes. Start sending once your campaign is approved. --- ### Use Case Examples > Source: https://developers.telnyx.com/docs/messaging/10dlc/campaign-use-cases.md Writing good sample messages is the #1 factor in getting your 10DLC campaign approved. Carriers reject campaigns when sample messages don't match the declared use case, lack opt-out language, or look like spam. This guide provides ready-to-use sample messages, compliant opt-in language, and content patterns for every campaign use case type. **How carriers review sample messages:** - Must match the declared use case type - Must include opt-out language (STOP to opt out) - Must represent actual messages you'll send - Vague or generic samples get rejected --- ## Standard use cases ### 2FA / Two-Factor Authentication The simplest use case — short, transactional, no marketing content. **Sample 1:** ``` Your Acme verification code is 847291. This code expires in 10 minutes. If you didn't request this, ignore this message. ``` **Sample 2:** ``` Your login code for Acme is 523016. Don't share this code with anyone. Reply STOP to opt out. ``` **Sample 3:** ``` Acme security code: 194738. Enter this code to complete your password reset. This code expires in 5 minutes. ``` ``` By entering your phone number, you agree to receive one-time verification codes from [Brand] via SMS. Message frequency varies. Message and data rates may apply. Reply STOP to opt out. ``` - Keep messages under 160 characters - Include the code prominently - Add an expiry time - Don't include marketing content or links - Brand name should appear in the message **API example — registering a 2FA campaign:** ```python Python import telnyx telnyx.api_key = "YOUR_API_KEY" campaign = telnyx.TenDLCCampaign.create( brand_id="B000001", usecase="2FA", description="One-time verification codes for user login and password reset", sample1="Your Acme verification code is 847291. This code expires in 10 minutes.", sample2="Your login code for Acme is 523016. Don't share this code with anyone. Reply STOP to opt out.", sample3="Acme security code: 194738. Enter this code to complete your password reset.", message_flow="Users enter their phone number during login or password reset. A one-time code is sent via SMS. Users enter the code to authenticate.", help_message="Reply HELP for support or contact support@acme.com", optin_message="By entering your phone number, you agree to receive verification codes from Acme via SMS. Reply STOP to opt out.", ) print(campaign.campaign_id) ``` ```javascript Node.js const telnyx = require('telnyx')('YOUR_API_KEY'); const campaign = await telnyx.tenDlcCampaigns.create({ brand_id: 'B000001', usecase: '2FA', description: 'One-time verification codes for user login and password reset', sample1: 'Your Acme verification code is 847291. This code expires in 10 minutes.', sample2: "Your login code for Acme is 523016. Don't share this code with anyone. Reply STOP to opt out.", sample3: 'Acme security code: 194738. Enter this code to complete your password reset.', message_flow: 'Users enter their phone number during login or password reset. A one-time code is sent via SMS.', help_message: 'Reply HELP for support or contact support@acme.com', optin_message: 'By entering your phone number, you agree to receive verification codes from Acme via SMS. Reply STOP to opt out.', }); console.log(campaign.data.campaign_id); ``` ```ruby Ruby require 'telnyx' Telnyx.api_key = 'YOUR_API_KEY' campaign = Telnyx::TenDlcCampaign.create( brand_id: 'B000001', usecase: '2FA', description: 'One-time verification codes for user login and password reset', sample1: 'Your Acme verification code is 847291. This code expires in 10 minutes.', sample2: "Your login code for Acme is 523016. Don't share this code with anyone. Reply STOP to opt out.", sample3: 'Acme security code: 194738. Enter this code to complete your password reset.', message_flow: 'Users enter their phone number during login or password reset.', help_message: 'Reply HELP for support or contact support@acme.com', optin_message: 'By entering your phone number, you agree to receive verification codes from Acme via SMS. Reply STOP to opt out.', ) puts campaign.campaign_id ``` ```java Java import com.telnyx.sdk.ApiClient; import com.telnyx.sdk.api.TenDlcApi; import com.telnyx.sdk.model.CreateTenDlcCampaignRequest; ApiClient client = new ApiClient(); client.setApiKey("YOUR_API_KEY"); TenDlcApi api = new TenDlcApi(client); CreateTenDlcCampaignRequest request = new CreateTenDlcCampaignRequest() .brandId("B000001") .usecase("2FA") .description("One-time verification codes for user login and password reset") .sample1("Your Acme verification code is 847291. This code expires in 10 minutes.") .sample2("Your login code for Acme is 523016. Don't share this code with anyone. Reply STOP to opt out.") .sample3("Acme security code: 194738. Enter this code to complete your password reset.") .messageFlow("Users enter their phone number during login or password reset.") .helpMessage("Reply HELP for support or contact support@acme.com") .optinMessage("By entering your phone number, you agree to receive verification codes from Acme via SMS. Reply STOP to opt out."); var campaign = api.createTenDlcCampaign(request); System.out.println(campaign.getData().getCampaignId()); ``` ```csharp .NET using Telnyx.net; TelnyxConfiguration.SetApiKey("YOUR_API_KEY"); var service = new TenDlcCampaignService(); var campaign = service.Create(new TenDlcCampaignCreateOptions { BrandId = "B000001", Usecase = "2FA", Description = "One-time verification codes for user login and password reset", Sample1 = "Your Acme verification code is 847291. This code expires in 10 minutes.", Sample2 = "Your login code for Acme is 523016. Don't share this code with anyone. Reply STOP to opt out.", Sample3 = "Acme security code: 194738. Enter this code to complete your password reset.", MessageFlow = "Users enter their phone number during login or password reset.", HelpMessage = "Reply HELP for support or contact support@acme.com", OptinMessage = "By entering your phone number, you agree to receive verification codes from Acme via SMS. Reply STOP to opt out.", }); Console.WriteLine(campaign.CampaignId); ``` ```php PHP $telnyx = new \Telnyx\Telnyx('YOUR_API_KEY'); $campaign = \Telnyx\TenDlcCampaign::create([ 'brand_id' => 'B000001', 'usecase' => '2FA', 'description' => 'One-time verification codes for user login and password reset', 'sample1' => 'Your Acme verification code is 847291. This code expires in 10 minutes.', 'sample2' => "Your login code for Acme is 523016. Don't share this code with anyone. Reply STOP to opt out.", 'sample3' => 'Acme security code: 194738. Enter this code to complete your password reset.', 'message_flow' => 'Users enter their phone number during login or password reset.', 'help_message' => 'Reply HELP for support or contact support@acme.com', 'optin_message' => 'By entering your phone number, you agree to receive verification codes from Acme via SMS. Reply STOP to opt out.', ]); echo $campaign->campaign_id; ``` ```go Go package main import ( "fmt" telnyx "github.com/telnyx/telnyx-go" ) func main() { client := telnyx.NewClient("YOUR_API_KEY") campaign, err := client.TenDlc.Campaigns.Create(&telnyx.TenDlcCampaignParams{ BrandID: "B000001", Usecase: "2FA", Description: "One-time verification codes for user login and password reset", Sample1: "Your Acme verification code is 847291. This code expires in 10 minutes.", Sample2: "Your login code for Acme is 523016. Don't share this code with anyone. Reply STOP to opt out.", Sample3: "Acme security code: 194738. Enter this code to complete your password reset.", MessageFlow: "Users enter their phone number during login or password reset.", HelpMessage: "Reply HELP for support or contact support@acme.com", OptinMessage: "By entering your phone number, you agree to receive verification codes from Acme via SMS. Reply STOP to opt out.", }) if err != nil { panic(err) } fmt.Println(campaign.CampaignID) } ``` --- ### Customer Care Support conversations, ticket updates, and service messages. **Sample 1:** ``` Hi [Name], your support ticket #4521 has been updated. Our team has responded — view details at support.acme.com/tickets/4521. Reply STOP to opt out. ``` **Sample 2:** ``` Acme Support: Your return request for order #8834 has been approved. A prepaid label has been sent to your email. Questions? Reply HELP. ``` **Sample 3:** ``` Your appointment with Acme Support is confirmed for March 15 at 2:00 PM EST. Reply YES to confirm or RESCHEDULE to change. Reply STOP to unsubscribe. ``` ``` By providing your phone number, you consent to receive customer service messages from [Brand] via SMS, including support ticket updates, appointment reminders, and service notifications. Message frequency varies. Msg & data rates may apply. Reply HELP for help, STOP to cancel. ``` - Messages should be reactive (responding to customer actions) - Include ticket/order numbers for context - Don't mix in promotional content - Keep a helpful, service-oriented tone - Include brand name in every message --- ### Delivery Notifications Order confirmations, shipping updates, and delivery status. **Sample 1:** ``` Acme: Your order #99281 has shipped! Tracking: 1Z999AA10123456784. Estimated delivery: March 18. Track at acme.com/track. Reply STOP to opt out. ``` **Sample 2:** ``` Your Acme delivery is out for delivery and will arrive today between 2-6 PM. A photo confirmation will be sent upon delivery. Reply STOP to unsubscribe. ``` **Sample 3:** ``` Acme: Your package was delivered at 3:42 PM and left at front door. View delivery photo at acme.com/orders/99281. Reply STOP to opt out. ``` ``` By placing an order, you agree to receive shipping and delivery updates from [Brand] via SMS. Message frequency varies based on order activity. Msg & data rates may apply. Reply STOP to cancel, HELP for help. ``` - Include order/tracking numbers - Messages should follow the order lifecycle - Include delivery estimates when available - Don't add promotional upsells in delivery messages --- ### Account Notifications Password changes, billing alerts, account activity. **Sample 1:** ``` Acme: Your password was successfully changed on March 4, 2026. If you didn't make this change, contact support immediately at 1-800-555-0199. Reply STOP to opt out. ``` **Sample 2:** ``` Acme billing alert: Your payment of $49.99 was processed successfully. Your next billing date is April 4, 2026. View invoice at acme.com/billing. Reply STOP to unsubscribe. ``` **Sample 3:** ``` Acme: Your subscription plan was upgraded to Pro. Your new monthly rate is $79.99, effective immediately. Questions? Reply HELP. Reply STOP to opt out. ``` ``` By creating an account, you consent to receive account-related notifications from [Brand] via SMS, including billing alerts, password changes, and account updates. Msg frequency varies. Msg & data rates apply. Reply STOP to cancel. ``` - Focus on account changes the user initiated - Include specific details (amounts, dates) - Provide a way to verify or dispute changes - Never include marketing in account notifications --- ### Marketing Promotions, sales, product announcements, and brand content. Marketing campaigns receive the most scrutiny. Opt-in must be explicit and separate from terms of service. **Sample 1:** ``` Acme Summer Sale! 🎉 Get 30% off all items this weekend only. Use code SUMMER30 at checkout. Shop now: acme.com/sale. Reply STOP to opt out. ``` **Sample 2:** ``` New at Acme: Our spring collection just dropped! Be the first to shop 50+ new styles starting at $19.99. Browse: acme.com/new. Txt STOP to unsubscribe. ``` **Sample 3:** ``` Acme: Thanks for being a loyal customer! Here's an exclusive 20% off coupon just for you. Use code VIP20 by March 31. Shop: acme.com. Reply STOP to cancel. ``` ``` By checking this box, you agree to receive recurring marketing messages from [Brand] via SMS, including promotions, sales, and product updates. Consent is not a condition of purchase. Message frequency varies (up to 8 msgs/month). Msg & data rates may apply. Reply HELP for help, STOP to unsubscribe. View our Privacy Policy at [link] and Terms at [link]. ``` - Opt-in MUST be separate from ToS (not buried in fine print) - Include message frequency estimate - State "consent is not a condition of purchase" - Link to privacy policy and terms - Every message must include opt-out language - Don't use ALL CAPS excessively - Include brand name in every message --- ### Security Alerts Login alerts, fraud detection, and security notifications. **Sample 1:** ``` Acme security alert: New login detected from Chrome on Windows in New York, NY at 3:42 PM EST. If this wasn't you, secure your account: acme.com/security. Reply STOP to opt out. ``` **Sample 2:** ``` Acme: Unusual activity detected on your account. A purchase of $299.99 was attempted. If this wasn't you, reply BLOCK or call 1-800-555-0199. Reply STOP to unsubscribe. ``` **Sample 3:** ``` Acme: Your account recovery email was changed. If you made this change, no action needed. If not, contact security immediately at 1-800-555-0199. Reply STOP to opt out. ``` ``` By enabling security alerts, you agree to receive security notifications from [Brand] via SMS, including login alerts, fraud detection, and account security updates. Message frequency varies based on account activity. Msg & data rates may apply. Reply STOP to cancel. ``` - Include specific details (device, location, time) - Always provide a way to take action - Keep urgency appropriate — don't create false panic - These are high-priority; carriers are lenient but still review --- ### Polling & Voting Surveys, feedback requests, and interactive polls. **Sample 1:** ``` Acme: How was your recent purchase? Rate your experience 1-5 (1=poor, 5=excellent). Your feedback helps us improve! Reply STOP to opt out. ``` **Sample 2:** ``` Acme customer survey: Would you recommend us to a friend? Reply YES or NO. As a thank you, get 10% off your next order. Reply STOP to unsubscribe. ``` **Sample 3:** ``` Acme: Quick poll — which new feature matters most to you? Reply A) Faster shipping B) More colors C) Lower prices. Reply STOP to opt out. ``` ``` By opting in, you agree to receive occasional survey and feedback requests from [Brand] via SMS. Message frequency: up to 2 msgs/month. Msg & data rates may apply. Reply STOP to opt out, HELP for help. ``` - Keep surveys short (1-2 questions per message) - Make responses simple (numbers, YES/NO, letters) - Don't disguise marketing as surveys - Include clear opt-out in every message --- ### Charity / Nonprofit Fundraising, awareness campaigns, and donation acknowledgments. **Sample 1:** ``` Habitat for Hope: Thanks to donors like you, we built 12 homes this month! See the impact at habitatforhope.org/impact. Reply STOP to opt out. ``` **Sample 2:** ``` Habitat for Hope: Our spring fundraiser starts March 15! Your $25 donation provides building materials for a family in need. Donate: habitatforhope.org/give. Reply STOP to unsubscribe. ``` **Sample 3:** ``` Thank you for your $50 donation to Habitat for Hope! Your tax receipt has been emailed. Together we're building stronger communities. Reply STOP to opt out. ``` ``` By texting JOIN to [number], you agree to receive updates from [Nonprofit] via SMS, including impact updates, fundraising campaigns, and donation receipts. Msg frequency varies (up to 4 msgs/month). Msg & data rates apply. Reply STOP to cancel, HELP for help. ``` - Clearly identify your nonprofit in every message - Show impact, not just ask for money - Include donation receipts/acknowledgments - Link to your organization's website --- ### Mixed (Most Common) Multiple message types from a single campaign. This is the most frequently used use case. **Sample 1 (transactional):** ``` Acme: Your order #77412 has shipped and will arrive by March 18. Track at acme.com/track. Reply STOP to opt out. ``` **Sample 2 (support):** ``` Acme Support: Your ticket #2291 has been resolved. If you need further help, reply to this message or visit support.acme.com. Reply STOP to unsubscribe. ``` **Sample 3 (promotional):** ``` Acme: Spring sale starts tomorrow! Get early access with code SPRING25 for 25% off. Shop: acme.com/sale. Reply STOP to opt out. ``` ``` By providing your phone number, you consent to receive messages from [Brand] via SMS, including order updates, customer support notifications, and promotional offers. Message frequency varies. Msg & data rates may apply. Consent is not required for purchase. Reply HELP for help, STOP to cancel. View our Privacy Policy at [link]. ``` - Each sample should demonstrate a DIFFERENT message type - Opt-in must mention ALL types of messages (transactional + marketing) - If you include marketing, follow marketing opt-in rules - Description should list all message categories - This is the safest choice if you're unsure which use case to pick --- ## Special use cases ### Low Volume For brands sending fewer than 6,000 messages per month. Simplified registration with reduced documentation. Same as whatever your primary use case is — low volume is about throughput, not content type. Use samples from the relevant standard use case above. - Best for small businesses with limited messaging needs - Lower throughput limits (75 messages/minute on T-Mobile) - Cannot be upgraded to standard — must create a new campaign - Still requires compliant opt-in and opt-out ### Sole Proprietor For individuals or small businesses without an EIN. See the full [Sole Proprietor guide](/docs/messaging/10dlc/sole-proprietor/index). ### Emergency For life-safety alerts. Requires demonstrating genuine emergency nature. **Sample 1:** ``` EMERGENCY ALERT: Severe weather warning for your area. Tornado watch until 8 PM. Seek shelter immediately. Details: alerts.example.com. Reply STOP to opt out. ``` **Sample 2:** ``` SafeAlert: Building evacuation in progress at 123 Main St. Exit via stairwell B. Do NOT use elevators. All clear will be sent when safe. Reply STOP to opt out. ``` - Must be genuinely life-safety related - Carriers may grant higher throughput - Don't abuse this category — misuse leads to suspension --- ## Writing compliant sample messages ### Do's and don'ts - Include your brand name in every message - Add opt-out language (STOP to opt out) - Use specific, realistic content - Match samples to your declared use case - Show the actual format you'll send - Include 3 distinct sample messages - Use generic placeholder text ("Hello, this is a test") - Mix marketing into transactional use cases - Use ALL CAPS for entire messages - Include URL shorteners (bit.ly, tinyurl) - Copy samples from other brands - Submit identical or near-identical samples ### Opt-out language requirements Every campaign must support these keywords: | Keyword | Required Response | |---------|-------------------| | `STOP` | "You have been unsubscribed from [Brand] messages. No more messages will be sent. Reply START to resubscribe." | | `HELP` | "[Brand] support: For help, visit [url] or call [number]. Msg & data rates may apply. Msg frequency varies. Reply STOP to cancel." | | `START` / `UNSTOP` | "You have been resubscribed to [Brand] messages. Reply STOP to opt out, HELP for help." | Telnyx handles STOP/START/HELP keyword processing automatically when [Advanced Opt-Out](/docs/messaging/messages/opt-out-opt-in/index) is enabled on your messaging profile. ### Message flow description Your campaign's `message_flow` field should clearly describe how users opt in. Carriers look for: How does the user first provide their phone number? (Website form, checkout, text-to-join keyword, etc.) How is consent captured? (Checkbox, keyword reply, verbal confirmation, etc.) What happens after opt-in? (Welcome message, double opt-in confirmation, etc.) What kinds of messages will the user receive? **Example message flow:** ``` Customers opt in by checking a consent checkbox during online checkout at acme.com/checkout. After opting in, they receive a welcome message confirming their subscription. They then receive order confirmations, shipping updates, and delivery notifications related to their purchases. Customers can opt out at any time by replying STOP. ``` --- ## Common rejection reasons | Rejection Reason | Fix | |-----------------|-----| | Sample messages don't match use case | Rewrite samples to clearly demonstrate your declared use case | | Missing opt-out language | Add "Reply STOP to opt out" to every sample | | Vague or generic samples | Use specific, realistic content with your brand name | | Inadequate opt-in description | Detail the exact opt-in flow (where, how, what users see) | | URL shorteners used | Use full branded URLs (acme.com/track, not bit.ly/abc) | | Samples too similar | Make each sample distinctly different | | Marketing content in non-marketing use case | Remove promotional language or switch to MIXED use case | For detailed troubleshooting of campaign rejections, see the [10DLC Troubleshooting Guide](/docs/messaging/10dlc/troubleshooting/index). --- ## Related resources Register your campaign via API or Portal Register your brand with TCR first Fix registration and delivery issues --- ### ISV/Reseller Onboarding > Source: https://developers.telnyx.com/docs/messaging/10dlc/isv-reseller-onboarding.md If you're an ISV, reseller, or SaaS platform sending messages on behalf of your customers, you need a **partner campaign** architecture — not a standard 10DLC registration. This guide covers everything from initial setup through production messaging. **Quick links:** [Architecture overview](#architecture-overview) · [Step-by-step setup](#step-by-step-isv-onboarding) · [Managing customers at scale](#managing-customers-at-scale) · [Troubleshooting](#troubleshooting) --- ## Who needs this guide? | Scenario | You are... | Your architecture | |----------|-----------|-------------------| | **SaaS platform** | Building messaging into your product | Partner campaign (shared across customers) | | **Reseller / agency** | Managing messaging for clients | One brand + campaign per client, or shared | | **ISV** | Offering white-label messaging | Partner campaign with downstream CSPs | | **Franchise system** | Central brand, many locations | One brand, multiple campaigns per location | **Direct customer?** If you're sending messages for your own business only, use the standard [10DLC quickstart](/docs/messaging/10dlc/quickstart/index) instead. --- ## Architecture overview ISV/reseller 10DLC uses a **shared campaign** model where you register campaigns with your upstream CSP (Campaign Service Provider) and share them to Telnyx for number assignment and messaging. ```mermaid graph TD A[Your Platform] -->|1. Register brand| B[TCR via Upstream CSP] A -->|2. Create campaign| B B -->|3. Share campaign| C[Telnyx] C -->|4. Assign numbers| D[Phone Numbers] D -->|5. Send messages| E[Carriers] style A fill:#6366f1,color:#fff style C fill:#14b8a6,color:#fff ``` ### Key concepts The Campaign Service Provider where you register brands and campaigns with TCR. This could be Telnyx (if you register directly) or another CSP. The messaging provider that sends traffic. Telnyx acts as your downstream CSP — you share campaigns **to** Telnyx for number assignment. A campaign registered at one CSP and shared to another for traffic delivery. Required for ISV architectures. The TCR process of granting a downstream CSP access to send traffic for a campaign. Sharing must be accepted by the downstream CSP. ### Native vs. partner campaigns | Feature | Native Campaign | Partner (Shared) Campaign | |---------|----------------|--------------------------| | Registration | Directly on Telnyx | On upstream CSP, shared to Telnyx | | Brand ownership | Your Telnyx account | Your upstream CSP account | | Campaign management | Telnyx API | Upstream CSP + Telnyx Partner API | | Number assignment | Standard | Via partner campaign endpoints | | Use case | Direct customer | ISV, reseller, multi-tenant | | Appeal process | Direct API | CSP nudge mechanism | --- ## Prerequisites Before starting, ensure you have: [Sign up](https://telnyx.com/sign-up) and complete account verification. You need a **Level 2 verified** account. An account with a CSP where you'll register brands and campaigns (this can be Telnyx or another provider like Campaign Registry direct access). For each customer: legal business name, EIN/tax ID, business address, website, authorized representative contact, and messaging use case details. 10DLC-eligible long code numbers on your Telnyx account. [Purchase numbers](https://portal.telnyx.com/#/app/numbers/buy-numbers) or use existing inventory. --- ## Step-by-step ISV onboarding ### Step 1: Register brands for each customer Each of your customers needs their own **brand** registered with TCR. A brand represents a business entity. If using Telnyx as your upstream CSP, register brands directly: ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/brand \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "entityType": "PRIVATE_PROFIT", "companyName": "Customer Corp LLC", "ein": "12-3456789", "einIssuingCountry": "US", "phone": "+12025551234", "street": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US", "email": "compliance@customercorp.com", "website": "https://customercorp.com", "vertical": "TECHNOLOGY", "displayName": "Customer Corp" }' ``` ```python Python import telnyx import os telnyx.api_key = os.environ["TELNYX_API_KEY"] brand = telnyx.Brand.create( entity_type="PRIVATE_PROFIT", company_name="Customer Corp LLC", ein="12-3456789", ein_issuing_country="US", phone="+12025551234", street="123 Main St", city="New York", state="NY", postal_code="10001", country="US", email="compliance@customercorp.com", website="https://customercorp.com", vertical="TECHNOLOGY", display_name="Customer Corp", ) print(f"Brand ID: {brand.id}") ``` ```javascript Node import Telnyx from "telnyx"; const telnyx = new Telnyx(process.env.TELNYX_API_KEY); const { data: brand } = await telnyx.messaging10dlc.brands.create({ entityType: "PRIVATE_PROFIT", companyName: "Customer Corp LLC", ein: "12-3456789", einIssuingCountry: "US", phone: "+12025551234", street: "123 Main St", city: "New York", state: "NY", postalCode: "10001", country: "US", email: "compliance@customercorp.com", website: "https://customercorp.com", vertical: "TECHNOLOGY", displayName: "Customer Corp", }); console.log(`Brand ID: ${brand.id}`); ``` ```ruby Ruby require "telnyx" Telnyx.api_key = ENV["TELNYX_API_KEY"] brand = Telnyx::Brand.create( entity_type: "PRIVATE_PROFIT", company_name: "Customer Corp LLC", ein: "12-3456789", ein_issuing_country: "US", phone: "+12025551234", street: "123 Main St", city: "New York", state: "NY", postal_code: "10001", country: "US", email: "compliance@customercorp.com", website: "https://customercorp.com", vertical: "TECHNOLOGY", display_name: "Customer Corp" ) puts "Brand ID: #{brand.id}" ``` ```java Java import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messaging10dlc.brands.BrandCreateParams; TelnyxClient client = TelnyxOkHttpClient.fromEnv(); var brand = client.messaging10dlc().brands().create(BrandCreateParams.builder() .entityType("PRIVATE_PROFIT") .companyName("Customer Corp LLC") .ein("12-3456789") .einIssuingCountry("US") .phone("+12025551234") .street("123 Main St") .city("New York") .state("NY") .postalCode("10001") .country("US") .email("compliance@customercorp.com") .website("https://customercorp.com") .vertical("TECHNOLOGY") .displayName("Customer Corp") .build()); System.out.println("Brand ID: " + brand.id()); ``` ```csharp .NET using Telnyx; var client = new TelnyxClient(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var brand = await client.Messaging10dlc.Brands.CreateAsync(new BrandCreateParams { EntityType = "PRIVATE_PROFIT", CompanyName = "Customer Corp LLC", Ein = "12-3456789", EinIssuingCountry = "US", Phone = "+12025551234", Street = "123 Main St", City = "New York", State = "NY", PostalCode = "10001", Country = "US", Email = "compliance@customercorp.com", Website = "https://customercorp.com", Vertical = "TECHNOLOGY", DisplayName = "Customer Corp" }); Console.WriteLine($"Brand ID: {brand.Id}"); ``` ```php PHP $telnyx = new \Telnyx\TelnyxClient(getenv('TELNYX_API_KEY')); $brand = $telnyx->messaging10dlc->brands->create([ 'entity_type' => 'PRIVATE_PROFIT', 'company_name' => 'Customer Corp LLC', 'ein' => '12-3456789', 'ein_issuing_country' => 'US', 'phone' => '+12025551234', 'street' => '123 Main St', 'city' => 'New York', 'state' => 'NY', 'postal_code' => '10001', 'country' => 'US', 'email' => 'compliance@customercorp.com', 'website' => 'https://customercorp.com', 'vertical' => 'TECHNOLOGY', 'display_name' => 'Customer Corp', ]); echo "Brand ID: " . $brand->id . "\n"; ``` ```go Go package main import ( "context" "fmt" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey("YOUR_API_KEY"), ) brand, err := client.Messaging10dlc.Brands.New(context.TODO(), telnyx.Messaging10dlcBrandNewParams{ EntityType: telnyx.String("PRIVATE_PROFIT"), CompanyName: telnyx.String("Customer Corp LLC"), Ein: telnyx.String("12-3456789"), EinIssuingCountry: telnyx.String("US"), Phone: telnyx.String("+12025551234"), Street: telnyx.String("123 Main St"), City: telnyx.String("New York"), State: telnyx.String("NY"), PostalCode: telnyx.String("10001"), Country: telnyx.String("US"), Email: telnyx.String("compliance@customercorp.com"), Website: telnyx.String("https://customercorp.com"), Vertical: telnyx.String("TECHNOLOGY"), DisplayName: telnyx.String("Customer Corp"), }) if err != nil { panic(err) } fmt.Printf("Brand ID: %s\n", brand.ID) } ``` If using an external CSP (e.g., direct TCR access), register brands through their API and note the TCR Brand ID (format: `BXXXXXX`). You'll need this when sharing campaigns to Telnyx. ### Step 2: Submit brand for vetting Higher vetting scores unlock greater throughput. For ISV use cases, **enhanced vetting is strongly recommended**. ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/brand/{brandId}/vetting \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"vettingClass": "ENHANCED"}' ``` Enhanced vetting costs a one-time fee and takes 1–7 business days. Brands with vetting scores above 75 get significantly higher throughput. See [10DLC Rate Limits](/docs/messaging/10dlc/10dlc-rate-limits/index) for details. ### Step 3: Create campaigns with the ISV use case For ISV architectures, use the `AGENTS_FRANCHISES` use case type when registering campaigns: ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/campaign \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "brandId": "BRAND_ID", "usecase": "AGENTS_FRANCHISES", "description": "Platform notifications sent on behalf of Customer Corp customers", "sample1": "Hi {name}, your appointment is confirmed for {date} at {time}. Reply STOP to opt out.", "sample2": "Your order #{order_id} has shipped! Track at {url}. Reply STOP to unsubscribe.", "messageFlow": "Users opt in via web form at customercorp.com/sms-signup with clear consent language. STOP/HELP keywords are honored.", "helpMessage": "Customer Corp support: help@customercorp.com or call 1-800-555-0123. Reply STOP to opt out.", "optinKeywords": "START,YES,SUBSCRIBE", "optoutKeywords": "STOP,CANCEL,UNSUBSCRIBE,QUIT,END", "helpKeywords": "HELP,INFO", "numberPool": false, "subscriberOptin": true, "subscriberOptout": true, "subscriberHelp": true, "embeddedLink": true, "embeddedPhone": false }' ``` ```python Python import telnyx import os telnyx.api_key = os.environ["TELNYX_API_KEY"] campaign = telnyx.Campaign.create( brand_id="BRAND_ID", usecase="AGENTS_FRANCHISES", description="Platform notifications sent on behalf of Customer Corp customers", sample1="Hi {name}, your appointment is confirmed for {date} at {time}. Reply STOP to opt out.", sample2="Your order #{order_id} has shipped! Track at {url}. Reply STOP to unsubscribe.", message_flow="Users opt in via web form at customercorp.com/sms-signup with clear consent language. STOP/HELP keywords are honored.", help_message="Customer Corp support: help@customercorp.com or call 1-800-555-0123. Reply STOP to opt out.", subscriber_optin=True, subscriber_optout=True, subscriber_help=True, embedded_link=True, embedded_phone=False, ) print(f"Campaign ID: {campaign.id}") ``` ```javascript Node import Telnyx from "telnyx"; const telnyx = new Telnyx(process.env.TELNYX_API_KEY); const { data: campaign } = await telnyx.messaging10dlc.campaigns.create({ brandId: "BRAND_ID", usecase: "AGENTS_FRANCHISES", description: "Platform notifications on behalf of Customer Corp customers", sample1: "Hi {name}, your appointment is confirmed for {date} at {time}. Reply STOP to opt out.", sample2: "Your order #{order_id} has shipped! Track at {url}. Reply STOP to unsubscribe.", messageFlow: "Users opt in via web form with clear consent language. STOP/HELP honored.", helpMessage: "Customer Corp support: help@customercorp.com. Reply STOP to opt out.", subscriberOptin: true, subscriberOptout: true, subscriberHelp: true, embeddedLink: true, embeddedPhone: false, }); console.log(`Campaign ID: ${campaign.id}`); ``` **ISV-specific requirements:** - Use case must be `AGENTS_FRANCHISES` for sending on behalf of clients - Sample messages must accurately reflect what your platform sends - Message flow must describe how **end users** (not your clients) consent to receive messages - Each campaign undergoes manual review by TCR — allow 5–10 business days ### Step 4: Share campaign to Telnyx Once your campaign is approved at your upstream CSP, share it to Telnyx. The sharing process depends on your CSP, but the result is a campaign visible in the Telnyx Partner Campaigns API. After sharing, verify the campaign appears on Telnyx: ```bash curl # List all shared campaigns curl -s https://api.telnyx.com/v2/10dlc/partner_campaigns \ -H "Authorization: Bearer $TELNYX_API_KEY" | python3 -m json.tool # Get a specific shared campaign curl -s https://api.telnyx.com/v2/10dlc/partner_campaigns/{campaignId} \ -H "Authorization: Bearer $TELNYX_API_KEY" | python3 -m json.tool ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) # List all shared campaigns page = client.messaging_10dlc.partner_campaigns.list() for campaign in page.records: print(f"Campaign: {campaign.tcr_campaign_id} — Status: {campaign.campaign_status}") # Get a specific campaign campaign = client.messaging_10dlc.partner_campaigns.retrieve("CAMPAIGN_ID") print(f"Brand: {campaign.brand_display_name}, Status: {campaign.campaign_status}") ``` ```javascript Node import Telnyx from "telnyx"; const client = new Telnyx(process.env.TELNYX_API_KEY); // List all shared campaigns for await (const campaign of client.messaging10dlc.partnerCampaigns.list()) { console.log(`Campaign: ${campaign.tcrCampaignId} — Status: ${campaign.campaignStatus}`); } // Get a specific campaign const specific = await client.messaging10dlc.partnerCampaigns.retrieve("CAMPAIGN_ID"); console.log(`Brand: ${specific.brandDisplayName}`); ``` ```go Go package main import ( "context" "fmt" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient(option.WithAPIKey("YOUR_API_KEY")) page, err := client.Messaging10dlc.PartnerCampaigns.List( context.TODO(), telnyx.Messaging10dlcPartnerCampaignListParams{}, ) if err != nil { panic(err) } for _, campaign := range page.Records { fmt.Printf("Campaign: %s — Status: %s\n", campaign.TcrCampaignID, campaign.CampaignStatus) } } ``` ```ruby Ruby require "telnyx" telnyx = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) # List shared campaigns page = telnyx.messaging_10dlc.partner_campaigns.list page.records.each do |campaign| puts "Campaign: #{campaign.tcr_campaign_id} — Status: #{campaign.campaign_status}" end ``` ```java Java import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; TelnyxClient client = TelnyxOkHttpClient.fromEnv(); var page = client.messaging10dlc().partnerCampaigns().list(); page.records().forEach(campaign -> { System.out.printf("Campaign: %s — Status: %s%n", campaign.tcrCampaignId(), campaign.campaignStatus()); }); ``` ```php PHP $telnyx = new \Telnyx\TelnyxClient(getenv('TELNYX_API_KEY')); $page = $telnyx->messaging10dlc->partnerCampaigns->list(); foreach ($page->records as $campaign) { echo "Campaign: {$campaign->tcrCampaignId} — Status: {$campaign->campaignStatus}\n"; } ``` ### Step 5: Assign phone numbers to the shared campaign Once the campaign is accepted by Telnyx, assign your 10DLC numbers to it: ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/phone_number_campaigns \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phoneNumber": "+12025551234", "campaignId": "CAMPAIGN_ID" }' ``` ```python Python import telnyx import os telnyx.api_key = os.environ["TELNYX_API_KEY"] assignment = telnyx.PhoneNumberCampaign.create( phone_number="+12025551234", campaign_id="CAMPAIGN_ID", ) print(f"Assignment status: {assignment.status}") ``` ```javascript Node import Telnyx from "telnyx"; const telnyx = new Telnyx(process.env.TELNYX_API_KEY); const { data } = await telnyx.messaging10dlc.phoneNumberCampaigns.create({ phoneNumber: "+12025551234", campaignId: "CAMPAIGN_ID", }); console.log(`Assignment status: ${data.status}`); ``` Number-to-campaign assignment typically completes within minutes. A number can only be assigned to **one campaign at a time**. To reassign, remove the existing assignment first. ### Step 6: Check sharing status Monitor whether Telnyx has accepted the shared campaign: ```bash curl curl -s https://api.telnyx.com/v2/10dlc/partnerCampaign/{campaignId}/sharing \ -H "Authorization: Bearer $TELNYX_API_KEY" | python3 -m json.tool ``` Possible sharing statuses: | Status | Meaning | |--------|---------| | `PENDING` | Campaign shared, awaiting Telnyx acceptance | | `ACCEPTED` | Telnyx accepted — you can assign numbers and send traffic | | `DECLINED` | Telnyx declined the sharing request | ### Step 7: Send messages Once numbers are assigned to the accepted campaign, send messages using the standard [Send Message API](/docs/messaging/messages/send-message/index): ```bash curl curl -X POST https://api.telnyx.com/v2/messages \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+12025551234", "to": "+13035551234", "text": "Hi Jane, your appointment is confirmed for March 15 at 2:00 PM. Reply STOP to opt out.", "messaging_profile_id": "YOUR_MESSAGING_PROFILE_ID" }' ``` --- ## Managing customers at scale ### Multi-tenant architecture patterns **Best for:** Agencies, resellers managing distinct businesses. Each customer gets their own brand and campaign. This provides: - Isolated throughput per customer - Independent compliance status - Clear separation for TCR **Trade-off:** More registration overhead, but better isolation. ``` Your Platform Account ├── Customer A → Brand A → Campaign A → Numbers [+1xxx, +1yyy] ├── Customer B → Brand B → Campaign B → Numbers [+1zzz] └── Customer C → Brand C → Campaign C → Numbers [+1www, +1vvv] ``` **Best for:** SaaS platforms where all customers send similar message types. One brand (yours) with shared campaigns. All customers' traffic flows through the same campaign. **Trade-off:** Simpler setup, but throughput is shared and one customer's violations affect all. ``` Your Platform Account └── Your Brand → Shared Campaign → Numbers [+1xxx, +1yyy, +1zzz] ├── Customer A traffic ├── Customer B traffic └── Customer C traffic ``` **Best for:** Platforms with a mix of high-volume and low-volume customers. - High-volume customers get dedicated brands + campaigns - Low-volume customers share a platform campaign - Migrate customers to dedicated as they grow ``` Your Platform Account ├── Platform Brand → Shared Campaign → [low-volume customers] ├── Big Customer A → Brand A → Campaign A → [dedicated numbers] └── Big Customer B → Brand B → Campaign B → [dedicated numbers] ``` ### Bulk brand registration For platforms onboarding many customers, automate brand registration: ```python Python import telnyx import os import time telnyx.api_key = os.environ["TELNYX_API_KEY"] customers = [ { "company_name": "Acme Corp LLC", "ein": "12-3456789", "email": "admin@acme.com", "website": "https://acme.com", "phone": "+12025550001", }, { "company_name": "Widget Inc", "ein": "98-7654321", "email": "admin@widget.com", "website": "https://widget.com", "phone": "+12025550002", }, ] results = [] for customer in customers: try: brand = telnyx.Brand.create( entity_type="PRIVATE_PROFIT", company_name=customer["company_name"], ein=customer["ein"], ein_issuing_country="US", phone=customer["phone"], street="123 Main St", city="New York", state="NY", postal_code="10001", country="US", email=customer["email"], website=customer["website"], vertical="TECHNOLOGY", display_name=customer["company_name"].split()[0], ) results.append({"customer": customer["company_name"], "brand_id": brand.id, "status": "created"}) print(f"✓ {customer['company_name']}: Brand {brand.id}") time.sleep(0.5) # Rate limit courtesy except Exception as e: results.append({"customer": customer["company_name"], "error": str(e)}) print(f"✗ {customer['company_name']}: {e}") print(f"\nRegistered {sum(1 for r in results if 'brand_id' in r)}/{len(customers)} brands") ``` ### Webhook monitoring for partner campaigns Set up webhooks to track campaign status changes across all your customers: ```python Python from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/webhooks/10dlc", methods=["POST"]) def handle_10dlc_webhook(self): event = request.json event_type = event.get("data", {}).get("event_type", "") if event_type == "campaign.status_update": campaign_id = event["data"]["payload"]["campaignId"] new_status = event["data"]["payload"]["campaignStatus"] print(f"Campaign {campaign_id} → {new_status}") if new_status == "ACTIVE": # Campaign approved — notify customer, assign numbers activate_customer_messaging(campaign_id) elif new_status == "REJECTED": # Campaign rejected — notify customer with reason reason = event["data"]["payload"].get("rejectionReason", "Unknown") notify_customer_rejection(campaign_id, reason) elif event_type == "brand.status_update": brand_id = event["data"]["payload"]["brandId"] identity_status = event["data"]["payload"]["identityStatus"] print(f"Brand {brand_id} → {identity_status}") return jsonify({"status": "ok"}), 200 ``` --- ## Partner campaign appeals When a shared campaign is rejected, the appeal process differs from native campaigns. Partner campaigns use a **CSP nudge mechanism**: Check the campaign status and rejection details via your upstream CSP. Update the campaign details (description, samples, message flow) at your upstream CSP based on the rejection reason. Your upstream CSP sends a `CAMPAIGN_NUDGE` event to TCR, which triggers a re-review. The nudge mechanism is **only available for partner campaigns**. Native campaigns use the direct appeal API. See [10DLC Event Notifications](/docs/messaging/10dlc/event-notifications/index) for details. Set up webhooks to receive campaign status updates automatically. --- ## Troubleshooting **Cause:** Telnyx hasn't processed the sharing request yet, or there's a data mismatch. **Fix:** 1. Verify the campaign is fully approved at your upstream CSP 2. Confirm you shared to the correct downstream CSP (Telnyx's TCR ID) 3. Contact [Telnyx support](https://support.telnyx.com) with the TCR Campaign ID **Cause:** Campaign sharing hasn't been accepted, or numbers aren't 10DLC-eligible. **Fix:** 1. Check sharing status — must be `ACCEPTED` 2. Verify numbers are long codes (not toll-free or short codes) 3. Ensure numbers aren't already assigned to another campaign 4. Check that numbers are on the same Telnyx account **Cause:** Message content doesn't match registered campaign samples, or throughput exceeds campaign limits. **Fix:** 1. Compare actual message content against registered samples 2. Check [10DLC rate limits](/docs/messaging/10dlc/10dlc-rate-limits/index) for your vetting score 3. Ensure opt-out keywords (STOP, etc.) are properly handled 4. Review the [error code reference](/docs/messaging/messages/error-codes/index) for specific guidance **Cause:** Business information doesn't match public records. **Fix:** See the [10DLC Troubleshooting Guide](/docs/messaging/10dlc/troubleshooting/index) for detailed brand failure resolution steps. **Steps:** 1. Register a new brand for the customer (if not already done) 2. Submit brand for vetting 3. Create a new campaign under their brand 4. Wait for campaign approval 5. Reassign their phone numbers from the shared campaign to the new dedicated campaign 6. Traffic switches immediately — no downtime required --- ## Compliance checklist for ISVs ISVs have **additional compliance responsibilities** because you're sending on behalf of customers. TCR and carriers hold you accountable for your customers' messaging practices. - [ ] **Customer vetting** — Verify your customers' business legitimacy before registration - [ ] **Content monitoring** — Monitor message content for compliance with campaign use case - [ ] **Opt-in verification** — Ensure customers collect proper consent from end users - [ ] **Opt-out processing** — STOP/HELP keywords must work across all customer traffic - [ ] **Volume management** — Don't exceed throughput limits for your campaign's vetting score - [ ] **Incident response** — Have a process to quickly disable a customer's messaging if they violate policies - [ ] **Record retention** — Keep opt-in records for at least 4 years per CTIA guidelines - [ ] **Sample message accuracy** — Registered samples must match actual production messages --- ## Next steps Understand throughput by vetting score and carrier. Set up webhooks for brand and campaign status changes. Details on campaign use cases and requirements. Full API reference for shared campaign management. --- ### Rate Limits & Scores > Source: https://developers.telnyx.com/docs/messaging/10dlc/10dlc-rate-limits.md Your 10DLC throughput is determined by your **brand vetting score** and **campaign type**. Each carrier (AT&T, T-Mobile, Verizon) applies different rate limits. Understanding these limits helps you plan capacity and optimize for higher throughput. --- ## AT&T throughput AT&T assigns throughput per campaign based on a **Message Class**, determined by your use case type and vetting score. | Message Class | Use Case | Vetting Score | SMS TPM | MMS TPM | |---------------|----------|---------------|---------|---------| | A | Standard (Dedicated) | 75-100 | 4,500 | 2,400 | | B | Standard (Mixed/Marketing) | 75-100 | 4,500 | 2,400 | | C | Standard (Dedicated) | 50-74 | 2,400 | 1,200 | | D | Standard (Mixed/Marketing) | 50-74 | 2,400 | 1,200 | | E | Standard (Dedicated) | 1-49 | 240 | 150 | | F | Standard (Mixed/Marketing) | 1-49 | 240 | 150 | | T | Low Volume Mixed | Any | 75 | 50 | | W | Sole Proprietor | N/A | 15 | 50 | **Special use cases** (fixed throughput regardless of vetting score): | Message Class | Use Case | SMS TPM | MMS TPM | |---------------|----------|---------|---------| | K | Political | 4,500 | 2,400 | | P | Charity / Nonprofit | 2,400 | 1,200 | | S | Social | 9,000 | 2,400 | | X | Emergency / Public Safety | 4,500 | 2,400 | | G | Proxy (per number) | 60 | 50 | | N | Agents & Franchises (per number) | 60 | 50 | **TPM = Throughput Per Minute.** AT&T measures throughput in messages per minute, not per second. To convert: 4,500 TPM ≈ 75 MPS. --- ## T-Mobile throughput T-Mobile uses **daily message caps** at the brand level, shared across all campaigns under that brand. | Brand Tier | Vetting Score | Daily SMS Cap | |------------|---------------|---------------| | Top | 75-100 | 200,000 | | High | 50-74 | 40,000 | | Medium | 25-49 | 10,000 | | Basic | 1-24 | 2,000 | | Sole Proprietor | N/A | 1,000 | T-Mobile caps are **per brand**, not per campaign. If you have 3 campaigns under one brand, they share the same daily cap. Plan accordingly for high-volume use cases. Unvetted brands default to the **Basic** tier (2,000/day) unless the business is listed on the Russell 3000 index. --- ## Verizon throughput Verizon has not published specific throughput numbers for 10DLC. They use content-based filtering rather than explicit rate limits. Messages that comply with your registered campaign use case are generally delivered without throttling. --- ## Vetting score impact Your brand's vetting score (0-100) is the single most important factor in determining throughput: ```mermaid graph LR A[Register Brand] --> B[Request Vetting] B --> C{Score?} C -->|75-100| D[Top TierHighest throughput] C -->|50-74| E[Mid TierModerate throughput] C -->|1-49| F[Low TierLimited throughput] C -->|Unvetted| G[BasicMinimum throughput] ``` | Score Range | AT&T SMS TPM | T-Mobile Daily Cap | Recommendation | |-------------|-------------|-------------------|----------------| | 75-100 | 4,500 | 200,000 | ✅ Ideal for production | | 50-74 | 2,400 | 40,000 | ⚠️ Adequate for moderate volume | | 25-49 | 240 | 10,000 | ⚠️ Limited — consider enhanced vetting | | 1-24 | 240 | 2,000 | ❌ Very limited — improve score | | Unvetted | 240 | 2,000 | ❌ Get vetted immediately | --- ## Check your brand and campaign scores ```bash curl # Get brand details including vetting score curl -s https://api.telnyx.com/v2/10dlc/brand/{brandId} \ -H "Authorization: Bearer YOUR_API_KEY" | jq '{ brandId: .data.brandId, displayName: .data.displayName, identityStatus: .data.identityStatus, vettingScore: .data.vettingScore }' # List campaigns with throughput info curl -s https://api.telnyx.com/v2/10dlc/campaign \ -H "Authorization: Bearer YOUR_API_KEY" | jq '.data[] | { campaignId: .campaignId, usecase: .usecase, attMsgClass: .attMsgClass, attTpm: .attTpm, tMobileBrandTier: .tMobileBrandTier }' ``` ```python Python import os import requests API_KEY = os.environ.get("TELNYX_API_KEY") headers = {"Authorization": f"Bearer {API_KEY}"} # Check brand vetting score brand_id = "your_brand_id" response = requests.get( f"https://api.telnyx.com/v2/10dlc/brand/{brand_id}", headers=headers, ) brand = response.json()["data"] print(f"Brand: {brand['displayName']}") print(f"Vetting Score: {brand.get('vettingScore', 'Not vetted')}") print(f"Identity Status: {brand['identityStatus']}") # Check campaign throughput response = requests.get( "https://api.telnyx.com/v2/10dlc/campaign", headers=headers, ) for campaign in response.json()["data"]: print(f"\nCampaign: {campaign['campaignId']}") print(f" Use Case: {campaign['usecase']}") print(f" AT&T Class: {campaign.get('attMsgClass', 'N/A')}") ``` ```javascript Node const axios = require('axios'); const headers = { Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, }; // Check brand vetting score const brandId = 'your_brand_id'; const brand = await axios.get( `https://api.telnyx.com/v2/10dlc/brand/${brandId}`, { headers } ); console.log(`Brand: ${brand.data.data.displayName}`); console.log(`Vetting Score: ${brand.data.data.vettingScore ?? 'Not vetted'}`); // Check campaigns const campaigns = await axios.get( 'https://api.telnyx.com/v2/10dlc/campaign', { headers } ); campaigns.data.data.forEach(c => { console.log(`\nCampaign: ${c.campaignId} (${c.usecase})`); console.log(` AT&T Class: ${c.attMsgClass ?? 'N/A'}`); }); ``` --- ## Maximize your throughput The most impactful action. Ensure before vetting: - **Website is live** and matches your brand information - **EIN matches** your legal business name exactly (IRS records) - **Phone number** is findable via Google for your business - **Email domain** matches your website domain - **Business address** is verifiable Some use cases have higher default throughput: - **Social** campaigns get 9,000 TPM on AT&T - **Political** and **Emergency** get 4,500 TPM - **Mixed** campaigns work for most businesses Don't misrepresent your use case — carriers audit campaigns. If your initial score is below 75, consider requesting enhanced vetting for a more thorough review. Contact [Telnyx support](mailto:support@telnyx.com) for guidance. Assign multiple phone numbers to your campaign. While per-campaign limits still apply, distributing across numbers helps with carrier-level delivery patterns. Track delivery rates via [Message Detail Records](/docs/messaging/messages/message-detail-records/index). High error rates may indicate you're hitting limits. Adjust sending patterns accordingly. --- ## Compliance checklist Carriers can reduce your throughput or reject campaigns that don't follow these guidelines: | Requirement | Details | |-------------|---------| | ✅ Website domain = email domain | `admin@acme.com` + `acme.com` | | ✅ Company name matches website | Consistent branding across registration | | ✅ Clear opt-in on website | SMS consent checkbox visible near submit button | | ✅ Detailed campaign description | Specific, not vague or generic | | ✅ Sample messages match use case | Realistic and representative | | ✅ No disallowed content | No SHAFT, cannabis, payday lending, sweepstakes | | ✅ Opt-out language in messages | Include "Reply STOP to unsubscribe" | ### Disallowed use cases The following use cases will be rejected or result in very low throughput: - Unsolicited messaging (cold outreach, lead generation spam) - Non-direct lending (3rd party auto loans, payday loans) - Indirect debt collection - Cannabis or CBD marketing - Gambling (unless licensed) - SHAFT content (Sex, Hate, Alcohol, Firearms, Tobacco) - Sweepstakes and "free giveaway" campaigns --- ## Related resources Register your brand and campaign for 10DLC messaging. Receive webhooks for brand vetting and campaign status changes. General messaging rate limits for all sender types. Track delivery rates and identify throughput issues. --- ### Phone Number Assignment > Source: https://developers.telnyx.com/docs/messaging/10dlc/phone-number-assignment.md After your [brand](/docs/messaging/10dlc/quickstart/index#step-1-create-a-brand) is registered and [campaign](/docs/messaging/10dlc/campaign-registration/index) is approved, you need to assign phone numbers to the campaign before sending messages. Only numbers assigned to an active campaign can send 10DLC A2P messages. ## How it works ```mermaid flowchart LR A[Phone Number] --> B[Messaging Profile] B --> C[Campaign Assignment] C --> D[Carrier Provisioning] D --> E[Ready to Send] ``` | Requirement | Details | |-------------|---------| | **Number type** | US long code (10-digit) numbers only | | **Messaging profile** | Number must be assigned to a [messaging profile](/docs/messaging/messages/messaging-profiles-overview/index) first | | **Campaign status** | Campaign must be `ACTIVE` (approved by carriers) | | **One campaign per number** | Each number can only be assigned to one campaign at a time | Numbers not assigned to an active 10DLC campaign will have messages filtered or blocked by carriers on AT&T, T-Mobile, and other major US networks. --- ## Assign a number to a campaign ```bash curl curl -X POST https://api.telnyx.com/v2/10dlc/phoneNumberCampaign \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "phoneNumber": "+15551234567", "campaignId": "CAMPAIGN_ID" }' ``` ```python Python import os import requests API_KEY = os.environ.get("TELNYX_API_KEY") headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } response = requests.post( "https://api.telnyx.com/v2/10dlc/phoneNumberCampaign", headers=headers, json={ "phoneNumber": "+15551234567", "campaignId": "CAMPAIGN_ID", }, ) result = response.json() print(f"Number: {result['data']['phoneNumber']}") print(f"Status: {result['data']['status']}") ``` ```javascript Node const axios = require('axios'); const headers = { Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, 'Content-Type': 'application/json', }; const response = await axios.post( 'https://api.telnyx.com/v2/10dlc/phoneNumberCampaign', { phoneNumber: '+15551234567', campaignId: 'CAMPAIGN_ID', }, { headers } ); console.log(`Number: ${response.data.data.phoneNumber}`); console.log(`Status: ${response.data.data.status}`); ``` ```ruby Ruby require "net/http" require "json" require "uri" uri = URI("https://api.telnyx.com/v2/10dlc/phoneNumberCampaign") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" request["Content-Type"] = "application/json" request.body = { phoneNumber: "+15551234567", campaignId: "CAMPAIGN_ID" }.to_json response = http.request(request) result = JSON.parse(response.body) puts "Number: #{result['data']['phoneNumber']}" puts "Status: #{result['data']['status']}" ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) func main() { data := map[string]string{ "phoneNumber": "+15551234567", "campaignId": "CAMPAIGN_ID", } body, _ := json.Marshal(data) req, _ := http.NewRequest("POST", "https://api.telnyx.com/v2/10dlc/phoneNumberCampaign", bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) d := result["data"].(map[string]interface{}) fmt.Printf("Number: %s\nStatus: %s\n", d["phoneNumber"], d["status"]) } ``` ```php PHP '+15551234567', 'campaignId' => 'CAMPAIGN_ID', ]; $ch = curl_init('https://api.telnyx.com/v2/10dlc/phoneNumberCampaign'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$apiKey}", 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($data), ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); echo "Number: {$response['data']['phoneNumber']}\n"; echo "Status: {$response['data']['status']}\n"; ``` ```csharp .NET using System.Net.Http.Headers; using System.Text; using System.Text.Json; var apiKey = Environment.GetEnvironmentVariable("TELNYX_API_KEY"); var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var data = new { phoneNumber = "+15551234567", campaignId = "CAMPAIGN_ID" }; var json = JsonSerializer.Serialize(data); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await client.PostAsync( "https://api.telnyx.com/v2/10dlc/phoneNumberCampaign", content); var result = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(result); var d = doc.RootElement.GetProperty("data"); Console.WriteLine($"Number: {d.GetProperty("phoneNumber")}"); Console.WriteLine($"Status: {d.GetProperty("status")}"); ``` Navigate to [Campaigns](https://portal.telnyx.com/#/messaging-10dlc/campaigns) and select the campaign you want to assign numbers to. Click the **Assign Numbers** panel within your campaign details. Choose the messaging profile that contains the numbers you want to assign. Enter or select the phone numbers to assign to this campaign and click **Assign**. --- ## Bulk assignment When assigning multiple numbers to the same campaign, loop through your numbers programmatically: ```python Python import os import requests import time API_KEY = os.environ.get("TELNYX_API_KEY") headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } campaign_id = "CAMPAIGN_ID" phone_numbers = [ "+15551234567", "+15551234568", "+15551234569", "+15551234570", "+15551234571", ] results = {"success": [], "failed": []} for number in phone_numbers: try: response = requests.post( "https://api.telnyx.com/v2/10dlc/phoneNumberCampaign", headers=headers, json={"phoneNumber": number, "campaignId": campaign_id}, ) if response.status_code in (200, 201): results["success"].append(number) print(f"✓ {number} assigned") else: error = response.json().get("errors", [{}])[0].get("detail", "Unknown") results["failed"].append({"number": number, "error": error}) print(f"✗ {number}: {error}") except Exception as e: results["failed"].append({"number": number, "error": str(e)}) time.sleep(0.5) # Rate limit safety print(f"\nAssigned: {len(results['success'])}, Failed: {len(results['failed'])}") ``` ```javascript Node const axios = require('axios'); const headers = { Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, 'Content-Type': 'application/json', }; const campaignId = 'CAMPAIGN_ID'; const phoneNumbers = [ '+15551234567', '+15551234568', '+15551234569', '+15551234570', '+15551234571', ]; const results = { success: [], failed: [] }; for (const number of phoneNumbers) { try { await axios.post( 'https://api.telnyx.com/v2/10dlc/phoneNumberCampaign', { phoneNumber: number, campaignId }, { headers } ); results.success.push(number); console.log(`✓ ${number} assigned`); } catch (err) { const detail = err.response?.data?.errors?.[0]?.detail || err.message; results.failed.push({ number, error: detail }); console.log(`✗ ${number}: ${detail}`); } await new Promise((r) => setTimeout(r, 500)); // Rate limit safety } console.log(`\nAssigned: ${results.success.length}, Failed: ${results.failed.length}`); ``` ```bash curl #!/bin/bash # Bulk assign numbers to a campaign CAMPAIGN_ID="CAMPAIGN_ID" NUMBERS=("+15551234567" "+15551234568" "+15551234569") for NUM in "${NUMBERS[@]}"; do RESULT=$(curl -s -w "\n%{http_code}" -X POST \ https://api.telnyx.com/v2/10dlc/phoneNumberCampaign \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -d "{\"phoneNumber\": \"$NUM\", \"campaignId\": \"$CAMPAIGN_ID\"}") HTTP_CODE=$(echo "$RESULT" | tail -1) if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then echo "✓ $NUM assigned" else echo "✗ $NUM failed (HTTP $HTTP_CODE)" fi sleep 0.5 done ``` --- ## Number pool integration If you're using [Number Pools](/docs/messaging/messages/number-pool/index), numbers in the pool must also be assigned to a 10DLC campaign. The number pool distributes sending across multiple numbers, but each number still needs campaign registration. Register your campaign via the [Campaign Registration](/docs/messaging/10dlc/campaign-registration/index) guide. Configure your [messaging profile](/docs/messaging/messages/messaging-profiles-overview/index) with number pool enabled. Every number in the pool must be assigned to the same campaign. Use the [bulk assignment](#bulk-assignment) method above. List all numbers assigned to your campaign to confirm all pool numbers are included. If a number in your pool is **not** assigned to a campaign, messages sent from that number will be filtered by carriers. This creates inconsistent delivery — some messages succeed, others fail depending on which pool number is selected. --- ## List assigned numbers ```bash curl # List all numbers assigned to a campaign curl -s "https://api.telnyx.com/v2/10dlc/phoneNumberCampaign?filter[campaignId]=CAMPAIGN_ID" \ -H "Authorization: Bearer YOUR_API_KEY" | jq '.data[] | {phoneNumber, status}' ``` ```python Python response = requests.get( "https://api.telnyx.com/v2/10dlc/phoneNumberCampaign", headers=headers, params={"filter[campaignId]": campaign_id}, ) numbers = response.json()["data"] for n in numbers: print(f"{n['phoneNumber']} | {n['status']}") ``` ```javascript Node const response = await axios.get( 'https://api.telnyx.com/v2/10dlc/phoneNumberCampaign', { headers, params: { 'filter[campaignId]': campaignId }, } ); response.data.data.forEach((n) => { console.log(`${n.phoneNumber} | ${n.status}`); }); ``` --- ## Remove a number from a campaign ```bash curl curl -X DELETE "https://api.telnyx.com/v2/10dlc/phoneNumberCampaign/+15551234567" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python response = requests.delete( "https://api.telnyx.com/v2/10dlc/phoneNumberCampaign/+15551234567", headers=headers, ) print(f"Status: {response.status_code}") # 200 = success ``` ```javascript Node await axios.delete( 'https://api.telnyx.com/v2/10dlc/phoneNumberCampaign/+15551234567', { headers } ); console.log('Number removed from campaign'); ``` Removing a number from a campaign means it can no longer send 10DLC messages. You can reassign it to a different campaign afterward. --- ## Troubleshooting **Cause:** The phone number isn't on your Telnyx account or isn't in E.164 format. **Fix:** - Verify the number is in your account: `GET /v2/phone_numbers?filter[phone_number]=+15551234567` - Ensure E.164 format: `+1` followed by 10 digits (e.g., `+15551234567`) **Cause:** Numbers must be assigned to a messaging profile before campaign assignment. **Fix:** ```bash # Assign number to a messaging profile first curl -X PATCH https://api.telnyx.com/v2/phone_numbers/+15551234567 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"messaging_profile_id": "PROFILE_ID"}' ``` **Cause:** The campaign hasn't been approved by carriers yet. **Fix:** - Check campaign status: `GET /v2/10dlc/campaignBuilder/{campaignId}` - Wait for carrier approval (1-5 business days) - Set up [Event Notifications](/docs/messaging/10dlc/event-notifications/index) to get notified when the campaign is approved **Cause:** Each number can only be assigned to one campaign at a time. **Fix:** 1. Remove the number from the current campaign: `DELETE /v2/10dlc/phoneNumberCampaign/+15551234567` 2. Assign it to the new campaign **Cause:** Carrier provisioning takes time after assignment. **Fix:** - Wait 24-72 hours for all carriers to propagate - Check MNO metadata on the campaign for per-carrier status - Verify the number is sending the same type of content registered in the campaign - Check [Message Detail Records](/docs/messaging/messages/message-detail-records/index) for specific error codes **Cause:** Some numbers may have issues while others succeed. **Fix:** - Check each error response for the specific reason - Common issues: number on different account, already assigned, not on messaging profile - Use the bulk assignment script above with error tracking to identify which numbers failed and why --- ## Next steps Register your campaign use case before assigning numbers. Distribute sending across multiple numbers automatically. Understand throughput per carrier and brand score. Start sending once numbers are assigned and provisioned. --- ### Event Notifications > Source: https://developers.telnyx.com/docs/messaging/10dlc/event-notifications.md You can choose to be notified about events on your 10DLC Brands, Campaigns and Phone Numbers by configuring webhooks. For this mechanism to work, you'll need a publicly accessible HTTP server that can receive our webhook requests at one or more specified URLs. We highly recommend using HTTPS (instead of HTTP). [This tutorial](/docs/messaging/messages/receive-message) walks through setting up a basic application for receiving webhooks. ## Configuring webhooks To receive notifications for brands you need to either provide the webhooks at the [creation](/api-reference/brands/create-brand) of the brand or you may [update](/api-reference/brands/update-brand) an existing brand. In both cases you have to pass your webhooks in the **webhookURL** and **webhookFailoverURL**. **webhookFailoverURL** is optional. Here is an example of updating the webhooks of a brand: *Don't forget to update `YOUR_API_KEY` here.* ```bash curl -X PUT https://api.telnyx.com/10dlc/brand/:brandid \ -H 'Content-type: application/json' \ -H 'Authorization: Bearer ' \ -d '{"webhookURL":"https://mywebhooks.com/c5e5e598-95b3-4076-bfe2-c7d2c58ec57f", "webhookFailoverURL":"https://mywebhooks.com/ae20ec14-1c23-4275-add5-3290706b450f"}' ``` The same applies for campaign event notifications. Webhooks can be provided either upon campaign [creation](/api-reference/campaign/submit-campaign) or through an [update](/api-reference/campaign/update-campaign). Webhooks configured for a campaign are also leveraged for event notifications with phone numbers associated with that campaign. Phone number notifications are triggered for shared campaigns as well. ## Types of events ### Overall structure of events Here is an example of a webhook event: ```json { "data": { "event_type": "10dlc.brand.update", "id": "02d4f0e2-7a9d-4ebf-86b9-3df81e862d49", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "brandId": "97091164-e814-435c-9c1b-14ab2d18e987", "brandName": "Some Brand LLC", "description": "Brand BBRAND1 is added", "eventType": "BRAND_ADD", "status": "success", "tcrBrandId": "BBRAND1", "type": "TCR_BRAND_UPDATE" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Let's have a closer look at the data key: | Field | Description | |-------|-------------| | event_type | We currently support 3 types of events: 10dlc.brand.update, 10dlc.campaign.update and 10dlc.phone_number.update for updates related to brands, campaigns and phone numbers respectively. | | id | Unique ID of this event. | | occurred_at | Timestamp of the event. | | payload | The content of the payload varies according to the type of event. Below we listed the different payload types grouped by entity. | | record_type | Always `event` for webhook events. | The `meta` object contains delivery metadata: | Field | Description | |-------|-------------| | attempt | The delivery attempt number, starting at 1. Useful for identifying retries. | | delivered_to | The webhook URL where this event was sent. | ### Brand events | Payload type | Description | |--------------|-------------| | REGISTRATION | Failures during the registration process. The payload will contain a field called reasons with more details about the errors encountered. | | REVET | Success of the revetting request operation. See the [revet brand endpoint](/api-reference/brands/revet-brand) for more details. | | ORDER_EXTERNAL_VETTING | Notification on the process of ordering an external vetting. The status field indicates if the order succeeded or failed. | | TCR_BRAND_UPDATE | Notifications received from TCR. The table below has a list of all TCR events that are included here. | Here is a list of all TCR events under the **TCR_BRAND_UPDATE** type: | TCR Event | Description | |-----------|-------------| | BRAND_ADD | Brand successfully added on TCR. | | BRAND_APPEAL_ADD | A Brand appeal was added. | | BRAND_APPEAL_COMPLETE | The result of a brand appeal. | | BRAND_REVET | Result of a brand revet request. | Here is an example of a **REGISTRATION** notification: ```json { "data": { "event_type": "10dlc.brand.update", "id": "456abc67-7a9d-4ebf-86b9-3df81e862d49", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "brandId": "b0e2ec67-b26f-4c77-affc-d10f4d1780d3", "status": "failed", "type": "REGISTRATION", "reasons": [ { "fields": [ "ein" ], "description": "Invalid EIN - EIN is a nine-digit number. The format is XX-XXXXXXX. The \"-\" symbol is also accepted." } ] }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of a **TCR_BRAND_UPDATE** notification: ```json { "data": { "event_type": "10dlc.brand.update", "id": "02d4f0e2-7a9d-4ebf-86b9-3df81e862d49", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "brandId": "97091164-e814-435c-9c1b-14ab2d18e987", "brandName": "Some Brand LLC", "description": "Brand BBRAND1 is added", "eventType": "BRAND_ADD", "status": "success", "tcrBrandId": "BBRAND1", "type": "TCR_BRAND_UPDATE" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of an **ORDER_EXTERNAL_VETTING** notification: ```json { "data": { "event_type": "10dlc.brand.update", "id": "a1b2c345-6789-4def-a123-456789abcdef", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "brandId": "97091164-e814-435c-9c1b-14ab2d18e987", "status": "success", "type": "ORDER_EXTERNAL_VETTING", "reasons": [] }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of a **REVET** notification: ```json { "data": { "event_type": "10dlc.brand.update", "id": "def45678-90ab-cdef-1234-567890abcdef", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "brandId": "97091164-e814-435c-9c1b-14ab2d18e987", "status": "success", "type": "REVET", "reasons": [] }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` ### Campaign events | Payload type | Description | |--------------|-------------| | REGISTRATION | Notifications about failures during the registration process. The errors will be listed in the reasons field of the payload. | | TELNYX_REVIEW | Telnyx internal compliance review notification. Sent when Telnyx approves or rejects a campaign. The status field contains `ACCEPTED` or `REJECTED`. The description field contains the TCR campaign ID (e.g., C6X6M95). | | NUMBER_POOL_PROVISIONED | Success on provisioning a number pool. | | NUMBER_POOL_DEPROVISIONED | Success on deprovisioning a number pool. | | TCR_EVENT | Notification received from TCR. See table below for specific event types. | | MNO_REVIEW | MNO/DCA review results. The status field contains `ACCEPTED` or `REJECTED`. In case of rejection, the description field provides a reason. | | TELNYX_EVENT | Telnyx system events such as campaign suspension. The status field contains `DORMANT` for suspended campaigns. | | VERIFIED | Campaign has been successfully provisioned with MNOs. Sent when campaign reaches `MNO_PROVISIONED` status. | Here is a list of TCR events under the **TCR_EVENT** type: | TCR Event | Description | |-----------|-------------| | CAMPAIGN_ADD | Campaign successfully added to TCR. | | CAMPAIGN_BILLED | Campaign billing event from TCR. | | CAMPAIGN_DCA_COMPLETE | DCA processing complete for campaign. | | CAMPAIGN_EXPIRED | Campaign has expired. | | CAMPAIGN_NUDGE | Nudge event sent by partner CSP to trigger campaign re-review after appeal or rejection. | | CAMPAIGN_RESUBMISSION | Campaign has been resubmitted. | | CAMPAIGN_UPDATE | Campaign has been updated. | | MNO_CAMPAIGN_OPERATION_APPROVED | MNO has approved the campaign. | | MNO_CAMPAIGN_OPERATION_REJECTED | MNO has rejected the campaign. | | MNO_CAMPAIGN_OPERATION_REVIEW | Campaign is under MNO review. | | MNO_CAMPAIGN_OPERATION_SUSPENDED | MNO has suspended the campaign. | | MNO_CAMPAIGN_OPERATION_UNSUSPENDED | MNO has unsuspended the campaign. | Here is an example of a campaign **REGISTRATION** failure notification: ```json { "data": { "event_type": "10dlc.campaign.update", "id": "c3d4e567-8901-4bcd-ef23-456789012345", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "status": "failed", "type": "REGISTRATION", "reasons": [ { "fields": ["sample1"], "description": "Sample message does not contain required opt-out language" } ] }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of a **NUMBER_POOL_PROVISIONED** notification: ```json { "data": { "event_type": "10dlc.campaign.update", "id": "d4e5f678-9012-4cde-f345-678901234567", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "brandId": "97091164-e814-435c-9c1b-14ab2d18e987", "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "type": "NUMBER_POOL_PROVISIONED" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of a **NUMBER_POOL_DEPROVISIONED** notification: ```json { "data": { "event_type": "10dlc.campaign.update", "id": "e5f6a789-0123-4def-a456-789012345678", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "brandId": "97091164-e814-435c-9c1b-14ab2d18e987", "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "type": "NUMBER_POOL_DEPROVISIONED" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of a **VERIFIED** notification: ```json { "data": { "event_type": "10dlc.campaign.update", "id": "456abc67-7a9d-4ebf-86b9-3df81e862d49", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "brandId": "97091164-e814-435c-9c1b-14ab2d18e987", "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "createdDate": "2024-07-06T14:22:37.328+00:00", "cspId": "CSPID1", "type": "VERIFIED", "isTMobileRegistered": true }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of a **TELNYX_REVIEW** notification (approval): ```json { "data": { "event_type": "10dlc.campaign.update", "id": "789def12-3a4b-5c6d-7e8f-9a0b1c2d3e4f", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "description": "C6X6M95 approved by Telnyx", "status": "ACCEPTED", "type": "TELNYX_REVIEW" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of a **TELNYX_EVENT** notification (campaign suspension): ```json { "data": { "event_type": "10dlc.campaign.update", "id": "a1b2c345-6789-4def-a123-456789abcdef", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "description": "Campaign has been marked as dormant", "status": "DORMANT", "type": "TELNYX_EVENT" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of an **MNO_REVIEW** notification (rejection): ```json { "data": { "event_type": "10dlc.campaign.update", "id": "def45678-90ab-cdef-1234-567890abcdef", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "description": "Campaign rejected by T-Mobile due to insufficient opt-out instructions", "status": "REJECTED", "type": "MNO_REVIEW" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of a **TCR_EVENT** notification: ```json { "data": { "event_type": "10dlc.campaign.update", "id": "fed98765-4321-dcba-9876-543210fedcba", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "type": "TCR_EVENT", "eventType": "CAMPAIGN_ADD", "description": "Campaign C6X6M95 successfully added to TCR" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Note: The `campaignId` field in webhooks contains the Telnyx UUID, not the TCR campaign ID. The TCR campaign ID (e.g., C6X6M95) may appear in the `description` field. ### Phone number events | Payload type | Description | |--------------|-------------| | ASSIGNMENT | Notifications about the phone number assignment process. In case of failure, an error message is displayed in the reasons field. That field is empty in case of a successful assignment. | | DELETION | Notifications about the phone number removal process. In case of failure, an error message is displayed in the reasons field. That field is empty in case of a successful removal. | | STATUS_UPDATE | The status of the phone number was updated. The new status is shown in the status field. | Phone numbers in webhook payloads use E.164 format (e.g., `+16715455939`), which includes the country code prefix. Here is an example of a successful **ASSIGNMENT** notification: ```json { "data": { "event_type": "10dlc.phone_number.update", "id": "123abc67-7a9d-4ebf-86b9-3df81e862d49", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "phoneNumber": "+16715455939", "status": "success", "type": "ASSIGNMENT", "reasons": [] }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` Here is an example of a **STATUS_UPDATE** notification: ```json { "data": { "event_type": "10dlc.phone_number.update", "id": "789def12-3a4b-5c6d-7e8f-9a0b1c2d3e4f", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "tcrCampaignId": "C6X6M95", "phoneNumber": "+16715455939", "status": "ADDED", "type": "STATUS_UPDATE" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` The `status` field in STATUS_UPDATE notifications can be: `ADDED`, `DELETED`, `PENDING`, or `FAILED`. Here is an example of a **DELETION** notification: ```json { "data": { "event_type": "10dlc.phone_number.update", "id": "b2c3d456-7890-4abc-def1-234567890abc", "occurred_at": "2024-08-07T17:22:37.328+00:00", "payload": { "campaignId": "751c6a5c-907b-43a9-8ada-ba1dc8335b07", "phoneNumber": "+16715455939", "status": "success", "type": "DELETION", "reasons": [] }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://mywebhooks.com/310fda1a-d415-4827-837b-5f7e72657b65" } } ``` ## Campaign Appeals and Nudging Mechanisms When campaigns are rejected, there are different flows for getting them back into the compliance review queue depending on the campaign type and rejection reason. ### Native Campaign Appeals For **native campaigns** rejected due to external factors (e.g., website compliance issues), customers can use the campaign appeal endpoint after addressing the issues: **API Endpoint:** ```bash curl -X POST 'https://api.telnyx.com/10dlc/campaign/{campaignId}/appeal' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "appealReason": "The website has been updated to include the required privacy policy and terms of service." }' ``` This will update the campaign status from `TELNYX_FAILED` to `TCR_ACCEPTED` and re-enter the compliance review queue. ### Partner Campaign Nudging For **partner campaigns**, the appeal process involves the CSP (Campaign Service Provider) sending a `CAMPAIGN_NUDGE` event after reviewing and approving customer changes: **CAMPAIGN_NUDGE Webhook Payload Example:** ```json { "data": { "event_type": "10dlc.campaign.update", "id": "example-event-id", "occurred_at": "2025-07-30T11:07:51.259711+00:00", "payload": { "campaignId": "C4D06C2F", "type": "CAMPAIGN_NUDGE", "nudgeIntent": "APPEAL_REJECTION", "description": "The campaign has been reviewed and approved after appeal.", "cspId": "TNX" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://your-webhook-url.com" } } ``` Note: `CAMPAIGN_NUDGE` events originate from TCR and use the TCR campaign ID format (e.g., `C4D06C2F`) in the `campaignId` field, unlike other campaign webhooks which use the Telnyx UUID. ### Campaign Appeal Scenarios There are several specific scenarios where campaign appeals may be needed: #### 1. Native Campaign Rejected for Content Issues When a customer submits a native campaign and Telnyx rejects it due to issues with the campaign content (e.g., unclear sample messages), the customer can make adjustments to their campaign. Using the campaign update endpoint will automatically reset the campaign's status to `TCR_ACCEPTED` so it goes back into the compliance team's review queue. #### 2. Native Campaign Rejected for External Factors When a customer submits a native campaign and Telnyx rejects it due to factors outside the campaign object (e.g., website compliance requirements), the customer must: - Fix the external issues (e.g., update website with required privacy policy). - Use the appeal API endpoint to get their campaign back in the review queue. **Example failure reason:** ```json { "reason": "Website does not meet compliance requirements." } ``` **After fixes are made, the appeal request:** ```bash curl -X POST 'https://api.telnyx.com/10dlc/campaign/{campaignId}/appeal' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "appealReason": "The website has been updated to include the required privacy policy and terms of service." }' ``` #### 3. Partner Campaign Rejected (Any Reason) For partner campaigns rejected for either content issues or external factors, the process involves the CSP: - Customer makes required adjustments based on rejection reasons. - CSP reviews the changes and approves them. - CSP forwards a `CAMPAIGN_NUDGE` event to Telnyx. - Campaign status changes from `TELNYX_FAILED` to `TCR_ACCEPTED` and re-enters compliance review. This nudging mechanism for partner campaigns cannot work with native campaigns. #### 4. DCA Rejection for External Factors When Telnyx accepts a campaign but the Direct Connect Aggregator (DCA) rejects it due to external factors: - Customer makes required external changes (e.g., website updates). - Customer notifies Telnyx via appropriate appeal mechanism (scenarios 2 or 3). - Once Telnyx approves the changes, Telnyx generates a nudge webhook that the DCA receives. - Campaign re-enters DCA review queue. #### 5. DCA Rejection for Content Issues When the DCA rejects a campaign due to campaign content issues: - Customer updates campaign content (e.g., sample messages). - Customer notifies Telnyx via appropriate appeal mechanism (scenarios 1 or 3). - Once Telnyx approves the changes, Telnyx generates a nudge webhook that the DCA receives. - Campaign re-enters DCA review queue. ### Campaign status flow The following diagram shows the campaign lifecycle including both success and failure paths: ```mermaid stateDiagram-v2 [*] --> TCR_PENDING: Campaign submitted TCR_PENDING --> TCR_ACCEPTED: TCR approves TCR_PENDING --> TCR_FAILED: TCR rejects TCR_ACCEPTED --> TELNYX_FAILED: Telnyx rejects TCR_ACCEPTED --> MNO_PENDING: Telnyx approves MNO_PENDING --> MNO_PROVISIONED: MNOs provision MNO_PENDING --> MNO_REJECTED: MNO rejects TELNYX_FAILED --> TCR_ACCEPTED: Appeal approved MNO_REJECTED --> MNO_PENDING: Appeal approved TCR_ACCEPTED --> TCR_SUSPENDED: Compliance issue MNO_PROVISIONED --> TCR_SUSPENDED: Compliance issue MNO_PROVISIONED --> TCR_EXPIRED: Campaign expires MNO_PROVISIONED --> [*]: Active campaign ``` #### Success path statuses | Status | Description | |--------|-------------| | TCR_PENDING | Campaign is pending review at The Campaign Registry (TCR). | | TCR_ACCEPTED | Campaign has been accepted by TCR and is ready for Telnyx/MNO review. | | MNO_PENDING | Campaign is pending provisioning with Mobile Network Operators (MNOs). | | MNO_PROVISIONED | Campaign has been successfully provisioned with all MNOs. | #### Failure and suspension statuses | Status | Description | |--------|-------------| | TCR_FAILED | Campaign was rejected by TCR during initial registration. | | TELNYX_FAILED | Campaign was rejected by Telnyx compliance review. Can be appealed. | | MNO_REJECTED | Campaign was rejected by one or more MNOs. Can be appealed. | | TCR_SUSPENDED | Campaign has been suspended due to compliance issues. | | TCR_EXPIRED | Campaign has expired and is no longer active. | ### Campaign appeal status flow The typical status flow for campaign appeals is: 1. **Initial rejection**: Campaign status becomes `TELNYX_FAILED`. 2. **Customer action**: Customer addresses the rejection reasons. 3. **Appeal submission**: - Native campaigns: Use appeal API endpoint or campaign update. - Partner campaigns: CSP sends `CAMPAIGN_NUDGE`. 4. **Re-review**: Campaign status changes to `TCR_ACCEPTED` and re-enters compliance review. The nudging mechanism for partner campaigns cannot be used with native campaigns. Native campaigns must use the direct appeal API endpoint or campaign update functionality. ## Process webhooks with SDK examples Handle 10DLC event notifications in your application to track registration status, respond to failures, and automate workflows: ```python Python from flask import Flask, request, jsonify import logging app = Flask(__name__) logger = logging.getLogger(__name__) @app.route("/webhooks/10dlc", methods=["POST"]) def handle_10dlc_webhook(): """Process 10DLC event notifications.""" event = request.json data = event["data"] event_type = data["event_type"] payload = data["payload"] event_id = data["id"] # Deduplicate — store processed event IDs if is_duplicate(event_id): return jsonify({"status": "already_processed"}), 200 if event_type == "10dlc.brand.update": handle_brand_event(payload) elif event_type == "10dlc.campaign.update": handle_campaign_event(payload) elif event_type == "10dlc.phone_number.update": handle_phone_number_event(payload) mark_processed(event_id) return jsonify({"status": "ok"}), 200 def handle_brand_event(payload): brand_id = payload["brandId"] event_type = payload["type"] status = payload.get("status", "") if event_type == "REGISTRATION" and status == "failed": reasons = payload.get("reasons", []) logger.error(f"Brand {brand_id} registration failed: {reasons}") alert_team(f"10DLC brand registration failed: {reasons}") elif event_type == "TCR_BRAND_UPDATE": tcr_event = payload.get("eventType", "") if tcr_event == "BRAND_ADD": logger.info(f"Brand {brand_id} added to TCR") elif tcr_event == "BRAND_REVET": logger.info(f"Brand {brand_id} revet completed: {status}") elif event_type == "ORDER_EXTERNAL_VETTING": logger.info(f"Brand {brand_id} vetting order: {status}") def handle_campaign_event(payload): campaign_id = payload.get("campaignId", "") event_type = payload["type"] status = payload.get("status", "") if event_type == "REGISTRATION" and status == "failed": reasons = payload.get("reasons", []) logger.error(f"Campaign {campaign_id} registration failed: {reasons}") elif event_type == "TELNYX_REVIEW": if status == "ACCEPTED": logger.info(f"Campaign {campaign_id} approved by Telnyx") elif status == "REJECTED": logger.warning(f"Campaign {campaign_id} rejected by Telnyx") elif event_type == "MNO_REVIEW": logger.info(f"Campaign {campaign_id} MNO review: {status}") elif event_type == "VERIFIED": logger.info(f"Campaign {campaign_id} fully provisioned!") # Campaign is ready — you can start sending messages def handle_phone_number_event(payload): phone = payload.get("phoneNumber", "") status = payload.get("status", "") logger.info(f"Phone number {phone} 10DLC status: {status}") ``` ```javascript Node import express from 'express'; const app = express(); app.use(express.json()); const processedEvents = new Set(); app.post('/webhooks/10dlc', (req, res) => { const { data } = req.body; const { event_type, payload, id: eventId } = data; // Deduplicate if (processedEvents.has(eventId)) { return res.json({ status: 'already_processed' }); } switch (event_type) { case '10dlc.brand.update': handleBrandEvent(payload); break; case '10dlc.campaign.update': handleCampaignEvent(payload); break; case '10dlc.phone_number.update': handlePhoneNumberEvent(payload); break; } processedEvents.add(eventId); res.json({ status: 'ok' }); }); function handleBrandEvent(payload) { const { brandId, type, status, reasons } = payload; if (type === 'REGISTRATION' && status === 'failed') { console.error(`Brand ${brandId} registration failed:`, reasons); alertTeam(`10DLC brand registration failed: ${JSON.stringify(reasons)}`); } else if (type === 'TCR_BRAND_UPDATE') { console.log(`Brand ${brandId} TCR event: ${payload.eventType} (${status})`); } else if (type === 'ORDER_EXTERNAL_VETTING') { console.log(`Brand ${brandId} vetting: ${status}`); } } function handleCampaignEvent(payload) { const { campaignId, type, status, reasons } = payload; if (type === 'REGISTRATION' && status === 'failed') { console.error(`Campaign ${campaignId} failed:`, reasons); } else if (type === 'TELNYX_REVIEW') { console.log(`Campaign ${campaignId} Telnyx review: ${status}`); } else if (type === 'VERIFIED') { console.log(`Campaign ${campaignId} fully provisioned — ready to send!`); } } function handlePhoneNumberEvent(payload) { console.log(`Phone ${payload.phoneNumber} status: ${payload.status}`); } app.listen(3000, () => console.log('Webhook server running on port 3000')); ``` ```ruby Ruby require "sinatra" require "json" processed_events = Set.new post "/webhooks/10dlc" do event = JSON.parse(request.body.read) data = event["data"] event_type = data["event_type"] payload = data["payload"] event_id = data["id"] return { status: "already_processed" }.to_json if processed_events.include?(event_id) case event_type when "10dlc.brand.update" if payload["type"] == "REGISTRATION" && payload["status"] == "failed" puts "Brand #{payload['brandId']} registration failed: #{payload['reasons']}" else puts "Brand #{payload['brandId']} event: #{payload['type']} (#{payload['status']})" end when "10dlc.campaign.update" if payload["type"] == "VERIFIED" puts "Campaign #{payload['campaignId']} fully provisioned!" else puts "Campaign event: #{payload['type']} (#{payload['status']})" end when "10dlc.phone_number.update" puts "Phone #{payload['phoneNumber']} status: #{payload['status']}" end processed_events.add(event_id) { status: "ok" }.to_json end ``` ```go Go package main import ( "encoding/json" "fmt" "log" "net/http" "sync" ) var ( processed = make(map[string]bool) mu sync.Mutex ) type WebhookEvent struct { Data struct { EventType string `json:"event_type"` ID string `json:"id"` Payload map[string]interface{} `json:"payload"` } `json:"data"` } func handler(w http.ResponseWriter, r *http.Request) { var event WebhookEvent json.NewDecoder(r.Body).Decode(&event) mu.Lock() if processed[event.Data.ID] { mu.Unlock() json.NewEncoder(w).Encode(map[string]string{"status": "already_processed"}) return } processed[event.Data.ID] = true mu.Unlock() p := event.Data.Payload switch event.Data.EventType { case "10dlc.brand.update": log.Printf("Brand %s event: %s (%s)", p["brandId"], p["type"], p["status"]) case "10dlc.campaign.update": if p["type"] == "VERIFIED" { log.Printf("Campaign %s fully provisioned!", p["campaignId"]) } else { log.Printf("Campaign %s event: %s (%s)", p["campaignId"], p["type"], p["status"]) } case "10dlc.phone_number.update": log.Printf("Phone %s status: %s", p["phoneNumber"], p["status"]) } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"status":"ok"}`) } func main() { http.HandleFunc("/webhooks/10dlc", handler) log.Println("Webhook server on :3000") log.Fatal(http.ListenAndServe(":3000", nil)) } ``` ```php PHP 'already_processed']); exit; } switch ($eventType) { case '10dlc.brand.update': if ($payload['type'] === 'REGISTRATION' && $payload['status'] === 'failed') { error_log("Brand {$payload['brandId']} registration failed: " . json_encode($payload['reasons'])); } else { error_log("Brand {$payload['brandId']} event: {$payload['type']} ({$payload['status']})"); } break; case '10dlc.campaign.update': if ($payload['type'] === 'VERIFIED') { error_log("Campaign {$payload['campaignId']} fully provisioned!"); } else { error_log("Campaign event: {$payload['type']} ({$payload['status']})"); } break; case '10dlc.phone_number.update': error_log("Phone {$payload['phoneNumber']} status: {$payload['status']}"); break; } echo json_encode(['status' => 'ok']); ``` **Important:** Always return a `200` response immediately, then process the webhook asynchronously. For production applications, use a message queue (Redis, RabbitMQ, SQS) to decouple webhook receipt from processing. --- ## Webhook delivery ### Retry policy If your webhook endpoint returns a non-2xx HTTP status code or times out, Telnyx will retry delivery. The `meta.attempt` field in the webhook payload indicates which delivery attempt this is (starting at 1). - **Default retry attempts:** 5 - **Default retry interval:** 30 seconds between attempts After 5 failed attempts, the webhook will be marked as failed and no further retries will be made. ### Best practices - **Return 2xx quickly**: Return a 200 response as soon as possible, then process the webhook asynchronously. - **Handle duplicates**: Webhooks may be delivered more than once. Use the `id` field to deduplicate. - **Use HTTPS**: Always use HTTPS endpoints to ensure webhook data is encrypted in transit. - **Verify the source**: Consider implementing signature verification for added security. - **Set up failover**: Configure a `webhookFailoverURL` to receive webhooks if your primary endpoint is unavailable. ### Testing webhooks locally During development, you can use tunneling tools to expose your local server to the internet for webhook testing: 1. **ngrok**: Run `ngrok http 3000` to create a public URL that forwards to your local port 3000. 2. **Cloudflare Tunnel**: Use `cloudflared tunnel` for a similar tunneling solution. 3. **localtunnel**: Run `lt --port 3000` for a quick temporary URL. Update your brand or campaign webhook URL to the tunnel URL, then monitor incoming webhooks as you trigger events in your 10DLC registration flow. ## Glossary | Term | Definition | |------|------------| | 10DLC | 10-Digit Long Code. Standard 10-digit phone numbers used for application-to-person (A2P) messaging. | | CSP | Campaign Service Provider. A company authorized to submit and manage campaigns on behalf of brands with The Campaign Registry. | | DCA | Direct Connect Aggregator. An entity that has a direct connection to mobile carriers for message delivery. | | MNO | Mobile Network Operator. Wireless carriers such as T-Mobile, AT&T, and Verizon that deliver SMS/MMS messages to end users. | | TCR | The Campaign Registry. The central registry that manages brand and campaign registration for 10DLC messaging in the United States. | ## Related resources Get started with 10DLC brand and campaign registration. Understand throughput limits based on vetting scores. Set up a server to receive webhook notifications. Track delivery status and troubleshoot issues. --- ### Troubleshooting > Source: https://developers.telnyx.com/docs/messaging/10dlc/troubleshooting.md This guide covers the most common 10DLC failures across brand registration, campaign approval, phone number assignment, and message delivery — with specific error codes, root causes, and fixes. **Quick links:** [Brand issues](#brand-registration-failures) · [Campaign issues](#campaign-rejection-reasons) · [Number assignment issues](#phone-number-assignment-failures) · [Delivery issues](#message-delivery-failures) · [Carrier-specific issues](#carrier-specific-issues) --- ## Brand registration failures Brand registration fails when TCR cannot verify your business identity. The `identityStatus` field on your brand object indicates the result. | Status | Meaning | |--------|---------| | `VERIFIED` | Brand identity confirmed | | `UNVERIFIED` | Verification failed — action required | | `VETTED_VERIFIED` | External vetting completed successfully | | `SELF_DECLARED` | Sole proprietor — limited verification | ### Common brand failures and fixes **Error:** Brand identity verification failed — company name mismatch. **Root cause:** The `companyName` you submitted doesn't match what the IRS has on file for that EIN. **Fix:** 1. Look up your exact legal name on the [IRS Tax Exempt Organization Search](https://apps.irs.gov/app/eos/) or your incorporation documents 2. Update your brand with the exact legal name (including suffixes like "Inc.", "LLC") 3. Re-submit for vetting ```bash curl curl -X PATCH https://api.telnyx.com/v2/10dlc/brand/{brandId} \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"companyName": "Exact Legal Name Inc."}' ``` **Error:** Brand address could not be verified. **Root cause:** The address doesn't match USPS records, or uses an incomplete format (e.g., missing suite number). **Fix:** 1. Verify your address via [USPS Address Lookup](https://tools.usps.com/zip-code-lookup.htm) 2. Use the exact USPS-standardized format 3. Include suite/unit numbers if applicable 4. Update the brand and re-submit PO Boxes are generally not accepted. Use a physical business address. **Error:** Brand website could not be verified. **Root cause:** TCR checks that the website is live and relates to the registered business. Common issues: - Website is down or returns errors - URL redirects to a different domain - Website content doesn't match the business name **Fix:** 1. Ensure your website is live and loads without errors 2. Use the root domain (e.g., `https://example.com`, not a deep link) 3. Verify the website clearly identifies your business name 4. Don't use placeholder/under-construction pages **Error:** A brand with this EIN already exists. **Root cause:** Your business was already registered with TCR, either by you or another Telnyx account (or through another CSP). **Fix:** 1. Check your existing brands: `GET /v2/10dlc/brand` 2. If registered under another account, contact [Telnyx support](https://support.telnyx.com) to transfer ownership 3. If registered through another CSP, you can still create campaigns through Telnyx as a secondary CSP **Error:** `400` / code `10012`. The detail message reads "An external vetting for evpId 'X' and vettingClass 'Y' already exists for this brand" or "is already being processed for this brand." **Root cause:** A successful vetting for the same `(evpId, vettingClass)` exists within the last 180 days, or one is currently being processed. **Fix:** 1. Retrieve existing vettings: `GET /v2/10dlc/brand/{brandId}/externalVetting` 2. To order a different vetting, use a different `vettingClass` (for example `ENHANCED` instead of `STANDARD`) or a different `evpId` 3. Failed vettings are excluded from this check, so you can retry immediately after a real failure 4. Successful vettings expire after 6 to 12 months; once expired (TCR `EXPIRED` status), re-submission is allowed **Error:** Brand vetting score too low for desired throughput. **Root cause:** The third-party vetting partner (Campaign Verify) scored your brand below the threshold for your desired messaging volume. **Fix:** 1. Review your brand profile for accuracy — incomplete or inconsistent data lowers scores 2. Ensure your website, social media, and public records align with your brand information 3. Request a re-vet after updating information (each vetting attempt has a fee) 4. See [10DLC Rate Limits](/docs/messaging/10dlc/10dlc-rate-limits/index) for score-to-throughput mapping **Vetting score ranges:** 0–24 (low), 25–49 (medium-low), 50–74 (medium), 75–100 (high). Each tier unlocks higher throughput per carrier. **Error:** OTP verification timed out or failed. **Root cause:** The one-time password sent to your registered phone number wasn't confirmed in time, or the phone number can't receive SMS. **Fix:** 1. Ensure the phone number on file can receive SMS 2. Request a new OTP and complete verification within the time window 3. Check that the phone number matches your identity documents 4. See the [Sole Proprietor guide](/docs/messaging/10dlc/sole-proprietor) for detailed steps ### Check brand status via API ```python Python import telnyx telnyx.api_key = "YOUR_API_KEY" brand = telnyx.TenDlcBrand.retrieve("BRAND_ID") print(f"Status: {brand.identityStatus}") print(f"Entity Type: {brand.entityType}") if brand.identityStatus == "UNVERIFIED": print("⚠ Brand verification failed. Check rejection reason and update brand info.") ``` ```javascript Node const telnyx = require("telnyx")("YOUR_API_KEY"); const brand = await telnyx.tenDlcBrands.retrieve("BRAND_ID"); console.log(`Status: ${brand.data.identityStatus}`); if (brand.data.identityStatus === "UNVERIFIED") { console.log("⚠ Brand verification failed. Check rejection reason and update brand info."); } ``` ```bash curl curl -s https://api.telnyx.com/v2/10dlc/brand/BRAND_ID \ -H "Authorization: Bearer $TELNYX_API_KEY" | python3 -m json.tool ``` --- ## Campaign rejection reasons Campaign rejections happen during carrier review. The rejection reason is delivered via [10DLC Event Notifications](/docs/messaging/10dlc/event-notifications/index) webhooks. | Rejection Reason | Common Cause | Fix | |-----------------|--------------|-----| | **Samples don't match use case** | Sample messages describe a different use case than declared | Rewrite samples to match the exact declared use case | | **Missing opt-out language** | No STOP/unsubscribe instructions in samples | Add "Reply STOP to unsubscribe" to every sample | | **Inadequate opt-in description** | Opt-in workflow is vague or missing | Describe the exact opt-in mechanism (web form URL, keyword, etc.) | | **Prohibited content** | SHAFT content (sex, hate, alcohol, firearms, tobacco) or cannabis | Remove prohibited content or apply for special use case approval | | **Brand not vetted** | Campaign submitted before brand vetting completed | Complete [brand vetting](/docs/messaging/10dlc/brand-registration/index#step-3-submit-for-external-vetting) first | | **Duplicate campaign** | Similar campaign already registered for this brand | Use the existing campaign or differentiate the use case | | **Insufficient sample messages** | Not enough variety in sample messages | Provide 2–5 diverse samples showing different message types | ### Writing samples that pass review If you registered as "Customer Care," every sample should be a customer service message: > ✅ "Hi Jane, your support ticket #4521 has been resolved. Reply HELP for assistance or STOP to opt out." > ❌ "Flash sale! 50% off today only!" (this is marketing, not customer care) Every sample message should include opt-out instructions: > ✅ "Your order #1234 has shipped. Track: https://example.com/track. Reply STOP to unsubscribe." Carriers reject generic or placeholder text: > ❌ "This is a test message" > > ❌ "Hello {name}, this is {company}" > ✅ "Hi Sarah, your appointment at Downtown Clinic is confirmed for March 5 at 2:00 PM. Reply C to confirm or R to reschedule. Reply STOP to opt out." Provide 2–5 samples that demonstrate different message types within your use case: - Confirmation messages - Follow-up messages - Status update messages - Opt-in confirmation messages ### Resubmitting after rejection You cannot edit a rejected campaign. Create a new one with corrected information: ```python Python import telnyx telnyx.api_key = "YOUR_API_KEY" # Create a new campaign with corrected samples campaign = telnyx.TenDlcCampaign.create( brandId="BRAND_ID", usecase="CUSTOMER_CARE", description="Customer support notifications for order updates and ticket resolution", sample1="Hi Jane, your support ticket #4521 has been resolved. Let us know if you need anything else. Reply STOP to opt out.", sample2="Your order #8876 is out for delivery and should arrive by 3 PM today. Reply STOP to unsubscribe.", messageFlow="Customers opt in via checkbox on our website checkout page at https://example.com/checkout. They can opt out at any time by replying STOP.", helpMessage="Reply HELP for support options or visit https://example.com/help", optoutMessage="You have been unsubscribed and will no longer receive messages from us." ) print(f"New campaign ID: {campaign.id}") print(f"Status: {campaign.campaignStatus}") ``` Each campaign submission incurs a TCR registration fee ($15 standard / $4 low-volume). Review samples carefully before submitting to avoid repeated fees. --- ## Phone number assignment failures After campaign approval, you must assign phone numbers to the campaign. Failures typically involve number eligibility or carrier registration issues. **Error:** Phone number is not eligible for 10DLC campaign assignment. **Root cause:** The number may be: - A toll-free number (use [toll-free verification](/docs/messaging/toll-free-verification) instead) - A short code - Not provisioned for messaging - Already assigned to another campaign **Fix:** 1. Verify the number type: `GET /v2/phone_numbers/{id}` 2. Ensure it's a standard long code (10-digit US number) 3. Enable messaging on the number if not already configured 4. Unassign from any existing campaign before reassigning **Error:** Phone number registration with AT&T timed out. **Root cause:** AT&T's 10DLC registration system can be slow, especially during high-volume periods. Registration can take up to 7 business days. **Fix:** 1. Wait 7 business days before escalating 2. Check the number's registration status via webhooks or API 3. If still pending after 7 days, [contact Telnyx support](https://support.telnyx.com) AT&T processes 10DLC number registrations in batches. Delays of 3–5 business days are normal. **Error:** T-Mobile rejected the phone number registration. **Root cause:** T-Mobile applies additional scrutiny to numbers with low-score brands or campaigns flagged for review. **Fix:** 1. Ensure your brand vetting score is sufficient 2. Verify the campaign isn't flagged or suspended 3. Try reassigning the number after the campaign is fully approved 4. Contact [Telnyx support](https://support.telnyx.com) if the issue persists **Error:** Some numbers in a bulk assignment request failed while others succeeded. **Root cause:** Mixed eligibility in the batch — some numbers may already be assigned or not eligible. **Fix:** Check results individually and retry only the failed numbers: ```python Python import telnyx import time telnyx.api_key = "YOUR_API_KEY" failed_numbers = ["+12125551234", "+12125555678"] # Numbers that failed for number in failed_numbers: try: # Check current assignment status resp = telnyx.PhoneNumber.retrieve(number) print(f"{number}: messaging_profile={resp.messaging_profile_id}") # Retry assignment telnyx.TenDlcPhoneNumberCampaign.create( phoneNumber=number, campaignId="CAMPAIGN_ID" ) print(f"✓ {number} assigned successfully") except Exception as e: print(f"✗ {number}: {e}") time.sleep(1) ``` --- ## Message delivery failures Even with approved campaigns and assigned numbers, messages can still fail. These are the most common 10DLC-specific delivery errors. | Error Code | Description | Fix | |-----------|-------------|-----| | `40300` | Number not registered for 10DLC | Assign the sending number to an approved campaign | | `40301` | Campaign suspended | Check campaign status; contact support if unexpected | | `40302` | Brand suspended | Review brand status and resolve compliance issues | | `40310` | Rate limit exceeded | Reduce sending rate; see [10DLC Rate Limits](/docs/messaging/10dlc/10dlc-rate-limits/index) | | `40311` | Daily message limit reached | Wait for daily reset or request higher throughput via vetting | | `40320` | Content flagged by carrier | Review message content for SHAFT violations | | `40321` | URL blocked by carrier | Remove or replace flagged URLs; use branded short links | ### Debugging delivery issues ```python Python import telnyx import json telnyx.api_key = "YOUR_API_KEY" # Check message detail record for error info mdr = telnyx.MessageDetailRecord.list( direction="outbound", phone_number="+12125551234", start_date="2026-03-01T00:00:00Z", end_date="2026-03-02T00:00:00Z" ) for record in mdr.data: if record.status != "delivered": print(f"Message {record.id}") print(f" Status: {record.status}") print(f" Error: {record.errors}") print(f" To: {record.to}") print(f" Sent at: {record.created_at}") ``` ```javascript Node const telnyx = require("telnyx")("YOUR_API_KEY"); const mdrs = await telnyx.messageDetailRecords.list({ direction: "outbound", phone_number: "+12125551234", start_date: "2026-03-01T00:00:00Z", end_date: "2026-03-02T00:00:00Z", }); for (const record of mdrs.data) { if (record.status !== "delivered") { console.log(`Message ${record.id}`); console.log(` Status: ${record.status}`); console.log(` Error: ${JSON.stringify(record.errors)}`); } } ``` ```bash curl curl -s "https://api.telnyx.com/v2/message_detail_records?direction=outbound&phone_number=%2B12125551234&start_date=2026-03-01T00:00:00Z&end_date=2026-03-02T00:00:00Z" \ -H "Authorization: Bearer $TELNYX_API_KEY" | python3 -m json.tool ``` --- ## Carrier-specific issues Each major US carrier handles 10DLC differently. Here are carrier-specific behaviors to be aware of: ### AT&T | Issue | Details | |-------|---------| | **Registration delay** | 3–7 business days for number registration | | **Content filtering** | Aggressive URL filtering; avoid URL shorteners (bit.ly, tinyurl) | | **Rate limits** | Lowest per-campaign limits; vetting score has largest impact | | **Suspension** | Will suspend campaigns with spam complaints without warning | ### T-Mobile | Issue | Details | |-------|---------| | **Content filtering** | Blocks messages with prohibited content patterns | | **Number reassignment** | Requires 24h cooldown when moving numbers between campaigns | | **Daily limits** | Enforces strict daily per-number limits | | **Opt-out enforcement** | Automatically blocks messages to numbers that texted STOP | ### Verizon | Issue | Details | |-------|---------| | **Registration** | Generally fastest registration processing | | **Content** | Less aggressive filtering than AT&T/T-Mobile | | **Rate limits** | More generous throughput at all vetting tiers | --- ## Diagnostic checklist Use this checklist when troubleshooting any 10DLC issue: `GET /v2/10dlc/brand/{brandId}` — confirm `identityStatus` is `VERIFIED` or `VETTED_VERIFIED` `GET /v2/10dlc/campaign/{campaignId}` — confirm `campaignStatus` is `ACTIVE` `GET /v2/10dlc/phoneNumberCampaign?phoneNumber={number}` — confirm the sending number is assigned to the active campaign Review [10DLC webhooks](/docs/messaging/10dlc/event-notifications/index) for rejection, suspension, or status change events Check [MDRs](/docs/messaging/messages/message-detail-records/index) for specific error codes on failed messages Compare your sending rate against your [campaign throughput limits](/docs/messaging/10dlc/10dlc-rate-limits/index). Vetting score determines your ceiling. --- ## Get help If you've followed this guide and still have issues: Open a support ticket with your brand ID, campaign ID, and error details. Check brand, campaign, and number status in Mission Control. Set up webhooks to catch status changes in real time. Review throughput limits by vetting score tier. --- ## API Reference (SMS & MMS) ### Messages - [Send a message](https://developers.telnyx.com/api-reference/messages/send-a-message.md) - [Retrieve a message](https://developers.telnyx.com/api-reference/messages/retrieve-a-message.md) - [Cancel a scheduled message](https://developers.telnyx.com/api-reference/messages/cancel-a-scheduled-message.md) - [Send a group MMS message](https://developers.telnyx.com/api-reference/messages/send-a-group-mms-message.md) - [Send a long code message](https://developers.telnyx.com/api-reference/messages/send-a-long-code-message.md) - [Send a message using number pool](https://developers.telnyx.com/api-reference/messages/send-a-message-using-number-pool.md) - [Schedule a message](https://developers.telnyx.com/api-reference/messages/schedule-a-message.md) - [Send a short code message](https://developers.telnyx.com/api-reference/messages/send-a-short-code-message.md) - [Send a message using an alphanumeric sender ID](https://developers.telnyx.com/api-reference/messages/send-a-message-using-an-alphanumeric-sender-id.md) - [Retrieve group MMS messages](https://developers.telnyx.com/api-reference/messages/retrieve-group-mms-messages.md) ### Whatsapp messaging - [Send a Whatsapp message](https://developers.telnyx.com/api-reference/whatsapp-messaging/send-a-whatsapp-message.md) ### Callbacks - [Delivery Update](https://developers.telnyx.com/api-reference/callbacks/delivery-update.md) - [Inbound Message](https://developers.telnyx.com/api-reference/callbacks/inbound-message.md) - [Replaced Link Click](https://developers.telnyx.com/api-reference/callbacks/replaced-link-click.md) - [Campaign Status Update](https://developers.telnyx.com/api-reference/callbacks/campaign-status-update.md) ### Profiles - [List messaging profiles](https://developers.telnyx.com/api-reference/profiles/list-messaging-profiles.md) - [Create a messaging profile](https://developers.telnyx.com/api-reference/profiles/create-a-messaging-profile.md) - [Retrieve a messaging profile](https://developers.telnyx.com/api-reference/profiles/retrieve-a-messaging-profile.md) - [Update a messaging profile](https://developers.telnyx.com/api-reference/profiles/update-a-messaging-profile.md) - [Delete a messaging profile](https://developers.telnyx.com/api-reference/profiles/delete-a-messaging-profile.md) - [List phone numbers associated with a messaging profile](https://developers.telnyx.com/api-reference/profiles/list-phone-numbers-associated-with-a-messaging-profile.md) - [List short codes associated with a messaging profile](https://developers.telnyx.com/api-reference/profiles/list-short-codes-associated-with-a-messaging-profile.md) ### Opt-Out Management - [List Auto-Response Settings](https://developers.telnyx.com/api-reference/opt-out-management/list-auto-response-settings.md) - [Create auto-response setting](https://developers.telnyx.com/api-reference/opt-out-management/create-auto-response-setting.md) - [Get Auto-Response Setting](https://developers.telnyx.com/api-reference/opt-out-management/get-auto-response-setting.md) - [Update Auto-Response Setting](https://developers.telnyx.com/api-reference/opt-out-management/update-auto-response-setting.md) - [Delete Auto-Response Setting](https://developers.telnyx.com/api-reference/opt-out-management/delete-auto-response-setting.md) - [List opt-outs](https://developers.telnyx.com/api-reference/opt-out-management/list-opt-outs.md) ### Messaging - [Regenerate messaging profile secret](https://developers.telnyx.com/api-reference/messaging/regenerate-messaging-profile-secret.md) - [List alphanumeric sender IDs for a messaging profile](https://developers.telnyx.com/api-reference/messaging/list-alphanumeric-sender-ids-for-a-messaging-profile.md) - [Get detailed messaging profile metrics](https://developers.telnyx.com/api-reference/messaging/get-detailed-messaging-profile-metrics.md) - [Retrieve a messaging hosted number](https://developers.telnyx.com/api-reference/messaging/retrieve-a-messaging-hosted-number.md) - [Update a messaging hosted number](https://developers.telnyx.com/api-reference/messaging/update-a-messaging-hosted-number.md) - [List messaging hosted numbers](https://developers.telnyx.com/api-reference/messaging/list-messaging-hosted-numbers.md) ### Short Codes - [List short codes](https://developers.telnyx.com/api-reference/short-codes/list-short-codes.md) - [Retrieve a short code](https://developers.telnyx.com/api-reference/short-codes/retrieve-a-short-code.md) - [Update short code](https://developers.telnyx.com/api-reference/short-codes/update-short-code.md) ### Hosted Numbers - [List messaging hosted number orders](https://developers.telnyx.com/api-reference/hosted-numbers/list-messaging-hosted-number-orders.md) - [Create a messaging hosted number order](https://developers.telnyx.com/api-reference/hosted-numbers/create-a-messaging-hosted-number-order.md) - [Retrieve a messaging hosted number order](https://developers.telnyx.com/api-reference/hosted-numbers/retrieve-a-messaging-hosted-number-order.md) - [Delete a messaging hosted number order](https://developers.telnyx.com/api-reference/hosted-numbers/delete-a-messaging-hosted-number-order.md) - [Upload hosted number document](https://developers.telnyx.com/api-reference/hosted-numbers/upload-hosted-number-document.md) - [Validate hosted number codes](https://developers.telnyx.com/api-reference/hosted-numbers/validate-hosted-number-codes.md) - [Create hosted number verification codes](https://developers.telnyx.com/api-reference/hosted-numbers/create-hosted-number-verification-codes.md) - [Check hosted messaging eligibility](https://developers.telnyx.com/api-reference/hosted-numbers/check-hosted-messaging-eligibility.md) - [Delete a messaging hosted number](https://developers.telnyx.com/api-reference/hosted-numbers/delete-a-messaging-hosted-number.md) ### Number Settings - [Retrieve a phone number with messaging settings](https://developers.telnyx.com/api-reference/number-settings/retrieve-a-phone-number-with-messaging-settings.md) - [Update the messaging profile and/or messaging product of a phone number](https://developers.telnyx.com/api-reference/number-settings/update-the-messaging-profile-andor-messaging-product-of-a-phone-number.md) - [List phone numbers with messaging settings](https://developers.telnyx.com/api-reference/number-settings/list-phone-numbers-with-messaging-settings.md) - [Bulk update phone number profiles](https://developers.telnyx.com/api-reference/number-settings/bulk-update-phone-number-profiles.md) - [Retrieve bulk update status](https://developers.telnyx.com/api-reference/number-settings/retrieve-bulk-update-status.md) ### Verification Requests - [List Verification Requests](https://developers.telnyx.com/api-reference/verification-requests/list-verification-requests.md) - [Submit Verification Request](https://developers.telnyx.com/api-reference/verification-requests/submit-verification-request.md) - [Get Verification Request](https://developers.telnyx.com/api-reference/verification-requests/get-verification-request.md) - [Update Verification Request](https://developers.telnyx.com/api-reference/verification-requests/update-verification-request.md) - [Delete Verification Request](https://developers.telnyx.com/api-reference/verification-requests/delete-verification-request.md) - [Get Verification Request Status History](https://developers.telnyx.com/api-reference/verification-requests/get-verification-request-status-history.md) ### Campaign - [Submit Campaign](https://developers.telnyx.com/api-reference/campaign/submit-campaign.md) - [Qualify By Usecase](https://developers.telnyx.com/api-reference/campaign/qualify-by-usecase.md) - [List Campaigns](https://developers.telnyx.com/api-reference/campaign/list-campaigns.md) - [Get campaign](https://developers.telnyx.com/api-reference/campaign/get-campaign.md) - [Update campaign](https://developers.telnyx.com/api-reference/campaign/update-campaign.md) - [Deactivate campaign](https://developers.telnyx.com/api-reference/campaign/deactivate-campaign.md) - [Submit campaign appeal for manual review](https://developers.telnyx.com/api-reference/campaign/submit-campaign-appeal-for-manual-review.md) - [Get Campaign Mno Metadata](https://developers.telnyx.com/api-reference/campaign/get-campaign-mno-metadata.md) - [Get campaign operation status](https://developers.telnyx.com/api-reference/campaign/get-campaign-operation-status.md) - [Get OSR campaign attributes](https://developers.telnyx.com/api-reference/campaign/get-osr-campaign-attributes.md) - [Get Sharing Status](https://developers.telnyx.com/api-reference/campaign/get-sharing-status.md) - [Accept Shared Campaign](https://developers.telnyx.com/api-reference/campaign/accept-shared-campaign.md) - [Get Campaign Cost](https://developers.telnyx.com/api-reference/campaign/get-campaign-cost.md) ### Brands - [List Brands](https://developers.telnyx.com/api-reference/brands/list-brands.md) - [Create Brand](https://developers.telnyx.com/api-reference/brands/create-brand.md) - [Get Brand](https://developers.telnyx.com/api-reference/brands/get-brand.md) - [Update Brand](https://developers.telnyx.com/api-reference/brands/update-brand.md) - [Delete Brand](https://developers.telnyx.com/api-reference/brands/delete-brand.md) - [Resend brand 2FA email](https://developers.telnyx.com/api-reference/brands/resend-brand-2fa-email.md) - [List External Vettings](https://developers.telnyx.com/api-reference/brands/list-external-vettings.md) - [Order Brand External Vetting](https://developers.telnyx.com/api-reference/brands/order-brand-external-vetting.md) - [Import External Vetting Record](https://developers.telnyx.com/api-reference/brands/import-external-vetting-record.md) - [Revet Brand](https://developers.telnyx.com/api-reference/brands/revet-brand.md) - [Get Brand SMS OTP Status by Brand ID](https://developers.telnyx.com/api-reference/brands/get-brand-sms-otp-status-by-brand-id.md) - [Trigger Brand SMS OTP](https://developers.telnyx.com/api-reference/brands/trigger-brand-sms-otp.md) - [Verify Brand SMS OTP](https://developers.telnyx.com/api-reference/brands/verify-brand-sms-otp.md) - [Get Brand SMS OTP Status](https://developers.telnyx.com/api-reference/brands/get-brand-sms-otp-status.md) - [Get Brand Feedback By Id](https://developers.telnyx.com/api-reference/brands/get-brand-feedback-by-id.md) ### Enum - [Get Enum](https://developers.telnyx.com/api-reference/enum/get-enum.md) ### Shared Campaigns - [List Shared Campaigns](https://developers.telnyx.com/api-reference/shared-campaigns/list-shared-campaigns.md) - [Get Single Shared Campaign](https://developers.telnyx.com/api-reference/shared-campaigns/get-single-shared-campaign.md) - [Update Single Shared Campaign](https://developers.telnyx.com/api-reference/shared-campaigns/update-single-shared-campaign.md) - [Get Sharing Status](https://developers.telnyx.com/api-reference/shared-campaigns/get-sharing-status.md) - [List shared partner campaigns](https://developers.telnyx.com/api-reference/shared-campaigns/list-shared-partner-campaigns.md) ### Phone Number Campaigns - [List phone number campaigns](https://developers.telnyx.com/api-reference/phone-number-campaigns/list-phone-number-campaigns.md) - [Create New Phone Number Campaign](https://developers.telnyx.com/api-reference/phone-number-campaigns/create-new-phone-number-campaign.md) - [Get Single Phone Number Campaign](https://developers.telnyx.com/api-reference/phone-number-campaigns/get-single-phone-number-campaign.md) - [Delete Phone Number Campaign](https://developers.telnyx.com/api-reference/phone-number-campaigns/delete-phone-number-campaign.md) ### Bulk Phone Number Campaigns - [Assign Messaging Profile To Campaign](https://developers.telnyx.com/api-reference/bulk-phone-number-campaigns/assign-messaging-profile-to-campaign.md) - [Get Assignment Task Status](https://developers.telnyx.com/api-reference/bulk-phone-number-campaigns/get-assignment-task-status.md) - [Get Phone Number Status](https://developers.telnyx.com/api-reference/bulk-phone-number-campaigns/get-phone-number-status.md)