Skip to main content
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

How webhook delivery works

1

An event occurs

A message is received by your number, or a sent message changes status (queued → sent → delivered).
2

Telnyx sends a POST request

An HTTP POST with a JSON payload is sent to your configured webhook URL.
3

Your server responds

Return a 2xx status code within 2 seconds to acknowledge receipt.
4

Failover and retries

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 URLswebhook_url and webhook_failover_url in the send message request body
  2. Messaging profile URLs — Configured on the messaging profile
  3. No webhook — If neither is set, no webhook is delivered (events are still available in Message Detail Records)

Webhook event types

Telnyx messaging produces the following webhook events:
Event TypeTriggerDirection
message.receivedAn inbound SMS/MMS arrives at your numberInbound
message.sentAn outbound message has been accepted and sent to the carrierOutbound
message.finalizedAn outbound message has reached a terminal state (delivered, failed, etc.)Outbound

Payload structure

All messaging webhooks share this top-level structure:
{
  "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"
  }
}
FieldDescription
data.event_typeThe event type (message.received, message.sent, message.finalized)
data.idUnique identifier for this webhook event
data.occurred_atISO 8601 timestamp of when the event occurred
data.payloadMessage details (see examples below)
data.record_typeAlways "event"
meta.attemptDelivery attempt number (starts at 1)
meta.delivered_toThe URL this webhook was delivered to

Event examples

Inbound message (message.received)

Triggered when your Telnyx number receives an SMS or MMS:
{
  "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:
{
  "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:
{
  "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:
{
  "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:
StatusDescription
queuedMessage is queued on Telnyx’s side
sendingMessage is being sent to an upstream carrier
sentMessage has been sent to the upstream carrier
deliveredCarrier has confirmed delivery to the recipient
sending_failedTelnyx failed to send the message to the carrier
delivery_failedThe carrier failed to deliver the message to the recipient
delivery_unconfirmedNo delivery confirmation was received from the carrier
When a message fails, the errors array in the payload contains details:
{
  "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:
CodeMeaning
40001Destination number invalid
40002Destination number not in service
40300Destination unreachable
40008Message filtered by carrier
40010Message blocked (spam/content filter)
4700010DLC campaign required
For a complete list, see the Error Codes reference.

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:
HeaderDescription
telnyx-signature-ed25519Base64-encoded Ed25519 signature
telnyx-timestampUnix 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 under Keys & Credentials → Public Key.

Verification examples

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'));
Timestamp tolerance: To prevent replay attacks, reject webhooks where telnyx-timestamp is more than 5 minutes old.

Handling webhooks in your application

Basic webhook handler

import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhooks', (req, res) => {
  // 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'));

Retry behavior and error handling

Retry policy

BehaviorDetail
TimeoutYour endpoint must respond within 2 seconds (API v2)
RetriesUp to 3 attempts per URL with exponential backoff
FailoverIf all retries fail, Telnyx tries the failover URL (if configured)
Total attemptsUp to 6 total (3 primary + 3 failover)
Success responseAny 2xx status code
Failure responseAny non-2xx response, including 3xx redirects

Best practices for reliability

  1. Respond immediately — Return 200 before processing the event. Offload heavy logic to a background queue.
  2. Handle duplicates — Webhooks may be delivered more than once. Use the data.id field as an idempotency key.
  3. Handle out-of-order delivery — Events may arrive in a different order than they occurred. Use data.occurred_at timestamps to sequence events.
  4. Use HTTPS — Always use TLS-encrypted endpoints in production.
  5. Verify signatures — Validate telnyx-signature-ed25519 headers to prevent spoofing.

Webhook IP allowlist

If your server uses a firewall or ACL, allowlist the following Telnyx subnet:
192.76.120.192/27

Troubleshooting

  1. Check your messaging profile — Confirm a webhook URL is configured in the Portal or via the API.
  2. Test your endpoint — Send a test POST request with curl to ensure your server is accessible:
    curl -X POST https://your-endpoint.com/webhooks \
      -H "Content-Type: application/json" \
      -d '{"test": true}'
    
  3. Check ngrok — If using ngrok, verify the tunnel is running and the URL matches your profile configuration.
  4. Check firewall — Ensure 192.76.120.192/27 is allowlisted.
  5. Check Message Detail Records — Events are logged regardless of webhook delivery. Check MDRs in the portal.
This is expected behavior. Telnyx may deliver the same webhook more than once, especially during retries. Track processed event IDs (data.id) and skip duplicates:
const processedEvents = new Set();

app.post('/webhooks', (req, res) => {
  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.
  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