Webhook Events

Txtly sends webhook events to notify your application about email delivery lifecycle changes, contact updates, and broadcast completions. This guide covers event types, payload structure, signature verification, and best practices.

Event types

Subscribe to any combination of the following event types when creating a webhook:

Email events

ParameterTypeDescription
email.senteventEmail accepted by Txtly and queued for delivery.
email.deliveredeventEmail successfully delivered to the recipient mail server.
email.delivery_delayedeventDelivery temporarily delayed. Txtly will continue retrying.
email.bouncedeventEmail bounced. Includes bounce_type (permanent or transient) and diagnostic info.
email.complainedeventRecipient marked the email as spam.
email.openedeventRecipient opened the email (requires open tracking enabled).
email.clickedeventRecipient clicked a link in the email (requires click tracking enabled).
email.unsubscribedeventRecipient used the one-click unsubscribe link.

Contact events

ParameterTypeDescription
contact.createdeventA new contact was added to your team.
contact.updatedeventA contact record was modified.
contact.deletedeventA contact was removed.
contact.unsubscribedeventA contact unsubscribed from one or more topics.

Broadcast events

ParameterTypeDescription
broadcast.senteventA broadcast finished sending to all recipients.
broadcast.cancelledeventA queued broadcast was cancelled before completion.

Payload structure

Every webhook delivery sends a JSON payload with a consistent envelope:

{
  "id": "evt_abc123",
  "type": "email.delivered",
  "created_at": "2026-03-21T14:30:00Z",
  "data": {
    "email_id": "em_xyz789",
    "from": "hello@yourdomain.com",
    "to": "user@example.com",
    "subject": "Welcome!",
    "delivered_at": "2026-03-21T14:30:00Z"
  }
}
ParameterTypeDescription
idstringUnique event ID. Use for deduplication.
typestringEvent type (e.g. email.delivered).
created_atstringISO 8601 timestamp of when the event occurred.
dataobjectEvent-specific payload. Shape varies by event type.

Bounce event example

{
  "id": "evt_bounce456",
  "type": "email.bounced",
  "created_at": "2026-03-21T14:31:00Z",
  "data": {
    "email_id": "em_xyz789",
    "from": "hello@yourdomain.com",
    "to": "invalid@example.com",
    "bounce_type": "permanent",
    "diagnostic_code": "550 5.1.1 User unknown",
    "action": "suppressed"
  }
}

Signature verification

Every webhook delivery includes a txtly-signature header. Verify this signature to ensure the payload was sent by Txtly and has not been tampered with.

The signature is an HMAC-SHA256 hash of the raw request body, signed with the webhook's signing secret. The header format is t=timestamp,v1=signature.

Verification steps

1. Extract the timestamp and signature from the header. 2. Concatenate the timestamp, a dot, and the raw request body. 3. Compute an HMAC-SHA256 using your signing secret. 4. Compare the computed signature with the one in the header using a timing-safe comparison. 5. Optionally reject payloads with timestamps older than 5 minutes to prevent replay attacks.

Node.js example

import crypto from "crypto";

function verifyWebhook(
  payload: string,
  header: string,
  secret: string
): boolean {
  const [tPart, vPart] = header.split(",");
  const timestamp = tPart.replace("t=", "");
  const signature = vPart.replace("v1=", "");

  const signedPayload = timestamp + "." + payload;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expected, "hex")
  );
}

Python example

import hmac
import hashlib

def verify_webhook(payload: bytes, header: str, secret: str) -> bool:
    t_part, v_part = header.split(",")
    timestamp = t_part.replace("t=", "")
    signature = v_part.replace("v1=", "")

    signed_payload = f"{timestamp}.".encode() + payload
    expected = hmac.new(
        secret.encode(), signed_payload, hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

Retry schedule

If your endpoint returns a non-2xx status code or times out (30 second limit), Txtly retries delivery with exponential backoff:

ParameterTypeDescription
Attempt 1retryImmediate (original delivery).
Attempt 2retryAfter 5 seconds.
Attempt 3retryAfter 5 minutes.
Attempt 4retryAfter 30 minutes.
Attempt 5retryAfter 2 hours.
Attempt 6retryAfter 5 hours.
Attempt 7retryAfter 10 hours (final attempt).

After all retries are exhausted, the delivery is marked as failed. You can view delivery history and manually retry failed deliveries from the dashboard or via the API.

Best practices

Return a 2xx response within 5 seconds to acknowledge receipt. Process the event asynchronously in a background job to avoid timeouts. Use the event id field for idempotent processing — Txtly may deliver the same event more than once during retries. Always verify the signature before processing the payload. Store the raw payload for debugging and audit purposes.