Webhooks
Truncus stuurt bezorgevenementen in realtime naar je endpoint.
Event-formaat
{
"event_id": "<event id>",
"event_type": "delivered",
"timestamp": "2026-10-17T09:30:00.000Z",
"email": {
"id": "<email id>",
"to": "user@example.com",
"subject": "Welcome!",
"status": "delivered"
},
"data": {}
}
event_type is het type event, bijvoorbeeld delivered, bounce, complaint of failed. email.status is de status van de e-mail op het moment dat de webhook werd verstuurd. data bevat de details van het event en hangt af van het type. Heb je voor een endpoint eventtypes gekozen, dan krijgt het alleen die.
Events worden verstuurd als HTTPS POST-verzoeken met Content-Type: application/json.
Request-headers
Content-Type: application/json
X-Truncus-Signature: <64 hex characters>
X-Truncus-Event-Id: <event id>
X-Truncus-Delivery-Id: <delivery id>
Test-events die je in het dashboard met „Send test event” verstuurt, hebben X-Truncus-Test: true in plaats van X-Truncus-Delivery-Id, en "test": true in de body.
Je endpoint moet binnen 30 seconden antwoorden met een HTTP 2xx-status. Elk ander antwoord, of een timeout, triggert een retry.
Handtekening verifiëren
Alle webhook-verzoeken zijn ondertekend met HMAC-SHA256 en het signing secret van je endpoint. Verifieer elk inkomend verzoek.
De header X-Truncus-Signature bevat de hex-gecodeerde HMAC-SHA256 van de ruwe request body. Er is geen timestamp en geen prefix:
hex(HMAC_SHA256(webhook_secret, raw_body))
Node.js-verificatie:
import crypto from 'crypto'
function verifyWebhookSignature(
secret: string,
rawBody: string,
signature: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex')
const a = Buffer.from(signature, 'hex')
const b = Buffer.from(expected, 'hex')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
Gebruik timingSafeEqual om timing-aanvallen te voorkomen. Wijs het verzoek af als de handtekening niet overeenkomt.
Events dedupliceren
Een retry stuurt hetzelfde event opnieuw met dezelfde X-Truncus-Event-Id. Bewaar de event-ID's die je hebt verwerkt en sla events over die je al hebt gezien:
const eventId = req.headers['x-truncus-event-id'] as string
if (await alreadyProcessed(eventId)) {
return res.status(200).send('ok')
}
// ... handle the event, then:
await markProcessed(eventId)
Volledig handler-voorbeeld
import express from 'express'
import crypto from 'crypto'
const app = express()
app.use(express.raw({ type: 'application/json' }))
app.post('/webhooks/truncus', async (req, res) => {
const signature = (req.headers['x-truncus-signature'] as string) ?? ''
const eventId = req.headers['x-truncus-event-id'] as string
const rawBody = req.body.toString('utf8')
// Handtekening verifiëren
const expected = crypto
.createHmac('sha256', process.env.TRUNCUS_WEBHOOK_SECRET!)
.update(rawBody, 'utf8')
.digest('hex')
const a = Buffer.from(signature, 'hex')
const b = Buffer.from(expected, 'hex')
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Ongeldige handtekening')
}
// Dedupliceren: een retry heeft dezelfde event-ID
if (await alreadyProcessed(eventId)) {
return res.status(200).send('ok')
}
const event = JSON.parse(rawBody)
if (event.event_type === 'bounce') {
// bezorgingsrecord bijwerken — event.data bevat details
}
if (event.event_type === 'complaint') {
// adres onderdrukken — niet opnieuw versturen
}
await markProcessed(eventId)
res.status(200).send('ok')
})
Belangrijk: parsen als raw bytes
Parseer de request body als raw bytes vóór JSON-parsing. De handtekeningverificatie moet op de exacte ontvangen bytes plaatsvinden.
Retry-schema
Geeft je endpoint geen 2xx-antwoord, dan probeert Truncus het opnieuw. Elke vertraging telt vanaf de vorige poging:
| Poging | Vertraging |
|---|---|
| 1 | Direct |
| 2 | 5 minuten |
| 3 | 30 minuten |
| 4 | 2 uur |
| 5 | 24 uur |
Na de vijfde mislukte poging wordt de bezorging als mislukt gemarkeerd, ongeveer 26,5 uur na de eerste poging. Een mislukte bezorging kun je vanuit het dashboard opnieuw proberen. Events blijven onbeperkt herhaalbaar.
Handmatige replay
Om de opgeslagen events van een e-mail opnieuw naar je webhooks te sturen, roep je het sandbox-replay-endpoint aan met een API-key met de scope send:
POST /api/v1/sandbox/replay
Content-Type: application/json
{ "source_message_id": "<email id>" }
Truncus maakt een sandbox-kopie van de e-mail en stuurt elk van de opgeslagen events opnieuw naar je webhooks. De e-mail wordt niet opnieuw verstuurd. Herhaalde events krijgen nieuwe event-ID's en hebben "replay": true in data.