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.sent
  • email.delivered
  • email.opened
  • email.clicked
  • email.bounced
  • email.complained
  • contact.created
  • topic.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:

HeaderDescription
X-SignatureHMAC-SHA256 hex digest of the JSON payload body
X-TimestampUnix timestamp (seconds) when the request was sent
X-Webhook-IdThe webhook ID
Content-Typeapplication/json
User-AgentOwlat-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:

  1. Immediate
  2. After 1 minute
  3. 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:

RouteProviderWhen it's used
POST /webhooks/mtaBundled Owlat MTADefault for self-host
POST /webhooks/resendResendWhen 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:

EventEffect
bouncedRecords the bounce (hard/soft, pre-classified by the MTA) and updates blocklist + contact activity
complainedRecords the spam complaint and updates blocklist + contact activity
sentMarks the send as dispatched
inbound.receivedIngests inbound mail (e.g. campaign replies) into the team inbox
org.circuit_breakerTrips the sending circuit breaker on a high bounce rate
ip.blocklisted / ip.delisted / ip.warming_complete / all_ips_blockedSurfaces IP reputation changes

Requests are signed with HMAC-SHA256 over ${timestamp}.${body}:

HeaderDescription
X-MTA-SignatureHMAC-SHA256 hex digest
X-MTA-TimestampUnix 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:

EventEffect
email.bouncedRecords the bounce (classified hard/soft from the provider message) and updates blocklist + contact activity
email.complainedRecords 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.

No delivered/open/click side effects from the inbound path

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:

HeaderDescription
svix-idMessage ID
svix-timestampUnix timestamp (seconds), rejected if more than 5 minutes from server time
svix-signatureOne 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.