Skip to content
MailMarcodocs

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.


StatusMechanismScope
400Suppression listOne recipient
402Plan send limitThe whole organization, this calendar month
403Organization pausedThe whole organization, until reactivated
429Warm-up daily capThe 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.

ScopeMatches
domainThe whole right-hand side. Store the bare domain in address, e.g. example.net — every recipient at that domain is blocked.
tenantThe exact address, case-insensitively. This is the default and the normal choice.
globalThe exact address. Recorded separately for platform-wide entries.
recipientThe 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.

POST/v1/suppressions
auth: API key201 on success

Adds an entry. Adding one that already exists returns the existing row rather than erroring.

Body

address
stringrequired
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.
request
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"}'
response · 201
{
  "id": "8a7b6c5d-4e3f-4a2b-9c8d-7e6f5a4b3c2d",
  "address": "[email protected]",
  "scope": "tenant",
  "reason": "bounce",
  "createdAt": "2026-07-29T10:15:00.000Z"
}
GET/v1/suppressions
auth: API key200 on success

Every suppression in the organization, as a bare array. There is no paging on this route.

DELETE/v1/suppressions/{id}
auth: API key200 on success

Removes a suppression, allowing sends to that address again.

Path parameters

id
string (uuid)required
Suppression id.
response · 200
{ "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.

TypeMeaningEffect
hardPermanent — no such mailbox, domain does not exist.The address is suppressed. Never retry it.
softTemporary — mailbox full, greylisting, transient failure.Retried automatically. Fires email.delivery_delayed while it is being retried.
blockThe receiver refused you specifically — policy, reputation or a blocklist.Investigate immediately; this is about your domain or IPs, not the recipient.
GET/v1/bounces
auth: API key200 on success

Recorded bounces for the organization.

response · 200
[
  {
    "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.

GET/v1/complaints
auth: API key200 on success

Recorded complaints for the organization.

response · 200
[
  {
    "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.

GET/v1/reputation
auth: Session200 on success

Current summary: volumes, bounce and complaint rates.

GET/v1/reputation/timeseries
auth: Session200 on success

Daily history.

Query parameters

days
integeroptionaldefault 30
Between 1 and 90.
GET/v1/reputation/dmarc
auth: Session200 on success

Aggregate DMARC reports and the sources seen sending as your domains.

Query parameters

limit
integeroptionaldefault 50
Between 1 and 200.
days
integeroptionaldefault 30
Between 1 and 365.
response · 200
{ "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:

response · 402
{
  "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_sent is 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:

response · 429
{
  "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.

SignalDefault thresholdAction
Bounce rateabove 4% of sends in the windowStatus becomes under_review. Sending continues, but an operator is alerted.
Complaint rateabove 0.1% of sends in the windowStatus becomes suspended. Sending stops immediately.
Minimum sample50 sends in the windowBelow this, no rule fires.

Thresholds are per organization and can be adjusted by an operator. If both rules fire, the pause wins.

While suspended

response · 403
{
  "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.