Telnyx API - Comprehensive Communication Platform
v2.0 Enhancement: This skill now includes official Telnyx documentation, production-tested patterns from Twilio-Aldea (including Ed25519 signature validation with tweetnacl, provider-agnostic webhook handlers, and database idempotency), and comprehensive TypeScript examples for SMS/MMS messaging.
When to Use This Skill
Use this skill when working with Telnyx's communication APIs for:
- SMS/MMS Messaging - Send and receive text messages programmatically
- Voice Communication - Build voice calling applications with Call Control API
- Phone Number Management - Search, purchase, and configure phone numbers
- Messaging Profiles - Configure messaging settings and webhooks
- Webhook Integration - Handle real-time events and delivery notifications
- Two-Way SMS Conversations - Build interactive SMS experiences
- Bulk SMS Sending - Send messages to multiple recipients with rate limiting
- Message Scheduling - Schedule messages for future delivery
- Production Deployment - Deploy messaging features with error handling and monitoring
- 10DLC Registration - Register brands and campaigns for US A2P messaging compliance
This skill applies to building communication features in applications, setting up SMS notification systems, creating voice IVR systems, or integrating telephony capabilities.
Quick Reference
1. Send Simple SMS
const axios = require('axios');
async function sendSMS(to, from, text) {
const response = await axios.post(
'https://api.telnyx.com/v2/messages',
{ from, to, text },
{
headers: {
'Authorization': `Bearer ${process.env.TELNYX_API_KEY}`,
'Content-Type': 'application/json'
}
}
);
return response.data;
}
// Usage
await sendSMS('+14155552671', '+14155559999', 'Hello from Telnyx!');
2. Validate Phone Numbers (E.164 Format)
function validateE164(phoneNumber) {
const e164Regex = /^\+[1-9]\d{1,14}$/;
if (!e164Regex.test(phoneNumber)) {
return {
valid: false,
error: 'Phone number must be in E.164 format (e.g., +14155552671)'
};
}
return { valid: true };
}
// Normalize US phone numbers to E.164
function formatToE164(number) {
let digits = number.replace(/\D/g, '');
if (!digits.startsWith('1')) {
digits = '1' + digits;
}
return '+' + digits;
}
3. Handle Incoming Messages (Webhook)
const express = require('express');
app.use(express.json());
app.post('/webhooks/telnyx', (req, res) => {
const event = req.body.data;
if (event.event_type === 'message.received') {
const from = event.payload.from.phone_number;
const text = event.payload.text;
const to = event.payload.to[0].phone_number;
console.log(`Received: "${text}" from ${from}`);
// Auto-reply
await sendSMS(from, to, 'Thanks for your message!');
}
res.status(200).send('OK');
});
4. Verify Webhook Signatures (Ed25519)
const nacl = require('tweetnacl');
function verifyWebhook(rawBody, signature, timestamp, publicKey) {
// Check timestamp freshness (prevent replay attacks)
const timestampMs = parseInt(timestamp);
const now = Date.now();
if (Math.abs(now - timestampMs) > 5 * 60 * 1000) {
return false; // Reject if older than 5 minutes
}
// Construct signed payload
const signedPayload = `${timestamp}|${rawBody}`;
// Decode signature and public key from hex
const signatureBytes = Buffer.from(signature, 'hex');
const publicKeyBytes = Buffer.from(publicKey, 'hex');
// Verify Ed25519 signature
return nacl.sign.detached.verify(
Buffer.from(signedPayload, 'utf8'),
signatureBytes,
publicKeyBytes
);
}
// Usage in Express
app.post('/webhooks/telnyx', (req, res) => {
const signature = req.headers['telnyx-signature-ed25519'];
const timestamp = req.headers['telnyx-timestamp'];
const rawBody = JSON.stringify(req.body);
if (!verifyWebhook(rawBody, signature, timestamp, process.env.TELNYX_PUBLIC_KEY)) {
return res.status(401).send('Invalid signature');
}
// Process webhook...
res.status(200).send('OK');
});
5. Send with Error Handling and Retry
async function sendWithRetry(to, from, text, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await sendSMS(to, from, text);
} catch (error) {
if (error.response?.status >= 500 && attempt < maxRetries) {
// Server error - retry with exponential backoff
const delayMs = Math.pow(2, attempt) * 1000;
console.log(`Retry ${attempt} in ${delayMs}ms...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
} else {
throw error;
}
}
}
}
6. Bulk Sending with Rate Limiting
async function sendBulkSMS(recipients, from, text) {
const delayMs = 100; // 10 messages/second
const results = [];
for (const recipient of recipients) {
try {
const result = await sendSMS(recipient, from, text);
results.push({ success: true, to: recipient, id: result.data.id });
} catch (error) {
results.push({ success: false, to: recipient, error: error.message });
}
await new Promise(resolve => setTimeout(resolve, delayMs));
}
return results;
}
7. Idempotent Webhook Processing (Database)
// Using PostgreSQL with unique constraint
async function processWebhookIdempotent(event, client) {
try {
await client.query('BEGIN');
// Try to insert event (will fail if duplicate due to unique constraint)
await client.query(
'INSERT INTO processed_webhooks (event_id, processed_at) VALUES ($1, NOW())',
[event.id]
);
// Process event
await handleEvent(event, client);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
// If duplicate key error, event was already processed
if (error.code === '23505') {
console.log('Event already processed, skipping');
return;
}
throw error;
}
}
8. Two-Way Conversation State Machine
const conversations = new Map();
app.post('/webhooks/telnyx', async (req, res) => {
const event = req.body.data;
if (event.event_type === 'message.received') {
const from = event.payload.from.phone_number;
const to = event.payload.to[0].phone_number;
const text = event.payload.text.toLowerCase();
let state = conversations.get(from) || 'start';
let reply;
switch (state) {
case 'start':
reply = "Welcome! What's your name?";
conversations.set(from, 'asked_name');
break;
case 'asked_name':
reply = `Nice to meet you, ${text}! How can I help?`;
conversations.set(from, 'helping');
break;
case 'helping':
reply = 'Thanks! A human will respond shortly.';
conversations.delete(from);
break;
}
await sendSMS(from, to, reply);
}
res.status(200).send('OK');
});
9. Check 10DLC Campaign Status
#!/bin/bash
# Monitor campaign approval status
CAMPAIGN_ID="your-campaign-id"
API_KEY="your-api-key"
STATUS=$(curl -s "https://api.telnyx.com/v2/10dlc/campaign/${CAMPAIGN_ID}" \
-H "Authorization: Bearer ${API_KEY}" \
| jq -r '.campaignStatus')
echo "Campaign status: ${STATUS}"
# Status values:
# - TCR_ACCEPTED: Campaign Registry approved (not yet ready)
# - MNO_PENDING: Waiting for carrier approval
# - MNO_PROVISIONED: Fully approved and ready for production
10. Handle Common Errors
function handleTelnyxError(error) {
if (!error.response) {
return { type: 'NETWORK_ERROR', retriable: true };
}
const status = error.response.status;
const errorData = error.response.data.errors?.[0];
switch (status) {
case 400:
case 422:
// Validation error - fix input
return {
type: 'VALIDAT