Inbound e-mail overview
Receive e-mail on your own domain: point its MX at wemail, route each message with a rule, then poll the mailbox or get it forwarded to your webhook.
wemail can be the mail exchanger for a domain you own. Every message delivered to that domain is parsed, checked for SPF / DKIM / DMARC, matched against your inbound routes, and then stored for the poll API (List inbound messages) and/or forwarded as a signed POST to a URL of yours. Replies to the mail you send land in the same place — the in-reply-to and references headers are preserved so you can thread them.
Set-up in three steps
- Enable inbound on a verified domain —
PUT /v3/domains/{name}/inbound {"enabled":true}(reference). The response appends the MX requirement todns_records:{"type":"MX","host":"<domain>","value":"10 inbound.wemail.io"}. - Publish the MX record
<domain>. MX 10 inbound.wemail.ioat your DNS provider — use a dedicated subdomain (e.g.in.example.com): on a domain that already receives mail (inbound.existing_mxnot empty) the MX change moves all of its mail to wemail — then call Verify a domain (or wait for the six-hourly re-check) untilinbound.okistrue. Mail is accepted as soon as the domain is verified and inbound is enabled — the MX flag only tells you whether the world can reach us. - Add routes (optional) — without a route every message is stored. Create an inbound route to forward some or all messages to your HTTPS endpoint, or to drop automated replies.
API keys and scopes
Reads — GET /v3/inbound/messages, GET /v3/inbound/messages/{id}, GET /v3/routes, GET /v3/routes/{id}, GET /v3/routes/{id}/deliveries — are allowed for `full_access` and `read_only` keys. A `send_only` key is denied on all of them (403 insufficient_scope): a sending credential must never read your inbound mail. Every write — PUT /v3/domains/{name}/inbound, route create / update / delete, DELETE /v3/inbound/messages/{id} — needs `full_access`. Keys are workspace-scoped: an id that belongs to another workspace answers 404, never a hint that it exists.
Route expressions
A route is one expression plus an ordered list of actions. Routes are evaluated in priority order (lower first, ties by creation time); the first matching route wins and its actions run in order. Exactly three forms are accepted — nothing else, no and / or, no single quotes, no trailing text:
match_recipient("<pattern>")— tested against the envelope recipient address (lower-cased).match_header("<name>", "<pattern>")— tested against the first value of that header (header names are case-insensitive, values are compared on their first 1 KB).catch_all()— matches every message.
A pattern without regex metacharacters (* + ? ^ $ ( ) [ ] { } | \) is a case-insensitive literal, matched in full — a bare . does not make it a regex, so match_recipient("kostas@leads.example.com") is a literal. Once any metacharacter is present the pattern is a JavaScript regular expression (flags iu, at most 256 characters, nested quantifiers such as (a+)+ are rejected) and . has its regex meaning — write \\. when you mean a literal dot (the expression string escapes a backslash as \\, so \\. reaches the regex as \.). Strings are double-quoted with \" and \\ escapes.
match_recipient("kostas@leads.example.com") literal
match_recipient(".*@leads\\.example\\.com") regex — any address on the domain
match_header("subject", "^Re:") regex on the subject
match_header("auto-submitted", "auto-replied") literal
catch_all()match_recipient('x') single quotes
match_recipient("a") and catch_all() no operators
catch_all missing parentheses
match_header("subject") two arguments required
match_recipient("") empty pattern
match_recipient("(a+)+quot;) nested quantifierRoute actions
{"type":"store"}— keep the message for the poll API (GET /v3/inbound/messages).{"type":"forward","url":"https://…","secret":"…"}— POST the message to a public HTTPS URL.secret(optional, 8–128 characters) turns on theApifon-SignatureHMAC header; it is stored encrypted and never returned (has_secret: true).{"type":"stop"}— ignore the remaining actions of this route.
Between one and five actions per route. A matching route without store leaves the message out of the poll API (routing-only; a forward-only or stop-only route therefore acts as forward-only or drop). No matching route is the same as [{"type":"store"}]. Malformed actions answer 422 invalid_actions; a forward URL that is not a public HTTPS address (private networks, localhost, link-local) answers 400 validation_error.
The forward contract
Each forward action sends POST <url> with Content-Type: application/json and the headers X-Wemail-Event: inbound.received, X-Wemail-Delivery: whd_… (unique per delivery — use it to de-duplicate), X-Webhook-Attempt: <n>, Wemail-Signature-Ed25519 (the same key-based signature as every webhook) and, when the action has a secret, Apifon-Signature plus X-Apifon-Signature — the hex HMAC-SHA256 of the raw request body keyed with your secret (openssl dgst -sha256 -hmac "$SECRET" over the body reproduces it). The body is the `InboundMessage` object plus "event": "inbound.received". Answer 2xx within 30 seconds; timeouts, network errors and 5xx are retried up to 7 times with increasing back-off (30 s … 24 h), 4xx is final. Every attempt is visible under List forward deliveries.
Limits, retention and what the SMTP sender sees
- Messages are kept for 30 days, then purged (attachment metadata and delivery attempts go with them). Delete earlier with `DELETE /v3/inbound/messages/{id}`.
- Quota — a plan allows a number of stored messages and bytes per rolling 30 days (10 000 messages / 1 GiB on Professional). When either is reached the receiver answers `452 4.2.2 inbound quota exceeded` at
RCPT TO: a well-behaved sender retries later; nothing is lost silently. - Message size up to 25 MB (
552 5.3.4above), at most 50 recipients per transaction (452 4.5.3), bodies truncated at 1 MB each (text_truncated/html_truncated), up to 100 attachments as metadata only (urlis alwaysnullin this version). - A recipient domain that is not verified and inbound-enabled answers
550 5.1.1 … recipient domain is not configured for inbound mail; a temporary platform problem answers451 4.3.0(the sender retries). - Duplicates: a message re-delivered with the same
Message-IDto the same domain is accepted (250) but stored only once and triggers no second forward. - SPF, DKIM and DMARC verdicts (
pass,fail,softfail,neutral,none,unknown) are informational — they never block acceptance.