Webhooks
Owlat supports both outbound customer webhooks and inbound provider webhooks.
Owlat supports both outbound customer webhooks and inbound provider webhooks.
Outbound customer webhooks
Configure in Settings → Webhooks.
Supported outbound events:
email.sentemail.deliveredemail.openedemail.clickedemail.bouncedemail.complainedcontact.createdtopic.unsubscribed
Delivery notes
- Endpoint must accept
POST - Use publicly reachable HTTP/HTTPS URLs
- Private/local network hosts are rejected
- Failed deliveries are tracked and can be retried
Signature verification
Webhook secrets use the whsec_... format and are generated at webhook creation.
Every delivery includes these headers:
| Header | Description |
|---|---|
X-Signature | HMAC-SHA256 hex digest of the JSON payload body |
X-Timestamp | Unix timestamp (seconds) when the request was sent |
X-Webhook-Id | The webhook ID |
Content-Type | application/json |
User-Agent | Owlat-Webhooks/1.0 |
Payload shape
{
"event": "email.delivered",
"timestamp": "2026-03-15T12:00:00.000Z",
"data": { /* event-specific data */ }
}
Verification example (Node.js)
import crypto from 'crypto'
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
}
// In your webhook handler:
const payload = await request.text()
const signature = request.headers.get('X-Signature')!
const isValid = verifyWebhookSignature(payload, signature, webhookSecret)
Retry behavior
Failed deliveries are retried up to 3 attempts:
- Immediate
- After 1 minute
- After 5 minutes
Each request has a 30-second timeout. Endpoints that resolve to private/local network IPs are rejected.
Inbound provider webhook routes
These are the endpoints Owlat itself exposes to receive delivery status and inbound mail from its sending provider. They are distinct from the outbound webhooks above, which Owlat sends to your endpoints.
There are two inbound routes, depending on which sending path you run:
| Route | Provider | When it's used |
|---|---|---|
POST /webhooks/mta | Bundled Owlat MTA | Default for self-host |
POST /webhooks/resend | Resend | When configured as the sending provider |
Bundled MTA route
POST /webhooks/mta
For self-hosted deployments, the bundled MTA is the default sending path, so this is the primary inbound route. It handles delivery status, inbound mail, and operational reputation events:
| Event | Effect |
|---|---|
bounced | Records the bounce (hard/soft, pre-classified by the MTA) and updates blocklist + contact activity |
complained | Records the spam complaint and updates blocklist + contact activity |
sent | Marks the send as dispatched |
inbound.received | Ingests inbound mail (e.g. campaign replies) into the team inbox |
org.circuit_breaker | Trips the sending circuit breaker on a high bounce rate |
ip.blocklisted / ip.delisted / ip.warming_complete / all_ips_blocked | Surfaces IP reputation changes |
Requests are signed with HMAC-SHA256 over ${timestamp}.${body}:
| Header | Description |
|---|---|
X-MTA-Signature | HMAC-SHA256 hex digest |
X-MTA-Timestamp | Unix timestamp (seconds), rejected if more than 5 minutes from server time |
The shared secret comes from the MTA_WEBHOOK_SECRET environment variable. If it is unset, the endpoint fails closed with 503.
Resend route
POST /webhooks/resend
Resend sends a wider set of provider events, but Owlat acts on only two of them:
| Event | Effect |
|---|---|
email.bounced | Records the bounce (classified hard/soft from the provider message) and updates blocklist + contact activity |
email.complained | Records the spam complaint and updates blocklist + contact activity |
The other Resend events — email.sent, email.delivered, email.delivery_delayed, email.opened, email.clicked — are received and acknowledged with 200 {"success": true, "ignored": true}, but Owlat does not consume them. Send status is recorded at dispatch time, and open/click data comes from Owlat's own tracking pixel and link redirects, not from Resend's counters.
Do not build integrations that expect the Resend inbound route to drive delivered, open, or click analytics — only bounce and complaint events have any effect. Use the outbound webhooks above to receive email.delivered, email.opened, and email.clicked events.
Resend requests are verified using the Svix signature scheme:
| Header | Description |
|---|---|
svix-id | Message ID |
svix-timestamp | Unix timestamp (seconds), rejected if more than 5 minutes from server time |
svix-signature | One or more space-separated v1,<sig> HMAC signatures |
The signature is verified against the RESEND_WEBHOOK_SECRET environment variable (the whsec_ Base64 secret from your Resend dashboard). If it is unset, the endpoint fails closed with 503.
Payload contract
For the full per-event payload shapes of the outbound webhooks Owlat sends to your endpoints, see Webhook Payloads.