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.
Each feedback-capable sending path has a stable inbound route:
| Route | Provider | When it's used |
|---|---|---|
POST /webhooks/mta | Bundled Owlat MTA | Default for self-host |
POST /webhooks/ses | Amazon SES through SNS | When SES feedback is configured |
POST /webhooks/resend | Resend | When configured as the sending provider |
POST /webhooks/mandrill | Mailchimp Transactional | When Mandrill feedback is configured |
POST /webhooks/emailit | Emailit | When Emailit feedback is configured |
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.
Emailit route
POST /webhooks/emailit
Emailit accepted, attempted, delivered, bounced, complained, failed, rejected, and suppressed events enter the same normalized lifecycle pipeline as incumbent providers. Suppression reasons are mapped into Owlat's closed recipient-safe vocabulary; loaded/clicked events are ignored because first-party tracking remains authoritative.
Requests require X-Emailit-Timestamp and X-Emailit-Signature. Owlat computes the lowercase hexadecimal HMAC-SHA256 of <timestamp>.<raw body> with the full EMAILIT_WEBHOOK_SECRET and rejects timestamps outside five minutes. An unset secret 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.