# Guide to Telnyx Webhooks: Features and Best Practices

Telnyx webhooks notify your application about communications activity: a message is received, a message reaches a final delivery state, a call changes status. If you're building messaging or voice on Telnyx, webhooks are how you react to events without polling.

This guide covers how Telnyx webhooks work, the events you'll handle, how to verify the Ed25519 signature (this is public-key crypto, not an HMAC), the replay window and failover behavior, and the best practices for production.

## What are Telnyx webhooks?

Telnyx delivers events to a webhook URL you configure per messaging profile or connection. The Webhook API v2 signs every event with an Ed25519 public-key signature: two headers, `telnyx-signature-ed25519` (a base64 Ed25519 signature) and `telnyx-timestamp`, verified against your account's public key. (v1 is legacy and unsigned; target v2.)

Because it's asymmetric, verification uses your account's public key, not a shared secret. The official SDKs verify for you, which is the recommended path.

## Telnyx webhook features

| Feature | Details |
| --- | --- |
| Configuration | Mission Control, per messaging profile / connection; set a webhook + failover URL + API version |
| Signature headers | `telnyx-signature-ed25519` (base64 Ed25519), `telnyx-timestamp` (Unix seconds) |
| Signature scheme | Ed25519 over `{telnyx-timestamp}|{raw body}` |
| Verification key | Account public key (Mission Control > Account Settings > Keys & Credentials > Public Key) |
| Replay protection | Timestamp tolerance (SDK default 300s / 5 min) |
| Acknowledgement | 2xx within 2000 ms |
| Failover | Retries with backoff, then fails over to the failover URL (~6 attempts) |
| SDKs | `telnyx` (Node, Python) verify via `constructEvent` / `construct_event` |

## Common events

Telnyx events span messaging and voice. Messaging events are the common starting point:

| Event | Fires when |
| --- | --- |
| `message.received` | An inbound message is received |
| `message.finalized` | A message reaches a final delivery state |
| `message.sent` | An outbound message is sent |

Subscribe to the events your profile needs, and consult Telnyx's webhook reference for the full messaging and voice catalogs.

## Setting up Telnyx webhooks

In Mission Control, configure a webhook URL, a failover URL, and the Webhook API version (choose v2 for signed events) per messaging profile or connection. Your account's base64 public key, used for verification, is under Account Settings > Keys & Credentials > Public Key.

## Securing Telnyx webhooks

Each v2 delivery carries `telnyx-signature-ed25519` (a base64 Ed25519 signature) and `telnyx-timestamp` (Unix seconds). Verification reconstructs the signed message as `{telnyx-timestamp}|{raw body}` and checks it against the signature using your account public key. Enforce a timestamp tolerance (the SDK default is 5 minutes) to block replays.

The official SDKs do all of this. Prefer them over hand-rolling Ed25519:

```javascript
const telnyx = require("telnyx")(process.env.TELNYX_API_KEY);

const PUBLIC_KEY = process.env.TELNYX_PUBLIC_KEY; // account public key

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  try {
    // Verifies the Ed25519 signature and the timestamp tolerance
    const event = telnyx.webhooks.constructEvent(
      req.body.toString("utf8"),
      req.headers["telnyx-signature-ed25519"],
      req.headers["telnyx-timestamp"],
      PUBLIC_KEY
    );
    res.sendStatus(200); // acknowledge within 2s
    processQueue.add(event); // process asynchronously
  } catch {
    res.sendStatus(401); // bad signature or stale timestamp
  }
});

```

The same in Python:

```python
import os
import telnyx

telnyx.api_key = os.environ["TELNYX_API_KEY"]
telnyx.public_key = os.environ["TELNYX_PUBLIC_KEY"]

@app.post("/webhook")
def webhook():
    try:
        event = telnyx.Webhook.construct_event(
            request.get_data(as_text=True),
            request.headers["telnyx-signature-ed25519"],
            request.headers["telnyx-timestamp"],
        )
    except Exception:
        return "", 401
    # process event asynchronously
    return "", 200

```

If you must verify manually, reconstruct `{timestamp}|{raw body}` and check the base64 Ed25519 signature against your account public key with an Ed25519 library (`tweetnacl` in Node, `PyNaCl` in Python), then enforce the timestamp window yourself.

## Telnyx webhook limitations and pain points

### It's Ed25519, not an HMAC

The Problem: Verification is asymmetric: an Ed25519 signature checked against a public key. Reaching for an HMAC (the reflex for most webhooks) doesn't apply, and the signed message is `{timestamp}|{body}`, not the body alone.

Why It Happens: Telnyx v2 uses public-key signing rather than a shared-secret HMAC.

Workarounds:

* Use the SDK `constructEvent` / `construct_event` helpers.
* If verifying manually, reconstruct `{timestamp}|{raw body}` and verify with an Ed25519 library.

How Hookdeck Can Help: Hookdeck verifies the Ed25519 signature at the edge, so your application receives pre-verified events without embedding public-key crypto in your handler.

### The 2-second acknowledgement window

The Problem: Endpoints must return a 2xx within 2000 ms or Telnyx retries and eventually fails over. Any synchronous work risks blowing the window.

Why It Happens: Telnyx caps how long it waits to keep delivery moving.

Workarounds:

* Verify, enqueue, and return 200 immediately; process off a queue.

How Hookdeck Can Help: Hookdeck acknowledges Telnyx within the window and durably queues events, decoupling the 2-second budget from your processing time.

### v1 is unsigned

The Problem: The legacy v1 webhook version is unsigned. An integration left on v1 has no signature to verify, so any POST to the URL looks legitimate.

Why It Happens: v1 predates the Ed25519 signing introduced in v2.

Workarounds:

* Set the Webhook API version to v2 on the profile/connection so deliveries are signed.

How Hookdeck Can Help: Hookdeck gives you a verified ingestion point and can enforce its own authentication, closing the gap if any source is still on an unsigned version.

### Failover and duplicates

The Problem: Telnyx retries and fails over to a secondary URL, so the same event can arrive more than once (and at two URLs).

Why It Happens: Retry-plus-failover favors delivering the event over exactly-once semantics.

Workarounds:

* Dedupe on the event ID and make side effects idempotent.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, so retries and failover don't double-process. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Best practices

### Verify Ed25519 over `{timestamp}|{body}`

Use the SDK helpers with your account public key, and enforce the 5-minute timestamp tolerance to block replays.

### Target v2 for signed events

Set the Webhook API version to v2 so deliveries carry an Ed25519 signature.

### Acknowledge within 2 seconds, process asynchronously

Return 200 immediately and defer work to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Dedupe on the event ID

Use the event ID as an idempotency key so retries and failover don't double-process.

## Conclusion

Telnyx v2 webhooks are signed with Ed25519, verified against your account public key over `{timestamp}|{raw body}`, with a 5-minute replay window and failover to a secondary URL. The keys to a solid integration are using the SDK verification helpers (not an HMAC), staying on v2, acknowledging within 2 seconds, and deduplicating across retries and failover.

[Hookdeck](https://hookdeck.com) verifies the Ed25519 signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Telnyx webhooks reliably in minutes.