08 · MailMarco API
Deliverability
Four different mechanisms can refuse a send. Each has its own status code, and each tells you something different about what to fix.
| Status | Mechanism | Scope |
|---|---|---|
400 | Suppression list | One recipient |
402 | Plan send limit | The whole organization, this calendar month |
403 | Organization paused | The whole organization, until reactivated |
429 | Warm-up daily cap | The whole organization, until tomorrow |
Suppressions
A suppression blocks an address from receiving anything from your organization. The send path checks every recipient — to, cc and bcc — before the message is written, and one suppressed recipient rejects the entire message with 400 recipient suppressed: <address>. There is no partial send.
| Scope | Matches |
|---|---|
domain | The whole right-hand side. Store the bare domain in address, e.g. example.net — every recipient at that domain is blocked. |
tenant | The exact address, case-insensitively. This is the default and the normal choice. |
global | The exact address. Recorded separately for platform-wide entries. |
recipient | The exact address. Recorded separately for per-recipient entries. |
Suppressions accumulate from two directions: you add them, and the bounce pipeline adds them for you when a hard bounce or a complaint comes back.
/v1/suppressionsAdds an entry. Adding one that already exists returns the existing row rather than erroring.
Body
addressstringrequired | The address, or a bare domain when scope is domain. Only a minimum length of 1 is enforced — the value is not validated as an email address. |
scope"global" | "tenant" | "domain" | "recipient"optionaldefault "tenant" | How the entry is classified; see the table above. |
reason"bounce" | "complaint" | "invalid" | "manual"optionaldefault "manual" | Why the address was suppressed. |
curl -X POST https://api.mailmarco.com/v1/suppressions \
-H "authorization: Bearer $MAILMARCO_API_KEY" \
-H "content-type: application/json" \
-d '{"address":"[email protected]","scope":"tenant","reason":"bounce"}'{
"id": "8a7b6c5d-4e3f-4a2b-9c8d-7e6f5a4b3c2d",
"address": "[email protected]",
"scope": "tenant",
"reason": "bounce",
"createdAt": "2026-07-29T10:15:00.000Z"
}/v1/suppressionsEvery suppression in the organization, as a bare array. There is no paging on this route.
/v1/suppressions/{id}Removes a suppression, allowing sends to that address again.
Path parameters
idstring (uuid)required | Suppression id. |
{ "deleted": true }Bounces
A bounce is the receiving server refusing the message. MailMarco attributes bounces through a signed return-path address, so attribution comes from the envelope token rather than from parsing the bounce body.
| Type | Meaning | Effect |
|---|---|---|
hard | Permanent — no such mailbox, domain does not exist. | The address is suppressed. Never retry it. |
soft | Temporary — mailbox full, greylisting, transient failure. | Retried automatically. Fires email.delivery_delayed while it is being retried. |
block | The receiver refused you specifically — policy, reputation or a blocklist. | Investigate immediately; this is about your domain or IPs, not the recipient. |
/v1/bouncesRecorded bounces for the organization.
[
{
"id": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6071",
"messageId": "b1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"address": "[email protected]",
"type": "hard",
"code": "550",
"diagnostic": "550 5.1.1 <[email protected]>: Recipient address rejected",
"at": "2026-07-29T09:24:03.000Z"
}
]Complaints
A complaint is a recipient pressing “this is spam”. It arrives through the provider's feedback loop, not through SMTP, so it is a different signal from a bounce and a far more serious one — the tolerated complaint rate is roughly forty times lower than the tolerated bounce rate.
/v1/complaintsRecorded complaints for the organization.
[
{
"id": "2b3c4d5e-6f70-4819-a2b3-c4d5e6f70819",
"messageId": "b1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"address": "[email protected]",
"source": "feedback-loop",
"at": "2026-07-29T11:02:41.000Z"
}
]Your reputation
/v1/reputation is your organization's own view of its sending health. It is session-authenticated, and there is deliberately no organization id on any route — the org comes from the token.
/v1/reputationCurrent summary: volumes, bounce and complaint rates.
/v1/reputation/timeseriesDaily history.
Query parameters
daysintegeroptionaldefault 30 | Between 1 and 90. |
/v1/reputation/dmarcAggregate DMARC reports and the sources seen sending as your domains.
Query parameters
limitintegeroptionaldefault 50 | Between 1 and 200. |
daysintegeroptionaldefault 30 | Between 1 and 365. |
{ "reports": [ /* … */ ], "sources": [ /* … */ ] }Plan limits (402)
Sends are metered against the plan attached to your subscription, per calendar month (YYYY-MM). Exceeding the allowance answers 402 Payment Required:
{
"error": "plan_limit_exceeded",
"metric": "emails_sent",
"limit": 50000,
"message": "Plan limit reached for emails_sent"
}- The counter is incremented in the same transaction that writes the message, and only after the message row is confirmed. A rejected or idempotently-deduped send consumes nothing.
- The check is an atomic conditional increment, so concurrent sends cannot both slip past the cap.
- An organization with no subscription is unmetered: no limit is applied and no counter is written. A subscribed organization on a plan that does not cap
emails_sentis metered for billing but never rejected. - Sandbox sends take the same metering path, so they count.
Warm-up caps (429)
A new organization can be placed on a warm-up schedule with a daily cap that grows over time. Once today's cap is reached, sends are refused until the next day:
{
"error": "warmup_cap_reached",
"message": "warm-up daily send cap reached"
}The cap is checked before anything is written, and the counter is incremented inside the message transaction, so a rejected send never consumes a slot. An organization with no warm-up configured is unrestricted by this rule.
What pauses an organization
A rule engine evaluates a rolling window of your real events and can change the organization's status. A window with fewer than the minimum sample of sends produces no action at all — a couple of bounces on a handful of messages is noise.
| Signal | Default threshold | Action |
|---|---|---|
| Bounce rate | above 4% of sends in the window | Status becomes under_review. Sending continues, but an operator is alerted. |
| Complaint rate | above 0.1% of sends in the window | Status becomes suspended. Sending stops immediately. |
| Minimum sample | 50 sends in the window | Below this, no rule fires. |
Thresholds are per organization and can be adjusted by an operator. If both rules fire, the pause wins.
While suspended
{
"error": "org_suspended",
"message": "organization sending is paused"
}Every send is refused, including sandbox sends and messages that were already scheduled — the paused state is re-checked when a scheduled message comes due, so scheduling ahead of a suspension does not evade it. Reads keep working. Reactivation is an operator action.
Endpoints
Covered above: GET/POST /v1/suppressions, DELETE /v1/suppressions/:id, GET /v1/bounces, GET /v1/complaints, GET /v1/reputation, GET /v1/reputation/timeseries, GET /v1/reputation/dmarc.
Threshold configuration and tenant suspension live on the operator surface (/v1/admin/deliverability/:orgId/thresholds and /v1/admin/tenants/:id/{suspend,reactivate,limit}) and require a platform-admin token; they are not part of the customer API.