Webhook-Handler
Verarbeiten Sie Owlats Zustell-Webhooks mit Signaturprüfung und Event-Routing.
Verarbeiten Sie Owlats Zustell-Webhooks mit Signaturprüfung und Event-Routing.
Voraussetzungen
- Ein unter Einstellungen → Webhooks konfigurierter Webhook mit einem
whsec_...-Secret - Ein öffentlich erreichbarer Endpunkt, der
POST-Anfragen annimmt
Vollständiger Express-Handler
import express from 'express'
import crypto from 'crypto'
const app = express()
// Use raw body for signature verification
app.post('/webhooks/owlat', express.raw({ type: 'application/json' }), async (req, res) => {
const payload = req.body.toString()
const signature = req.headers['x-signature'] as string
const timestamp = req.headers['x-timestamp'] as string
// 1. Verify signature
if (!verifySignature(payload, signature, process.env.OWLAT_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' })
}
// 2. Check timestamp freshness (reject requests older than 5 minutes)
const age = Math.abs(Date.now() / 1000 - Number(timestamp))
if (age > 300) {
return res.status(401).json({ error: 'Request too old' })
}
// 3. Return 200 immediately — process async
res.status(200).json({ received: true })
// 4. Route events
const event = JSON.parse(payload)
await handleEvent(event)
})
function verifySignature(payload: string, signature: string, secret: string): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
const sigBuf = Buffer.from(signature, 'hex')
const expBuf = Buffer.from(expected, 'hex')
// timingSafeEqual throws on length mismatch — guard against malformed signatures
if (sigBuf.length !== expBuf.length) return false
return crypto.timingSafeEqual(sigBuf, expBuf)
}
Owlat erwartet eine Antwort innerhalb von 30 Sekunden. Geben Sie 200 zurück, sobald Sie die Signatur geprüft haben, und verarbeiten Sie das Event anschließend asynchron. Das verhindert Wiederholungsversuche bei langsamen Handlern.
Event-Routing
Routen Sie Events nach Typ über ein switch/case:
interface WebhookEvent {
event: string
timestamp: string
data: Record<string, unknown>
}
async function handleEvent(event: WebhookEvent) {
switch (event.event) {
case 'email.delivered':
console.log(`Delivered to ${event.data.email}`)
// Outbound payloads key only on the recipient email — correlate by
// email (and the event's own data.timestamp) rather than a message ID.
await db.users.update({
where: { email: event.data.email as string },
data: { lastDeliveredAt: event.data.timestamp as string },
})
break
case 'email.bounced':
console.log(`Bounced: ${event.data.email}`)
// Mark the contact as undeliverable in your system
await db.users.update({
where: { email: event.data.email as string },
data: { emailStatus: 'bounced' },
})
break
case 'email.opened':
console.log(`Opened by ${event.data.email}`)
await db.users.update({
where: { email: event.data.email as string },
data: { lastOpenedAt: event.data.timestamp as string },
})
break
case 'email.clicked':
console.log(`Click from ${event.data.email}: ${event.data.url}`)
await db.clickEvents.create({
data: {
email: event.data.email as string,
url: event.data.url as string,
clickedAt: event.data.timestamp as string,
},
})
break
case 'email.complained':
console.log(`Complaint from ${event.data.email}`)
await db.users.update({
where: { email: event.data.email as string },
data: { emailStatus: 'complained', suppressEmail: true },
})
break
default:
console.log(`Unhandled event: ${event.event}`)
}
}
Ausgehende Payloads enthalten keine Message-ID. Die data-Map ist ein flacher Satz primitiver Werte, adressiert über die Empfängeradresse email (plus url bei Klicks), und die data jedes Events trägt ihren eigenen ISO-8601-timestamp. Ordnen Sie Events Ihren eigenen Datensätzen über E-Mail-Adresse und Zeitstempel zu.
Bounce-Behandlung
Bounces sind das wichtigste zu verarbeitende Event. Hard Bounces bedeuten, dass die Adresse dauerhaft ungültig ist — weiterhin dorthin zu senden schadet Ihrer Absenderreputation:
case 'email.bounced':
const { email, bounceType, message } = event.data as {
email: string
bounceType: 'hard' | 'soft'
message: string // provider-supplied reason, '' when none given
timestamp: string
}
if (bounceType === 'hard') {
// Permanently suppress this address
await db.users.update({
where: { email },
data: { emailStatus: 'hard_bounced', suppressEmail: true, bounceReason: message },
})
} else {
// Soft bounce — log it, but don't suppress yet
await db.bounceLog.create({
data: { email, type: 'soft', reason: message, timestamp: event.data.timestamp },
})
}
break
Owlat nimmt hart gebouncte Adressen automatisch in Ihre Blockliste auf. Der Webhook erlaubt es Ihnen, diesen Zustand in Ihrer eigenen Datenbank zu spiegeln, damit Sie Sendungen an diese Adressen gar nicht erst auslösen.
Events, die Owlat auslöst
Owlat sendet diese acht ausgehenden Events. Abonnieren Sie pro Webhook diejenigen, die für Sie relevant sind:
| Event | data-Felder |
|---|---|
email.sent | email, campaignId (oder null), transactionalEmailId (oder null), timestamp |
email.delivered | email, timestamp |
email.opened | email, timestamp |
email.clicked | email, url, timestamp |
email.bounced | email, bounceType (hard/soft), message, timestamp |
email.complained | email, timestamp |
contact.created | contactId, email, source, timestamp |
topic.unsubscribed | contactId, email, unsubscribedAt, listsRemoved |
Die data-Map ist ein flacher Satz primitiver Werte, daher wird listsRemoved als JSON-kodierter String ausgeliefert, nicht als Array. Empfänger müssen JSON.parse(event.data.listsRemoved) aufrufen, um die Liste { topicId, topicName }[] zu erhalten. unsubscribedAt sind Epoch-Millisekunden (eine Zahl), anders als der ISO-8601-timestamp der E-Mail-Events.
Aufbau des Webhook-Payloads
Jede Webhook-Zustellung folgt diesem Umschlag. Beachten Sie, dass data zusätzlich zum timestamp des Umschlags einen eigenen ISO-8601-timestamp trägt:
{
"event": "email.delivered",
"timestamp": "2026-03-19T12:00:00.000Z",
"data": {
"email": "mira@acme.io",
"timestamp": "2026-03-19T12:00:00.000Z"
}
}
Bei jeder Zustellung enthaltene Header:
| Header | Beschreibung |
|---|---|
X-Signature | HMAC-SHA256-Hex-Digest des Payload-Bodys |
X-Timestamp | Unix-Zeitstempel (Sekunden) des Absendezeitpunkts der Anfrage |
X-Webhook-Id | Die ID der Webhook-Konfiguration |
Content-Type | application/json |
User-Agent | Owlat-Webhooks/1.0 |
Nächste Schritte
- Webhook-Payloads — der maßgebliche Payload-Contract je Event
- Referenz zur Webhooks-API — Konfiguration, Signierung und Retry-Verhalten
- Leitfaden zur Zustellbarkeit — Inbox-Platzierung und Reputation überwachen
- Rechnungs-E-Mail — die Zustellung von Zahlungsbestätigungen verfolgen