Webhooks Guide

Webhooks push new messages to your server the moment they arrive — no polling required. Register a URL and Mailto.Bot will deliver a JSON payload for each inbound message.

Last updated: March 25, 2026

Overview

Each mailbox can have one or more webhooks. When a message arrives, Mailto.Bot sends an HTTP POST to each registered URL with the full message payload as JSON.

  • Delivery is near-instant — typically under 500 ms from message receipt.
  • Failed deliveries are retried with exponential back-off (see Retry behaviour).
  • Your endpoint must respond with a 2xx status within 10 seconds.
  • Multiple webhooks on the same mailbox each receive their own delivery.

Registration

Register a webhook with POST /api/mailboxes/:name/webhooks. You can use HTTPS or HTTP (HTTP is allowed for local development).

cURL
curl -X POST \
  https://mailto.bot/api/mailboxes/abc123/webhooks \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-server.com/webhook" }'
Save the secret immediately. The secret field is returned only once at registration time. Use it to verify that incoming webhook requests are genuinely from Mailto.Bot — see the verification section below.

You can list or delete webhooks from the API Reference or the Dashboard.

Payload format

Mailto.Bot sends a Content-Type: application/json POST to your URL with the following body:

JSON
{
  "event": "message.received",
  "mailbox": "abc123",
  "message": {
    "id": "msg_01HXZ...",
    "from": "sender@example.com",
    "to": ["abc123@mailto.bot"],
    "subject": "Hello from the webhook",
    "text": "Plain-text body here.",
    "html": "<p>Plain-text body here.</p>",
    "attachments": [],
    "receivedAt": "2026-04-15T20:00:00.000Z"
  }
}
FieldTypeDescription
eventstringAlways "message.received" currently.
mailboxstringSlug of the receiving mailbox.
message.idstringUnique message ID.
message.fromstringSender email address.
message.tostring[]Recipient addresses.
message.subjectstringSubject line.
message.textstringPlain-text body (may be empty).
message.htmlstringHTML body (may be empty).
message.attachmentsobject[]Attachment metadata (filename, contentType, size, url).
message.receivedAtstringISO 8601 receipt timestamp.

Handling events

Your endpoint should acknowledge receipt immediately and process the event asynchronously. If processing takes more than 10 seconds, Mailto.Bot will consider the delivery a failure and retry.

JavaScript
// Express.js example
app.post('/webhook', express.json(), (req, res) => {
  const { event, mailbox, message } = req.body;

  if (event === 'message.received') {
    console.log('New message in', mailbox, ':', message.subject);
    // Process message...
  }

  // Always respond 2xx quickly
  res.sendStatus(200);
});
Idempotency. Due to retries, your handler may receive the same event more than once. Use the message.id field as an idempotency key to deduplicate deliveries.

Retry behaviour

If your endpoint returns a non-2xx response or times out, Mailto.Bot retries the delivery with exponential back-off:

AttemptDelay after failure
1st retry30 seconds
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
Final attempt6 hours — after this, delivery is abandoned

Webhook failures are visible in the Dashboard under your mailbox settings. You can re-enable a paused webhook by editing it in the Dashboard.

Testing with ngrok

During development your server runs on localhost, which Mailto.Bot cannot reach. Use ngrok to expose a public tunnel to your local server.

Shell
# Install ngrok (https://ngrok.com) then:
ngrok http 3000

# ngrok outputs a public URL like:
# Forwarding  https://abc123.ngrok.io -> http://localhost:3000

# Register that URL as your webhook:
curl -X POST \
  https://mailto.bot/api/mailboxes/abc123/webhooks \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://abc123.ngrok.io/webhook" }'

The ngrok URL changes each time you restart. For persistent tunnels during active development, consider a fixed subdomain on a paid ngrok plan or an alternative like localtunnel.

Continue reading