Register a webhook
Request
curl -X POST https://havitomail.com/api/v1/webhooks \
-H "Authorization: Bearer $HAVITO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://agent.acme.com/havito-webhook", "inbox_id": "8f2c1e0a-…"}'Leave out inbox_id to receive events for every inbox on the account. The response includes a secret, shown once — store it; you need it to verify signatures. List webhooks with GET /webhooks and remove one with DELETE /webhooks/{id}.
Event payload
Each new message produces one POST with a JSON body:
message.received
{
"event": "message.received",
"inbox_id": "8f2c1e0a-5b7d-4c11-9a3e-2d6f0b9c7e41",
"inbox": "support-agent@acme.com",
"message": {
"uid": 42,
"message_id": "<CAF3x9k2@mail.example.com>",
"from": { "name": "Sam Lee", "address": "sam@example.com" },
"to": [{ "name": "", "address": "support-agent@acme.com" }],
"subject": "Where is my refund?",
"date": "2026-10-10T09:14:03.000Z",
"text": "Hi, I returned the jacket last week…",
"attachments": []
}
}The payload carries the plain-text body. Fetch the full message (HTML, cc, attachments) with GET /inboxes/{inbox_id}/messages/{uid} when you need it.
Headers
X-Havito-Timestamp— Unix time (seconds) when the event was signed.X-Havito-Signature—sha256=followed by the hex HMAC-SHA256 of{timestamp}.{raw request body}, keyed with your webhook secret.
Verify in Node.js
Express
// Express — verify X-Havito-Signature before trusting the payload
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.HAVITO_WEBHOOK_SECRET;
app.post("/havito-webhook", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.header("X-Havito-Timestamp");
const sig = req.header("X-Havito-Signature") || "";
const expected = "sha256=" + crypto
.createHmac("sha256", SECRET)
.update(`${ts}.${req.body.toString("utf8")}`)
.digest("hex");
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300; // 5-minute window
if (!ok || !fresh) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
// hand event.message to your agent (queue it — respond fast)
res.status(200).end();
});Verify in Python
Flask
# Flask — verify X-Havito-Signature before trusting the payload
import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["HAVITO_WEBHOOK_SECRET"].encode()
@app.post("/havito-webhook")
def havito_webhook():
ts = request.headers.get("X-Havito-Timestamp", "")
sig = request.headers.get("X-Havito-Signature", "")
raw = request.get_data() # raw bytes, before JSON parsing
expected = "sha256=" + hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected) or abs(time.time() - int(ts or 0)) > 300:
abort(401)
event = request.get_json()
# hand event["message"] to your agent (queue it — respond fast)
return "", 200Always compute the HMAC over the raw body bytes, before any JSON parsing — re-serialising the JSON changes whitespace and breaks the signature. Reject events whose timestamp is more than a few minutes old to stop replays.
Delivery
- Events are delivered within about 30 seconds of a message arriving (new mail is detected by polling the mailbox).
- Respond with any 2xx status within 10 seconds — queue the work and let your agent process it asynchronously.
- Each event is attempted once; failed deliveries are not retried, so keep the polling fallback below. A webhook is switched off after 50 failures in a row (re-create it once your endpoint is healthy).
- Only new mail in INBOX triggers events, starting from when the webhook is created — existing mail is never replayed. Bodies over 20,000 characters are cut (text_truncated: true).
- Design your handler to be idempotent: use inbox_id + message.uid as the key, and ignore an event you have already processed.
- For a belt-and-braces setup, also poll
GET /inboxes/{id}/messages?unread=trueoccasionally and process anything the webhook path missed.
Next: wire the event into your agent with the reply endpoint, or let Claude handle it via the MCP server.