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
| Parameter | Type | Description |
|---|---|---|
email.sent | event | Email accepted by Txtly and queued for delivery. |
email.delivered | event | Email successfully delivered to the recipient mail server. |
email.delivery_delayed | event | Delivery temporarily delayed. Txtly will continue retrying. |
email.bounced | event | Email bounced. Includes bounce_type (permanent or transient) and diagnostic info. |
email.complained | event | Recipient marked the email as spam. |
email.opened | event | Recipient opened the email (requires open tracking enabled). |
email.clicked | event | Recipient clicked a link in the email (requires click tracking enabled). |
email.unsubscribed | event | Recipient used the one-click unsubscribe link. |
Contact events
| Parameter | Type | Description |
|---|---|---|
contact.created | event | A new contact was added to your team. |
contact.updated | event | A contact record was modified. |
contact.deleted | event | A contact was removed. |
contact.unsubscribed | event | A contact unsubscribed from one or more topics. |
Broadcast events
| Parameter | Type | Description |
|---|---|---|
broadcast.sent | event | A broadcast finished sending to all recipients. |
broadcast.cancelled | event | A 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"
}
}| Parameter | Type | Description |
|---|---|---|
id | string | Unique event ID. Use for deduplication. |
type | string | Event type (e.g. email.delivered). |
created_at | string | ISO 8601 timestamp of when the event occurred. |
data | object | Event-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:
| Parameter | Type | Description |
|---|---|---|
Attempt 1 | retry | Immediate (original delivery). |
Attempt 2 | retry | After 5 seconds. |
Attempt 3 | retry | After 5 minutes. |
Attempt 4 | retry | After 30 minutes. |
Attempt 5 | retry | After 2 hours. |
Attempt 6 | retry | After 5 hours. |
Attempt 7 | retry | After 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.