Three ways to get inbound mail
| Way | Best for | How |
|---|---|---|
| Webhook | Reacting at once: agents, tickets, order mail | One URL per mailbox, JSON, HMAC-SHA256 signed |
| REST API | Reading history, threads and attachments | GET /v1/messages, /v1/threads/:key, /v1/messages/:id/attachments/:n |
| IMAP | Existing IMAP code, and people | mail.usemektup.com:993, every folder |
The webhook
When a message is stored, Mektup posts this to your URL with an X-Mektup-Signature: sha256=… header, an HMAC-SHA256 of the raw body with your webhook secret:
{
"event": "message.received",
"mailbox": "orders@yourcompany.com",
"message": {
"id": "3f6d…",
"from": "customer@example.org",
"to": "orders@yourcompany.com",
"subject": "Order 1042: wrong size",
"text": "Hi, I received a medium but ordered a large…",
"receivedAt": "2026-10-23T09:14:02Z",
"threadKey": "<CAF…@mail.gmail.com>"
}
}The text body is included up to 20,000 characters; fetch the HTML and attachments with GET /v1/messages/:id. Mail from senders you have blocked never fires the webhook.
import hashlib, hmac, json, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["MEKTUP_WEBHOOK_SECRET"].encode()
@app.post("/inbound")
def inbound():
raw = request.get_data() # the exact bytes Mektup signed
expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
got = request.headers.get("X-Mektup-Signature", "").removeprefix("sha256=")
if not hmac.compare_digest(expected, got):
abort(401)
event = json.loads(raw)
handle(event["mailbox"], event["message"]) # your code
return "", 200Read with the REST API
# unread threads in a mailbox, newest first
curl "https://api.usemektup.com/v1/messages?mailbox=orders@yourcompany.com&direction=inbound&limit=50" \
-H "Authorization: Bearer $MEKTUP_API_KEY"
# the full message: text, html and the list of attachments
curl "https://api.usemektup.com/v1/messages/3f6d…" -H "Authorization: Bearer $MEKTUP_API_KEY"
# the first attachment, raw bytes
curl -o invoice.pdf "https://api.usemektup.com/v1/messages/3f6d…/attachments/0" \
-H "Authorization: Bearer $MEKTUP_API_KEY"Messages are grouped into threads from their Message-ID, In-Reply-To and References headers. Reading a message marks it read, like a mail client would; PATCH /v1/messages/:id marks it unread, flags it or moves it to a folder.
Compared with parse webhooks
SendGrid Inbound Parse, Postmark inbound, Mailgun routes and Resend inbound receive mail for your domain and post it to your endpoint. That is what you need when mail is only data for your code. Mektup also stores every message in a mailbox, which matters when:
- you need to read a message again, or the whole conversation, after your code handled it;
- a person has to see or answer the mail, in webmail or their own mail app;
- the same address must also send, and replies should come back to it;
- your endpoint was down and you want the mail anyway.
Side by side: SendGrid, Postmark, Mailgun, Resend.
Set it up
- Add the domain (or a subdomain such as
in.yourcompany.com, so your normal mail stays where it is) and add the records Mektup returns. - Create a mailbox for each address that should receive.
- Set the webhook URL per mailbox and keep the secret from the response.
- Verify the signature in your handler, answer within 8 seconds, then do the work.