Maildesk Webhook Integration Guide#
This guide shows how to receive, verify, and process Maildesk webhooks safely in your application.1. Understand what Maildesk sends#
Maildesk sends webhook requests as:Content-Type: application/json
Success response expected from your server: 200, 201, or 202
Maildesk signs each request with:X-Maildesk-Timestamp: <unix_timestamp_seconds>
X-Maildesk-Signature: t=<unix_timestamp_seconds>,v1=<hex_hmac_sha256>
${timestamp}.${raw_json_body}
key: your Maildesk API key secret
2. Create a webhook endpoint in your app#
Create an HTTPS route that accepts POST requests.POST /api/webhooks/maildesk
Read the raw request body bytes before JSON parsing for signature verification.
Return 200, 201, or 202 quickly after validation and enqueue heavy work asynchronously.
3. Verify the webhook signature#
1.
Read X-Maildesk-Timestamp and X-Maildesk-Signature.
2.
Rebuild the signed payload as "<timestamp>.<raw_body>".
3.
Compute HMAC-SHA256 with your Maildesk secret.
4.
Compare to v1 using constant-time comparison.
5.
Reject old timestamps (recommended: older than 5 minutes) to reduce replay risk.
Node.js (Express) example#
import crypto from 'crypto'
import express from 'express'
const app = express()
// Keep raw bytes for exact signature verification
app.use('/api/webhooks/maildesk', express.raw({ type: 'application/json' }))
function verifyMaildeskSignature(rawBody: Buffer, timestamp: string, signatureHeader: string, secret: string) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(kv => {
const [k, v] = kv.split('=')
return [k?.trim(), v?.trim()]
}),
)
const v1 = parts.v1
const t = parts.t
if (!v1 || !t || t !== timestamp) return false
const payload = `${timestamp}.${rawBody.toString('utf8')}`
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex')
try {
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'))
} catch {
return false
}
}
app.post('/api/webhooks/maildesk', (req, res) => {
const timestamp = req.header('X-Maildesk-Timestamp')
const sig = req.header('X-Maildesk-Signature')
if (!timestamp || !sig) {
return res.status(401).send('Missing Maildesk signature headers')
}
const maxSkewSeconds = 5 * 60
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - Number(timestamp)) > maxSkewSeconds) {
return res.status(401).send('Stale timestamp')
}
const ok = verifyMaildeskSignature(req.body as Buffer, timestamp, sig, process.env.MAILDESK_WEBHOOK_SECRET!)
if (!ok) {
return res.status(401).send('Invalid signature')
}
const event = JSON.parse((req.body as Buffer).toString('utf8'))
// TODO: idempotency + async processing
// queue.publish(event)
return res.status(200).send('ok')
})
Python (Flask) example#
4. Implement idempotency with eventId#
Each webhook payload includes a unique eventId.Use it as an idempotency key:1.
Store processed eventId in durable storage.
2.
If the same eventId is received again, skip side effects and return success.
This is required because Maildesk retries failed deliveries.5. Handle Maildesk event payloads#
Current webhook payload format:{
"type": "subscriber.confirmed",
"eventId": "01HV...",
"id": "contact-id",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"status": "subscribed",
"createdAt": "2026-04-21T07:30:00.000Z"
}
Currently produced contact-related event types:Recommended handler pattern:2.
Validate required fields.
3.
Queue internal processing.
4.
Return 200 immediately when accepted.
6. Prepare for retries and backoff#
If your endpoint does not return a successful response, Maildesk retries with backoff:After max retries, the event is moved to Maildesk dead-letter flow.Integration implications:Your endpoint must be idempotent.
Avoid long synchronous processing.
Return 200, 201, or 202 only after basic validation and queuing.
7. Security hardening checklist#
Verify HMAC signature for every request.
Reject stale timestamps (for replay protection).
Use constant-time signature comparison.
Rotate webhook secret safely (support overlap window during rotation).
Log eventId, type, and verification outcome (never log full secret).
8. Test end-to-end#
1.
Configure your webhook URL in Maildesk settings.
2.
Trigger a supported event (subscriber.confirmed or subscriber.unsubscribed).
3.
Confirm your endpoint receives and verifies the signature.
4.
Confirm duplicate delivery of the same eventId is ignored safely.
5.
Confirm failures are retried and eventually succeed after recovery.
9. Common integration pitfalls#
Verifying signature on parsed/reformatted JSON instead of raw body.
Returning 204 No Content (Maildesk success handling expects 200, 201, or 202).
Missing idempotency guard for retried events.
Doing heavy downstream calls before returning success.
If you want, this can be extended with framework-specific templates (NestJS, Django, FastAPI, Laravel, Rails) using the same signing and idempotency rules.Modified at 2026-04-21 06:29:34