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
- Open Webhooks, then Add endpoint.
- Enter a publicly reachable HTTPS URL.
- Tick the events you want. For inbound emails:
message.received. - Keep All mailboxes or select specific mailboxes.
- 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_replyflags 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
410response 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.