Inbound Webhooks
Receive every email sent to your domain as a JSON POST to your endpoint.
How It Works
When inbound email processing is enabled for your domain, GetMailer receives mail addressed to your domain, parses it, and delivers each accepted email to your webhook URL as an HTTP POST with a JSON body. The email is also stored in your GetMailer inbox, so your endpoint never has to be the only copy.
Inbound webhooks are separate from event webhooks, which notify you about the delivery status of emails you send. Inbound webhooks carry the full content of emails you receive.
Setup
- Go to your Domains dashboard and open your domain
- Enable inbound email processing and complete the DNS setup (see Inbound Email)
- Enter your webhook URL in the Webhook section (must be
http://orhttps://— HTTPS strongly recommended) - Save your changes
Leaving the webhook URL empty stores inbound emails in your GetMailer inbox without webhook delivery. Each domain has its own signing secret used to sign webhook requests; the secret is never exposed in API responses. Contact support if you need your signing secret issued or rotated.
Request Headers
Every inbound webhook request includes these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-GetMailer-Signature | t=<unix-seconds>,v1=<hex HMAC-SHA256> |
X-GetMailer-Event | inbound.received |
inbound.received is currently the only inbound event type.
Payload
The request body is a JSON object describing the received email:
{
"id": "cmb1x2y3z0001abc123def456",
"from": "Jane Doe <jane@example.com>",
"to": ["support@yourdomain.com"],
"cc": [],
"subject": "Help with my order",
"html": "<p>Hi, I need help with order #1234.</p>",
"text": "Hi, I need help with order #1234.",
"headers": {
"message-id": "<CABc123@mail.example.com>",
"date": "Fri, 7 Aug 2026 10:15:00 +0000",
"dkim-signature": "v=1; a=rsa-sha256; ..."
},
"attachments": [
{
"filename": "invoice.pdf",
"contentType": "application/pdf",
"size": 48213,
"storageKey": "inbound/attachments/cmb1x2y3z/invoice.pdf"
}
],
"receivedAt": "2026-08-07T10:15:03.412Z"
}| Field | Type | Description |
|---|---|---|
id | string | Unique ID of the stored inbound email |
from | string | Sender address, may include a display name |
to | string[] | Recipient addresses |
cc | string[] | CC addresses (empty array if none) |
subject | string | Email subject line |
html | string | null | HTML body, if present |
text | string | null | Plain-text body, if present |
headers | object | Parsed email headers as key-value pairs |
attachments | array | Attachment metadata (see below); binary content is not inlined |
receivedAt | string | ISO 8601 timestamp when the email was received |
Verifying Signatures
Always verify the signature before trusting a payload. The X-GetMailer-Signature header contains a Unix timestamp and an HMAC-SHA256 hex digest:
X-GetMailer-Signature: t=1754561703,v1=5f8a2c...The signature is computed over the string {timestamp}.{rawBody} using your domain's signing secret as the HMAC key, where rawBody is the exact raw request body. Verify against the raw bytes — do not re-serialize parsed JSON, as key ordering or whitespace differences will break verification. Reject requests whose timestamp is older than 5 minutes to protect against replay attacks.
Node.js
const crypto = require("crypto");
function verifyGetMailerSignature(rawBody, signatureHeader, secret, maxAgeSeconds = 300) {
const parts = signatureHeader.split(",");
const t = parts.find((p) => p.startsWith("t="))?.slice(2);
const v1 = parts.find((p) => p.startsWith("v1="))?.slice(3);
if (!t || !v1) return false;
const ts = parseInt(t, 10);
const now = Math.floor(Date.now() / 1000);
if (isNaN(ts) || now - ts > maxAgeSeconds) return false; // replay protection
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
try {
return crypto.timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"));
} catch {
return false;
}
}
// Express: read the raw body, verify, then parse
app.post(
"/webhooks/inbound",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body.toString("utf8");
const ok = verifyGetMailerSignature(
rawBody,
req.get("X-GetMailer-Signature") || "",
process.env.GETMAILER_INBOUND_SECRET
);
if (!ok) {
return res.status(401).send("Invalid signature");
}
const email = JSON.parse(rawBody);
// Queue for async processing, then acknowledge
res.status(200).send("OK");
}
);Python
import hashlib
import hmac
import time
def verify_getmailer_signature(raw_body: bytes, signature_header: str, secret: str, max_age_seconds: int = 300) -> bool:
parts = dict(
p.split("=", 1) for p in signature_header.split(",") if "=" in p
)
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1:
return False
try:
ts = int(t)
except ValueError:
return False
if time.time() - ts > max_age_seconds:
return False # replay protection
signed_payload = t.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(v1, expected)Attachments
Attachment binaries are not included in the webhook payload. Each attachment appears as a metadata object:
{
"filename": "invoice.pdf",
"contentType": "application/pdf",
"size": 48213,
"storageKey": "inbound/attachments/cmb1x2y3z/invoice.pdf"
}filename— original file name from the emailcontentType— MIME type of the attachmentsize— size in bytesstorageKey— identifier of the stored file in GetMailer
Attachment files are stored securely by GetMailer and can be downloaded from the email in your dashboard inbox, where downloads are served via short-lived signed URLs (valid for 1 hour).
Delivery Behavior
- Clean mail only — emails classified as spam, quarantined, rejected, or flagged as containing a virus never trigger a webhook
- Duplicate suppression — an email with the same Message-ID received again within 7 days is not delivered a second time
- 30-second timeout — your endpoint must respond within 30 seconds
- No automatic retries — each email gets a single delivery attempt; if your endpoint returns a non-2xx status, times out, or is unreachable, the webhook is not retried
- Emails are never lost — whether or not webhook delivery succeeds, the email is stored in your GetMailer inbox, and failed deliveries are marked so you can spot them
Because there are no retries, keep your endpoint highly available and treat your GetMailer inbox as the source of truth for anything your endpoint may have missed.
Responding to Webhooks
- Return a
2xxstatus as quickly as possible — well within the 30-second timeout - Process asynchronously — persist or queue the payload first, then acknowledge; do heavy work (parsing, AI, database writes) outside the request cycle
- Verify before processing — reject requests with a missing or invalid signature with a
401 - Key on the email ID — use the payload's
idto keep your processing idempotent