Webhooks
PostMTA delivers real-time event notifications to your endpoint via signed HTTP POST requests. This page covers webhook registration, event types, HMAC-SHA256 verification, idempotency, retry policy, and the dead letter queue.
Webhook Registration
Register a webhook endpoint with POST /v1/webhooks. You can register up to 20 active webhooks per workspace.
{webhook_register}Request fields
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint to receive events. Must return 2xx within 30 seconds. |
events | array of strings | Yes | Event types to subscribe to (see below). |
secret | string | Yes | 32+ byte secret used for HMAC-SHA256 signature generation. |
description | string | No | Human-readable label for this webhook. |
active | boolean | No | Enable/disable without deleting. Defaults to true. |
Listing Webhooks
{webhook_list}Event Types
Subscribe to any combination of the following event types:
| Event | Fired when... | Deduplication window |
|---|---|---|
delivered | The remote MTA accepts the message (2xx SMTP response). | 72 hours |
bounced | The remote MTA rejects the message (4xx/5xx or timeout after 30 days). | 72 hours |
complained | The recipient marks the message as spam (via FBL / Abuse Feedback Loop). | 7 days |
deferred | The remote MTA returns a soft fail; PostMTA will retry automatically. | 72 hours |
rejected | PostMTA rejects the message before any SMTP delivery attempt (policy, suppression, validation). | 72 hours |
Payload Structure
Every webhook POST contains a JSON body with these top-level fields:
{event_payload}All timestamps are ISO 8601 UTC. The event_id field is stable and can be used for idempotency checks.
HMAC-SHA256 Signature Verification
Every webhook request includes a PostMTA-Signature header (also sent as X-PostMTA-Signature for compatibility). The value is sha256=<hex_digest>.
The digest is computed over the raw, unparsed request body as UTF-8. Do NOT use req.body after any JSON parsing middleware — read the raw bytes directly.
timingSafeEqual returns true. Reject everything else with 401 Unauthorized.{hmac_node}Idempotency via event_id
Each event carries a stable event_id (prefixed evt_). If your endpoint receives the same event_id more than once (possible due to retry behaviour), treat it as a duplicate. Store processed event_id values in a set or database row and skip re-processing.
-- Example: idempotency check in SQL
SELECT 1 FROM processed_events WHERE event_id = $1 AND processed_at > NOW() - INTERVAL '72 hours';
-- If row exists, skip processing and return 200 OK immediately.Retry Policy
If your endpoint returns a non-2xx response or times out (30-second connection timeout), PostMTA retries delivery with exponential backoff. The total retry window is approximately 72 hours.
| Attempt | Delay after previous failure | Cumulative elapsed |
|---|---|---|
| Attempt 1 | Immediate | 0 minutes |
| Attempt 2 | 15 minutes | 15 minutes |
| Attempt 3 | 1 hour | 1 hour 15 min |
| Attempt 4 | 4 hours | 5 hours 15 min |
| Attempt 5 | 16 hours | ~21 hours 15 min |
| Attempt 6 | 48 hours | ~69 hours 15 min |
200 OK immediately) and process the event asynchronously.Dead Letter Queue
After all 6 retry attempts are exhausted without a successful delivery (2xx response), the event is moved to the Dead Letter Queue (DLQ). DLQ events are stored for 30 days and can be:
- Inspected via the dashboard under App → Webhooks → Dead Letter.
- Replayed manually via the API:
POST /v1/webhooks/:webhook_id/dlq/:event_id/replay. - Discarded individually or in bulk.
curl -X POST https://api.postmta.com/v1/webhooks/whk_01HXTST001/dlq/evt_01HXEVT001/replay \\
-H "Authorization: Bearer ***"TLS Requirement (PMH-SEC-018)
Webhook endpoints must use HTTPS with TLS 1.2 or higher. Self-signed certificates are not accepted. Certificate chain validation is performed at connection time. Plain HTTP endpoints are rejected at registration time.
http:// (plaintext) or invalid TLS will be rejected during registration. Update any existing plaintext webhook URLs immediately to avoid event loss.Testing Webhooks
Send a test payload to your endpoint without triggering real events:
{webhook_test}This delivers a synthetic test event to your endpoint. Use it to validate connectivity, TLS, and signature verification in staging environments.
Webhook Rate Limits
PostMTA does not apply rate limits to your webhook endpoint — the responsibility is on you to respond within 30 seconds. If your endpoint consistently times out or returns 5xx errors, PostMTA will continue retrying up to the maximum attempts. Consider scaling your webhook consumer horizontally to handle burst deliveries.