Receive inbound emails as webhooks (IMAP to JSON)

Updated on

Smapiz keeps an IMAP connection open on each mailbox. When an email arrives, it is parsed and sent as a POST to your URL, usually a few seconds after it was received.

Create the endpoint

  1. Open Webhooks, then Add endpoint.
  2. Enter a publicly reachable HTTPS URL.
  3. Tick the events you want. For inbound emails: message.received.
  4. Keep All mailboxes or select specific mailboxes.
  5. Save and copy the secret (whsec_…): it is used to verify the signature.

The Send test button sends a test event to validate your endpoint.

Events

  • message.received: a new email arrived.
  • message.sent: an email was accepted by the SMTP server.
  • message.failed: an email could not be sent after several attempts.
  • message.bounced: a bounce was received for a sent email.
  • mailbox.error / mailbox.recovered: a mailbox lost, then regained, its connection.

What you receive

{
  "id": "evt_...",
  "object": "event",
  "type": "message.received",
  "created_at": "2026-10-12T08:30:00.000Z",
  "data": {
    "message": {
      "id": "msg_...",
      "mailbox_id": "mbx_...",
      "thread_id": "msg_...",
      "from": { "name": "Supplier", "address": "[email protected]" },
      "to": [{ "address": "[email protected]" }],
      "subject": "Invoice F-2026-118",
      "text": "Please find attached…",
      "html": "<p>Please find attached…</p>",
      "attachments": [{ "index": 0, "filename": "F-2026-118.pdf", "url": "https://smapiz.com/v1/messages/msg_.../attachments/0" }],
      "is_auto_reply": false
    }
  }
}
  • Attachments are downloaded from their url, with your API key.
  • is_auto_reply flags automatic replies (out-of-office, etc.) so you don’t answer them.
  • To leave out the text and HTML, turn off Include text & HTML bodies in payloads and fetch the message with GET /v1/messages/{id}.

Verify the signature

Each request carries a Smapiz-Signature: t=…,v1=… header. v1 is an HMAC-SHA256 of "<t>.<raw body>" with your secret. Reject the request if the signature does not match or if t is more than 5 minutes old.

import crypto from 'node:crypto';

app.post('/hooks/email', express.raw({ type: 'application/json' }), (req, res) => {
  const [t, v1] = req.header('smapiz-signature').split(',').map((p) => p.split('=')[1]);
  const expected = crypto.createHmac('sha256', process.env.SMAPIZ_WEBHOOK_SECRET)
    .update(`${t}.${req.body}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  if (!fresh || v1?.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  res.sendStatus(200); // respond fast, process afterwards
});

Compute the signature on the raw body, before any JSON parsing.

Errors and retries

  • Any 2xx response is a success. Otherwise Smapiz retries for about 36 hours, at increasing intervals.
  • A 410 response or repeated failures disable the endpoint.
  • The Recent deliveries list shows every attempt; the Redeliver button sends a delivery again.
  • Rotate generates a new secret; the old one stops working immediately.

Without a public server, you can also read events with GET /v1/events.

To answer a received email, see Reply in the same thread.

Next guideReply to an email over the API in the same thread