mailvina
DocsWebhooks ▾

Guides

Webhooks

Register an HTTPS endpoint and choose which events to receive. Each delivery is a JSON POST.

Events

EventMeaning
email.sentAccepted by Mailvina and queued
email.scheduledAccepted for future delivery
email.deliveredAccepted by the recipient’s mail server
email.delivery_delayedTemporary failure, retrying
email.bouncedPermanently rejected
email.complainedRecipient marked it as spam
email.opened / email.clickedEngagement tracking
email.suppressedSkipped because the address is suppressed
domain.*, contact.*Resource changes

Payload

{
  "type": "email.bounced",
  "created_at": "2026-11-14T10:00:00.000Z",
  "data": {
    "email_id": "49a3999c-…",
    "from": "Acme <[email protected]>",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome",
    "recipient": "[email protected]",
    "bounced": {
      "type": "permanent",
      "message": "550 5.1.1 Mailbox does not exist"
    }
  }
}

Verify signatures

Each request carries mailvina-id, mailvina-timestamp and mailvina-signature. The signature is v1, followed by the base64 HMAC-SHA256 of {id}.{timestamp}.{body} using your signing secret (decoded from base64 after the whsec_ prefix).

import crypto from 'node:crypto';

export function verify(rawBody, headers, secret) {
  const id = headers['mailvina-id'];
  const ts = headers['mailvina-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay protection
  const key = Buffer.from(secret.replace('whsec_', ''), 'base64');
  const expected = 'v1,' + crypto.createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest('base64');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(headers['mailvina-signature']));
}

Retries

Respond with any 2xx within 10 seconds. Failed deliveries retry after 30s, 5m, 30m, 2h, 5h and 10h.