Inbound Routes
Configure routing rules to direct incoming emails to storage, webhooks, or other actions
Routes define how MailLand handles incoming emails for a domain. Each route matches on the
recipient's local part (the part of the address before @) and runs one or more actions:
store, deliver to a webhook, forward, reject at SMTP, or drop. Routes are evaluated in priority
order (lowest number first) and the first match wins.
Routes match the local part of the recipient address only. There is no sender, subject, or header
matching: a route cannot filter on from or subject.
Match Types
| Match Type | Description |
|---|---|
exact | Local part equals the pattern exactly |
prefix | Local part starts with the pattern |
suffix | Local part ends with the pattern |
contains | Local part contains the pattern |
plusaddressing | Local part is the pattern followed by +anything (support matches support+ticket123) |
catchall | Matches every recipient; no pattern needed |
regex | Local part matches the pattern as a regular expression (max 200 characters, 1 second timeout) |
Matching is case-insensitive and always runs against the local part, [email protected] and
[email protected] both match a route with pattern support scoped to the same domain.
The regex match type requires a plan with regex inbound routing (Pro and Max today, not Free).
Creating a route with matchType: "regex", or updating an existing route so its match type
becomes regex, on a plan without it returns 400 PLAN_LIMIT_REACHED. An existing regex route
keeps matching even if the workspace later downgrades, and editing it without changing its match
type is unaffected.
Route Actions
A route's actions field is a list of one or more of the following string values:
| Action | Description |
|---|---|
store | Store the parsed email for retrieval via the API and dashboard |
webhook | Deliver the parsed email to a webhook (requires webhookId) |
forward | Send a copy of the email to forwardToEmail, through the normal sending path |
reject | Refuse the message at the SMTP level with rejectStatus/rejectMessage |
drop | Accept the message at SMTP, then silently discard it, no storage, no webhook |
unsubscribe | Treat the address as a List-Unsubscribe mailbox; terminal, see below |
store and webhook can be combined with each other and with forward. reject, drop, and
unsubscribe are terminal: each must be the only action on the route; the API rejects a
request that combines one of them with any other action.
A forward copy is not sent by raw SMTP relay: it goes through the same sending pipeline as any other transactional email, which is why it needs the destination's consent (below), counts against your sending quota, and appears in your message log like any other send.
Forward Destinations
Before any mail is forwarded to an address, that address's owner must confirm they want it: unless the address is on one of your own domains that is fully verified for sending. There is no way around this for an outside address: it protects the destination's owner from receiving mail they never agreed to, and it protects your sending reputation from an address that bounces or complains.
When you create or update a route with forward in actions, MailLand automatically:
- Creates (or reuses) a forward-destination record for
forwardToEmail, scoped to your tenant: the same address is remembered across every route that forwards to it. - If the address needs confirmation and has never been asked, sends it a confirmation email containing a link.
The route save itself always succeeds; a failed confirmation email does not fail the route:
the destination simply stays pending (nothing is forwarded to it) until you resend the
confirmation.
Confirmation Link
The link in the confirmation email is valid for 7 days and is single-use: once the
recipient confirms or declines, visiting the link again just shows the same decision, it cannot
be reversed by revisiting it. An expired, never-decided link returns 410 Gone if opened again.
- Confirm: mail forwarded to this tenant will now be sent to the address.
- Decline: final. Nothing is ever forwarded to the address for this tenant again, and the tenant cannot request confirmation again for it.
Get a Forward Destination
GET https://api.mail.land/v1/transactional/inbound/forward-destinations/{destinationId}{
"id": "f2k9m3q7p4w8x1",
"email": "[email protected]",
"status": "pending",
"confirmedVia": null,
"lastSentAt": "2026-09-26T10:00:00Z",
"confirmedAt": null,
"declinedAt": null
}status is one of pending, confirmed or declined. confirmedVia is link when the owner
confirmed through the email, verified_domain when the address is on one of your own
sending-verified domains and needed no confirmation, or null otherwise. A route's own response
also carries this same object under forwardDestination (see the route response below).
Resend a Confirmation
POST https://api.mail.land/v1/transactional/inbound/forward-destinations/{destinationId}/resendOnly for a destination that is still pending and does need a link (an address on one of your own
verified sending domains returns 409 FORWARD_DESTINATION_NO_CONFIRMATION_NEEDED, since there is
nothing to resend). Rate-limited: at most once every 5 minutes and 5 times a day per destination,
and 50 times a day across the whole tenant. Exceeding either limit returns
429 FORWARD_CONFIRMATION_RATE_LIMITED.
There is no way to un-decline a destination through the API once it has declined: a declined address stays refused for that tenant.
Create a Route
Routes belong to a domain and are created under that domain.
POST https://api.mail.land/v1/transactional/inbound/domains/{domainId}/routes{
"name": "Support Inbox",
"matchType": "exact",
"matchPattern": "support",
"actions": ["store", "webhook"],
"webhookId": "w4k8m2q7p9x3z1",
"priority": 10
}| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Route name |
matchType | string | Yes | One of the match types above |
matchPattern | string | Yes | Pattern to match against the local part (ignored for catchall, max 64 characters, max 200 for regex) |
actions | string[] | Yes | One or more of store, webhook, forward, reject, drop, unsubscribe |
webhookId | string | Cond. | Public ID of an existing webhook; required when actions includes webhook. Routes never create a webhook inline: create it first on the Webhooks page |
forwardToEmail | string | Cond. | Destination for forward; required when actions includes forward. Must be a bare address (no display name, no angle brackets). The route's own domain must be fully verified for sending, the forwarded copy is sent from it, or the request is rejected with 400 FORWARD_DOMAIN_NOT_VERIFIED. The destination itself must separately confirm, unless it is on one of your own sending-verified domains (see Forward Destinations) |
rejectStatus | string | No | SMTP status for reject, e.g. 550 or 550 5.1.1 (default 550 5.1.1) |
rejectMessage | string | No | SMTP rejection text for reject |
priority | int | No | Lower number = evaluated first. Defaults to one more than the domain's current highest priority. Must be unique per domain |
curl -X POST https://api.mail.land/v1/transactional/inbound/domains/k3v9x2m8q1w7z4/routes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "Support Inbox",
"matchType": "exact",
"matchPattern": "support",
"actions": ["store", "webhook"],
"webhookId": "w4k8m2q7p9x3z1",
"priority": 10
}'Response
{
"id": "r7k3m9q2p4w8x1",
"name": "Support Inbox",
"domainId": "k3v9x2m8q1w7z4",
"domainName": "yourdomain.com",
"priority": 10,
"isEnabled": true,
"matchType": "Exact",
"matchPattern": "support",
"actions": ["store", "webhook"],
"webhookId": "w4k8m2q7p9x3z1",
"webhookName": "Support Webhook",
"webhookUrl": "https://yourapp.com/webhooks/support",
"forwardToEmail": null,
"forwardDestination": null,
"rejectStatus": null,
"rejectMessage": null,
"createdAt": "2026-02-17T12:00:00Z",
"updatedAt": "2026-02-17T12:00:00Z"
}Requests accept matchType in any case; responses always return the enum name capitalized:
Exact, Prefix, Suffix, Contains, PlusAddressing, CatchAll, Regex. Do not compare a
response's matchType against a lowercase string.
forwardDestination is only populated when actions includes forward; it carries the same
object described in Get a Forward Destination:
{
"...": "...",
"actions": ["store", "forward"],
"forwardToEmail": "[email protected]",
"forwardDestination": {
"id": "f2k9m3q7p4w8x1",
"email": "[email protected]",
"status": "pending",
"confirmedVia": null,
"lastSentAt": "2026-09-26T10:00:00Z",
"confirmedAt": null,
"declinedAt": null
}
}List Routes for a Domain
GET https://api.mail.land/v1/transactional/inbound/domains/{domainId}/routesReturns every route for the domain, sorted by priority (lowest first).
curl https://api.mail.land/v1/transactional/inbound/domains/k3v9x2m8q1w7z4/routes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"items": [
{ "id": "r7k3m9q2p4w8x1", "name": "Support Inbox", "priority": 10, "isEnabled": true, "..." : "..." }
],
"totalCount": 1
}List All Routes
To list routes across every domain in the tenant, use the cursor-paginated endpoint instead:
GET https://api.mail.land/v1/transactional/inbound/routes| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | - | Opaque cursor from the previous page's nextCursor. Omit for the first page |
limit | int | 50 | Items per page |
Get Route Details
GET https://api.mail.land/v1/transactional/inbound/routes/{routeId}curl https://api.mail.land/v1/transactional/inbound/routes/r7k3m9q2p4w8x1 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Update a Route
PUT https://api.mail.land/v1/transactional/inbound/routes/{routeId}The request body matches the create request, but every field is optional: only the fields you send are updated.
curl -X PUT https://api.mail.land/v1/transactional/inbound/routes/r7k3m9q2p4w8x1 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"matchPattern": "help",
"priority": 5
}'Toggle a Route
Flips isEnabled without touching any other field. No request body.
POST https://api.mail.land/v1/transactional/inbound/routes/{routeId}/togglecurl -X POST https://api.mail.land/v1/transactional/inbound/routes/r7k3m9q2p4w8x1/toggle \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Reorder Routes
Update the priority of every route in a domain at once, by passing the route IDs in the desired order: the first ID becomes priority 1, the second priority 2, and so on.
POST https://api.mail.land/v1/transactional/inbound/domains/{domainId}/routes/reorder{
"routeIds": ["r7k3m9q2p4w8x1", "m2p8w4x1k7q3z9", "x4k1p7m3q9w2z8"]
}curl -X POST https://api.mail.land/v1/transactional/inbound/domains/k3v9x2m8q1w7z4/routes/reorder \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"routeIds": ["r7k3m9q2p4w8x1", "m2p8w4x1k7q3z9", "x4k1p7m3q9w2z8"]
}'Test a Route
Check which route would match a given recipient address without processing an email.
POST https://api.mail.land/v1/transactional/inbound/domains/{domainId}/routes/test{
"emailAddress": "[email protected]"
}curl -X POST https://api.mail.land/v1/transactional/inbound/domains/k3v9x2m8q1w7z4/routes/test \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"emailAddress": "[email protected]"
}'{
"matched": true,
"matchedRouteId": "r7k3m9q2p4w8x1",
"matchedRouteName": "Support Inbox",
"actions": ["store", "webhook"]
}If no route matches, the response is 200 with matched: false and matchedRouteId/
matchedRouteName/actions all null. 404 means the domain was not found, or the tested address
is invalid (no @).
Delete a Route
DELETE https://api.mail.land/v1/transactional/inbound/routes/{routeId}curl -X DELETE https://api.mail.land/v1/transactional/inbound/routes/r7k3m9q2p4w8x1 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Route Examples
Catch-all route: store every unmatched recipient:
{
"name": "Catch All",
"matchType": "catchall",
"matchPattern": "*",
"actions": ["store"],
"priority": 999
}Plus-addressing: route all support+*@yourdomain.com variants to one webhook:
{
"name": "Support Tickets",
"matchType": "plusaddressing",
"matchPattern": "support",
"actions": ["store", "webhook"],
"webhookId": "w4k8m2q7p9x3z1",
"priority": 5
}Reject at SMTP: refuse mail to a retired mailbox before it is accepted:
{
"name": "Retired Mailbox",
"matchType": "exact",
"matchPattern": "old-team",
"actions": ["reject"],
"rejectStatus": "550 5.1.1",
"rejectMessage": "Mailbox does not exist",
"priority": 1
}Routes are evaluated in priority order (lowest number first). Place specific routes at lower priority numbers and catch-all routes at the highest number so they run last.
What a Forwarded Copy Looks Like
A forwarded copy is a new, ordinary transactional email, not a raw relay of the original bytes, sent from your verified domain so it can pass DKIM and DMARC at the destination's mailbox provider:
- From:
"<original sender name> via <matched address>@<your domain>", e.g."Jane Doe via [email protected]". A display name that looks like an address (contains@,<or>) is replaced with the bare original address, so a copy can never impersonate a brand under your DKIM. - Reply-To: the original message's
Reply-Toif it had one, otherwise the original sender's address, so replies reach the real author, not you. - No tracking: open and click tracking are always off on a forwarded copy, whatever your workspace's tracking settings are.
- No branding footer: the Free-plan footer that other sends may carry is never added.
- Attachments: filtered through the same rules as an outbound API send (type, count, total size). Anything left out is noted at the top of the copy: "Forwarded without N attachment(s) that exceed the outbound attachment limits: ...", the copy still arrives, just without those parts.
- Tag: every forwarded copy carries the tag
inbound-forward, so you can find them in your regular message log; its delivery status (queued, sent, delivered, bounced, etc.) is the same status any other transactional send gets, because it goes through the same delivery pipeline. - Usage: each forwarded copy counts as one send against your plan's quota and billing, shown as its own "Inbound forward" line in usage, separate from API, SMTP, campaign and automation sends.
What Is Never Forwarded
A message matching a forward route is not forwarded, and never billed, when:
- It was flagged as spam, or the sender is blocklisted as spam.
- It failed DMARC under the sender domain's effective policy of
quarantineorreject, resolved by walking up from the author domain to find the nearest applicable record, so a subdomain's ownsp=overrides its organizational domain'sp=, and at=y(testing) orpct=0record is treated one policy level lower, forwarding it would re-sign a message the sender explicitly asked receivers to refuse. - It, or its HTML/text body, is over the platform's outbound size limits.
- Forwarding it would create a loop: the destination is one of the message's own recipients, or it
already carries too many hops or
Receivedheaders.
A copy that is eligible but temporarily can't be sent (a policy delay, a quota window, an outage) is retried with back-off for up to 24 hours, then expires without being sent. A route that is disabled, deleted, or has its destination changed after a message was received also stops any copy still waiting for it: the current route configuration is always authoritative.
Checking a Forward's Outcome
Every inbound email that matched a forward route carries a forwards array on its detail
response (GET /v1/transactional/inbound/emails/{id}): one entry per destination the message was queued to
forward to, so you can see what actually happened, not just where it was configured to go:
"forwards": [
{
"destination": "[email protected]",
"status": "Admitted",
"reason": null,
"omittedAttachmentCount": 0,
"messageId": "m_01hz3k...",
"createdAt": "2026-09-26T10:00:00Z",
"updatedAt": "2026-09-26T10:00:02Z"
}
]| Field | Type | Description |
|---|---|---|
destination | string | The forward destination address |
status | string | One of the statuses below |
reason | string | null | Machine-readable detail code (for example hop_limit), or null |
omittedAttachmentCount | number | Attachments left out of the copy for exceeding outbound attachment limits |
messageId | string | null | The forwarded copy's message id once admitted, otherwise null |
createdAt / updatedAt | string | ISO-8601 timestamps for the forward record |
status is one of:
| Status | Meaning |
|---|---|
Pending | Written with the inbound message; not yet claimed by the forward worker |
Processing | Claimed by the forward worker; in flight |
Deferred | A transient refusal (policy delay, rate, quota window, outage); retried with back-off for up to 24 hours |
Admitted | Sent through the normal sending pipeline: messageId names the copy, whose own delivery status (queued/sent/delivered/bounced) is authoritative |
SkippedLoop | Loop protection: hop limit, too many Received headers, or the destination is one of the message's own recipients |
SkippedSpam | The message was flagged as spam, or the sender is blocklisted as spam |
SkippedAuthFail | DMARC failed under the sender domain's effective policy of quarantine or reject (subdomain sp= honored) |
SkippedTooLarge | The message is over the outbound forward size cap |
RefusedSuppressed | The destination is suppressed, or refused by recipient protection |
RefusedDomainNotVerified | The route's domain is not (or no longer) verified for sending |
RefusedPolicy | A terminal policy or account refusal |
RefusedRouteInactive | The route was disabled, deleted, lost its forward action, or changed destination after the message was received |
RefusedInvalid | The stored message could not be turned into a sendable copy |
Expired | Still Deferred when the 24-hour retry window closed |
RefusedDestinationUnconfirmed | The destination has not confirmed (see Forward Destinations) |
RefusedDestinationDeclined | The destination's owner declined; nothing is ever forwarded to it |
Pending, Processing and Deferred are the only non-final statuses, every other value is
terminal.
Forward Error Responses
| Status | Code | Description |
|---|---|---|
400 | VALIDATION_FAILED (field forwardToEmail) | On create: actions includes forward but forwardToEmail is empty |
400 | FORWARD_DESTINATION_REQUIRED | On update (PUT): the resulting effective route forwards but has no forwardToEmail |
400 | FORWARD_DOMAIN_NOT_VERIFIED | The route's domain is not fully verified for sending, so no forwarded copy could ever be sent from it |
404 | RESOURCE_NOT_FOUND | Forward destination not found |
409 | FORWARD_DESTINATION_NOT_PENDING | Resend requested for a destination that already confirmed or declined |
409 | FORWARD_DESTINATION_NO_CONFIRMATION_NEEDED | Resend requested for a destination on one of your own verified sending domains |
410 | FORWARD_CONFIRMATION_EXPIRED | The confirmation link expired (7 days) while still undecided |
409 | FORWARD_CONFIRMATION_ALREADY_DECIDED | The link was used for the other decision |
429 | FORWARD_CONFIRMATION_RATE_LIMITED | Resend requested too soon, or the daily resend limit was reached |
Error Responses
| Status | Code | Description |
|---|---|---|
400 | VALIDATION_FAILED | Missing required fields, invalid matchType/actions, or a terminal action combined with another |
400 | INVALID_REQUEST | A route with this priority already exists for the domain, or the referenced webhookId was not found |
400 | PLAN_LIMIT_REACHED | Creating a route with matchType: "regex", or updating a route so its match type becomes regex, on a plan without regex route support (Free today) |
403 | FORBIDDEN | Insufficient permissions |
404 | RESOURCE_NOT_FOUND | Domain or route not found (test endpoint: also an invalid address without @; no match is 200 with matched: false, not a 404) |
Next Steps
- Inbound Overview: how inbound email processing works
- Inbound Webhooks: webhook payload format and delivery
- Domains: configure MX records for inbound