Skip to content
MailMarcodocs

11 · MailMarco API

Errors & limits

One envelope for every failure, a small set of status codes, and two independent limiting mechanisms that are easy to confuse.


The error envelope

Every non-2xx response is JSON with at least these three fields:

shape
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Validation failed"
}

Depending on the failure, two more fields may appear:

FieldWhenContains
detailsSchema validation failedAn array of { path, message }, one entry per offending field.
constraintA uniqueness or reference conflictThe database constraint name. The conflicting value is never echoed back — it may belong to another tenant.
coded error · 402
{
  "error": "plan_limit_exceeded",
  "metric": "emails_sent",
  "limit": 50000,
  "message": "Plan limit reached for emails_sent"
}

The coded errors, in full:

errorStatusMeaning
plan_limit_exceeded402Monthly plan allowance exhausted. Carries metric and limit.
org_suspended403The organization is paused; sending is blocked.
warmup_cap_reached429Today's warm-up cap is used up.

Status codes

StatusMeaningRetry?
200Success on GET, DELETE, PATCH, and POST routes that declare it.
201Success on POST. This is the default, so action-style routes like /cancel, /verify and /rotate-secret answer 201, not 200.
202A send was accepted and queued. Not yet delivered.
400Validation failed, or a domain rule rejected the request (unverified domain, suppressed recipient, infected attachment, invalid token).No — fix the request.
401Missing, malformed, unknown or revoked credential.No — fix the credential.
402Plan limit reached.Not this period. Upgrade or wait for the month to roll over.
403Valid credential, forbidden state: paused organization, non-admin on an admin route, or an attachment that is not scan-clean.No.
404No such resource in your organization.No.
409Conflict: the resource already exists, or the state transition is not allowed (cancelling a message that is not scheduled).No.
429Rate limit or warm-up cap. The two are distinguishable by the body.Yes, after the indicated wait.
500Unhandled server error. The response body carries no detail.Yes, with backoff.

Validation errors

A schema violation is always a 400 with message: "Validation failed" and a details array. path is dot-joined, so a nested or indexed field reads naturally.

response · 400
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Validation failed",
  "details": [
    { "path": "to", "message": "Invalid email" },
    { "path": "subject", "message": "subject must not contain CR or LF characters" },
    { "path": "attachments.0.content", "message": "attachment exceeds 10485760 bytes" }
  ]
}

Some rules are cross-field and report against the object rather than one path — for example subject is required unless a template is provided on a send, or supply exactly one of `contacts` or `csv` on a contact import.

Conflicts and references

response · 409
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Resource already exists",
  "constraint": "domains_domain_unique"
}

A reference to something that does not exist comes back as a 400 with message: "Referenced resource does not exist" and the same constraint field. Neither response echoes the value you sent.

Rate limits

Request-rate limiting is applied per route with a fixed window, and only on the routes listed below. There is no global request rate limit on the send path today — sending volume is governed by plan limits and warm-up caps instead.

RouteLimitKeyed by
POST /v1/auth/signup5 per hourClient IP
POST /v1/auth/login10 per minuteClient IP
POST /v1/domains/{id}/verify10 per hourOrganization
response · 429 (rate limit)
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded. Try again later.",
  "retryAfterSeconds": 2847
}

Windows are fixed, not sliding: the counter resets wholesale when the window expires. There is no Retry-After HTTP header — the hint is in the body.

Resource bounds

These are enforced at the schema boundary, so exceeding one is always a 400 and never a partial write.

BoundValueWhere
Attachments per message20Emails
Attachment size (decoded)10 MiB eachEmails
Total attachment size (decoded)25 MiBEmails
Emails per batch500Emails
Custom header names^[A-Za-z0-9-]+$Emails
Pagination limit1–100, default 25 (broadcast recipients allow up to 200, default 100)List routes
Contacts per import10,000 rowsMarketing
CSV import document8 MiBMarketing
Contact custom properties100 keys, keys ≤ 64 chars, string values ≤ 1024 charsMarketing
Segment rule depth5 levelsMarketing
Segment rule nodes50Marketing
in200 entriesMarketing
Preference-center topics per update200Marketing
Broadcast subject1–998 charactersMarketing
Webhook delivery attempts5, then deadWebhooks
Reputation history window1–90 days (DMARC reports: 1–365)Deliverability

Retrying safely

  • Always send an Idempotency-Key on POST /v1/emails. It makes a timeout safe to retry, and a replay costs nothing.
  • Retry 429 after retryAfterSeconds when present; otherwise treat it as “not until tomorrow”.
  • Retry 500 with exponential backoff and a cap.
  • Never retry 400, 401, 403, 404 or 409 unchanged — nothing about the request will succeed on the second attempt.
  • The batch route ignores the idempotency header. Retry individual failed indices, not the whole batch.