Skip to content

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.td 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 /api/user/webhooks

Request ​

bash
curl -X POST https://api.mail.td/api/user/webhooks \
  -H "Authorization: Bearer td_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 /api/user/webhooks

Request ​

bash
curl https://api.mail.td/api/user/webhooks \
  -H "Authorization: Bearer td_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 /api/user/webhooks/{id}

Request ​

bash
curl -X DELETE https://api.mail.td/api/user/webhooks/w1e2b3h4-... \
  -H "Authorization: Bearer td_xxxxxxxxxxxx"

Response 204 ​

No content.


Rotate Webhook Secret ​

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

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

Request ​

bash
curl -X POST https://api.mail.td/api/user/webhooks/w1e2b3h4-.../rotate \
  -H "Authorization: Bearer td_xxxxxxxxxxxx"

Response 200 ​

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

List Deliveries ​

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

View recent delivery attempts for debugging.

Request ​

bash
curl https://api.mail.td/api/user/webhooks/w1e2b3h4-.../deliveries \
  -H "Authorization: Bearer td_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.td API Documentation