Skip to content

Pro Webhooks

Receive HTTP POST callbacks when emails arrive. No polling required.

How Webhooks Work

  1. You register a webhook URL and select events
  2. When an event occurs, Mail.cx sends an HTTP POST to your URL
  3. Your server responds with 2xx to acknowledge receipt
  4. Failed deliveries are retried with exponential backoff

Webhook Payload

When an email.received event fires, the payload looks like:

json
{
  "id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "email.received",
  "created_at": "2025-01-15T10:30:00Z",
  "data": {
    "email_id": "f5e6d7c8-a1b2-3c4d-e5f6-789012345678",
    "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "address": "info@yourdomain.com",
    "from": "Example <noreply@example.com>",
    "sender": "noreply@example.com",
    "subject": "Verify your email",
    "preview_text": "Click the link below to verify...",
    "size": 4523,
    "created_at": "2025-01-15T10:30:00Z"
  }
}

Signature Verification

Each webhook request includes these headers:

HeaderDescription
X-Webhook-IDUnique event ID (e.g. evt_...)
X-Webhook-TimestampUnix timestamp (seconds)
X-Webhook-SignatureHMAC-SHA256 signature: sha256=...

The signature is computed over timestamp + "." + body using your webhook secret:

javascript
const crypto = require('crypto');

function verifySignature(body, timestamp, signature, secret) {
  const payload = timestamp + '.' + body;
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret).update(payload).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Usage in Express:
app.post('/webhook', (req, res) => {
  const body = req.rawBody; // raw string
  const timestamp = req.headers['x-webhook-timestamp'];
  const signature = req.headers['x-webhook-signature'];

  if (!verifySignature(body, timestamp, signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  // Process event...
  res.sendStatus(200);
});
python
import hmac, hashlib

def verify_signature(body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    payload = timestamp.encode() + b'.' + body
    expected = 'sha256=' + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Create a Webhook

POST /pro/api/webhooks

Request

bash
curl -X POST https://api.mail.cx/pro/api/webhooks \
  -H "Authorization: Bearer tm_pro_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://yourapp.com/webhook","events":["email.received"]}'
FieldTypeRequiredDescription
urlstringYesHTTPS URL to receive callbacks
eventsstring[]YesEvents to subscribe to

Available Events

EventDescription
email.receivedNew email delivered to a mailbox

Response 201

json
{
  "id": "w1e2b3h4-o5o6-k7i8-d9e0-abcdef123456",
  "url": "https://yourapp.com/webhook",
  "events": ["email.received"],
  "secret": "whsec_aBcDeFgHiJkLmNoPqRsTuVwXyZ",
  "status": "active",
  "created_at": "2025-01-15T10:00:00Z"
}

WARNING

The secret is only returned when the webhook is created. Save it securely — you'll need it to verify signatures.

Errors

StatusError CodeDescription
400invalid_urlURL must be HTTPS
403webhook_limit_reachedWebhook limit reached

List Webhooks

GET /pro/api/webhooks

Request

bash
curl https://api.mail.cx/pro/api/webhooks \
  -H "Authorization: Bearer tm_pro_xxxxxxxxxxxx"

Response 200

json
{
  "webhooks": [
    {
      "id": "w1e2b3h4-o5o6-k7i8-d9e0-abcdef123456",
      "url": "https://yourapp.com/webhook",
      "events": ["email.received"],
      "status": "active",
      "failure_count": 0,
      "last_triggered_at": "2025-01-15T10:30:00Z",
      "created_at": "2025-01-15T10:00:00Z"
    }
  ]
}
FieldTypeDescription
statusstringactive or disabled
failure_countintegerConsecutive delivery failures
last_triggered_atstring?Last successful delivery timestamp

INFO

Webhooks are automatically disabled after 10 consecutive failures. Delete and re-create to re-enable.


Delete a Webhook

DELETE /pro/api/webhooks/{id}

Request

bash
curl -X DELETE https://api.mail.cx/pro/api/webhooks/w1e2b3h4-... \
  -H "Authorization: Bearer tm_pro_xxxxxxxxxxxx"

Response 204

No content.


Rotate Webhook Secret

POST /pro/api/webhooks/{id}/rotate

Generate a new signing secret. The old secret is invalidated immediately.

Request

bash
curl -X POST https://api.mail.cx/pro/api/webhooks/w1e2b3h4-.../rotate \
  -H "Authorization: Bearer tm_pro_xxxxxxxxxxxx"

Response 200

json
{
  "id": "w1e2b3h4-o5o6-k7i8-d9e0-abcdef123456",
  "secret": "whsec_NewSecretKeyHere123456"
}

List Deliveries

GET /pro/api/webhooks/{id}/deliveries

View recent delivery attempts for debugging.

Request

bash
curl https://api.mail.cx/pro/api/webhooks/w1e2b3h4-.../deliveries \
  -H "Authorization: Bearer tm_pro_xxxxxxxxxxxx"

Response 200

json
{
  "deliveries": [
    {
      "id": "d1e2l3i4-v5e6-r7y8-9012-abcdef345678",
      "event_type": "email.received",
      "event_id": "evt_abc123",
      "status_code": 200,
      "error": null,
      "attempt": 1,
      "duration_ms": 142,
      "created_at": "2025-01-15T10:30:01Z"
    },
    {
      "id": "d2e3l4i5-v6e7-r8y9-0123-bcdef4567890",
      "event_type": "email.received",
      "event_id": "evt_def456",
      "status_code": 0,
      "error": "connection timeout",
      "attempt": 3,
      "duration_ms": 30000,
      "created_at": "2025-01-15T11:00:05Z"
    }
  ]
}
FieldTypeDescription
event_typestringEvent that triggered the delivery
event_idstringUnique event identifier
status_codeintegerHTTP response code (0 if connection failed)
errorstring?Error message (null if successful)
attemptintegerAttempt number (1 = first try)
duration_msintegerRequest duration in milliseconds

Mail.cx API Documentation