Skip to main content

Limits, Quotas & Errors

The three sending ceilings, domain warm-up and reputation pauses, the four error body shapes, and every error reason.

Overview

Everything that can stop a send lives here: the three ceilings that govern volume, and the four error shapes plus the full reason table a client has to branch on. Per-message limits (recipients, size, tags, attachments) are under Send Email; batch limits under Batch (messageVersions).

Limits & Quotas

Three separate ceilings govern sending, and they count three different things. A rejection always names which one you hit.

PlanSend rateRecipients per calendar monthDaily-quota ceiling
Free10 requests / min2,000200
Starter60 requests / min50,0005,000
Pro300 requests / min1,000,00050,000
Enterprise1,000 requests / minUnlimited500,000
  • Send rate counts requests accepted in the trailing 60 seconds, across your whole account. One call is one request whether it carries 1 recipient or 50, and a messageVersions batch is still one request. Exceeding it is 429 rate_limited.
  • Recipients per calendar month counts delivered recipients in the current calendar month. It resets on the 1st, not on a rolling 30 days. A send is refused up front if it would push you past the cap. Exceeding it is 429 monthly_cap_exceeded.
  • Daily quota counts sends, meaning messages and not recipients, for one domain over a rolling 24 hours. Exceeding it is 429 quota_exceeded.

Warm-up. The daily quota is a property of each domain, not of your plan. Every newly verified domain starts at 200 sends / day and climbs the ladder 200 → 1,000 → 5,000 → 20,000 → 50,000, at most one step per day, and only after a day that carried real volume with a healthy bounce and complaint rate. Past the top of the ladder the quota keeps doubling on each clean day rather than jumping straight to the plan ceiling, so a plan whose ceiling is higher than 50,000 is reached over several more clean days. The plan column above is the ceiling the plan permits; the figure actually enforced is the domain's current daily_quota, which GET /api/v1/email/domains returns.

Reputation pause. A domain whose bounce or complaint rate degrades is paused automatically, whatever the plan or remaining quota. Sends from it then return 403 domain_paused carrying the reason, and DomainOut shows paused_at and pause_reason.

# Read the quota actually enforced for each domain
curl https://api.callmissed.com/api/v1/email/domains \
  -H "Authorization: Bearer cm_your_key"

Errors

Response shapes

Error bodies come in four shapes. They are not interchangeable, and a client that always reads reason from the same place breaks, most obviously on relay_failed, where reason sits at the top level rather than under detail.

1. Send rejection: reason nested under detail. Everything the send pipeline refuses (every row in the table below except relay_failed), plus 403 email_not_enabled, 422 scheduled_batch_unsupported, 503 sending_unavailable, and the 502 from the domain routes:

{
  "detail": {
    "error": "acme.com has not completed DNS verification",
    "reason": "domain_not_verified"
  }
}

2. 502 relay_failed: flat, and it carries an id. This one is not wrapped in detail. The send row already exists, so the id comes back for you to look up with GET /api/v1/email/sends:

{
  "error": "The message could not be accepted for delivery",
  "reason": "relay_failed",
  "id": "9d0f8b3a-1c2e-4a5b-8f7d-6e2a1b0c9d4e"
}

3. Auth, not-found and conflict: detail is a plain string with no reason at all. Covers 401, the read-only-key 403, every 404 (domain, template, inbound message, suppression, scheduled send), 409 (duplicate template name, address already claimed), the 400 on a duplicate domain, the 400s and 422s on the sender and inbound-address routes, the 502s from the sender routes, and the 503s raised when email is not configured:

{ "detail": "A valid API key is required" }

4. Schema-validation 422: detail is an array, with no reason key. Anything rejected before the send pipeline runs: a malformed or control-character-bearing address, a subject or header carrying control characters, an attachment that is not exactly one of url/content (or content without name), over-limit params, a scheduledAt in the past or beyond the 72-hour horizon, and the batch cross-version rules:

{
  "detail": [
    {
      "type": "value_error",
      "loc": ["body"],
      "msg": "Value error, invalid email address: 'not-an-email'",
      "input": { "...": "..." }
    }
  ]
}

When handling errors, branch on the HTTP status first, then check whether detail is an object, a string or an array before reaching for reason.

Handling all four shapes:

import httpx

r = httpx.post(
    "https://api.callmissed.com/api/v1/email/send",
    headers={"Authorization": "Bearer cm_your_key"},
    json={
        "from": "Acme Ops <donotreply@acme.com>",
        "to": ["Ada <customer@example.com>"],
        "subject": "Your receipt",
        "text": "Thanks for your order.",
    },
)

if r.status_code != 202:
    body = r.json()
    if "reason" in body:                       # shape 2 - relay_failed, flat, has id
        reason, send_id = body["reason"], body.get("id")
    else:
        detail = body.get("detail")
        if isinstance(detail, dict):           # shape 1 - send rejection
            reason, send_id = detail.get("reason"), None
        elif isinstance(detail, list):         # shape 4 - schema validation
            reason, send_id = "validation_error", None
        else:                                  # shape 3 - plain string detail
            reason, send_id = None, None
    print(r.status_code, reason, send_id)

Reasons

StatusReasonMeaning
401(none, string detail)Missing, malformed or unrecognised Authorization header
402payment_requiredNot enough credit balance to cover the send
403(none, string detail)The API key is read-only and this route writes
403email_not_enabledThe API key lacks the email permission
403domain_not_foundThe From domain is not registered on your account at all
403domain_not_verifiedThe From domain is registered but hasn't passed verification
403domain_pausedSending from the domain is paused (reputation)
403all_recipients_suppressedEvery recipient is on your suppression list, nothing to send
404template_not_foundThe templateId doesn't exist or isn't yours
422no_senderNeither from/sender nor a template default_sender supplied one
422invalid_fromThe resolved sender is not a usable email address
422no_recipientsThe resolved recipient list came out empty
422empty_bodyNeither text nor html (nor a template body) was present
422invalid_headersA rendered header value contains invalid characters, usually a template param with a newline in it
422unresolvable_template_varsThe subject or body references {{ contact.something }}, a namespace nothing can populate, so it would render empty. Pass the value in params instead
422message_too_largeThe assembled message exceeds 25 MB
422template_inactiveThe template exists but is not active (is_active: false)
422too_many_recipientsOver 50 recipients on a single send, or over the union limit on a batch
422scheduled_batch_unsupportedA send set both scheduledAt and messageVersions, pick one
422invalid_attachmentAn attachment's content is not valid base64
422attachment_fetch_failedA url attachment could not be fetched, or exceeded the size cap
422attachment_url_forbiddenA url attachment points at a blocked (internal/private) address, or uses a scheme other than http/https
429rate_limitedPer-minute request rate for your plan exceeded
429monthly_cap_exceededMonthly recipient volume for your plan exceeded
429quota_exceededThe domain's daily send quota is exhausted
502relay_failedThe message could not be accepted for delivery. Uses the flat shape above
502acs_unavailableDomain provisioning or verification is temporarily unavailable, retry the domain call
503sender_propagatingThe from address has now been registered as a sender for the domain, but the mail service has not finished propagating it. Retry the send shortly; no further setup is needed. See Sender Addresses
503sending_unavailableSending is temporarily unavailable

A malformed recipient address is not invalid_attachment. It is a schema 422 (shape 4), rejected before the send pipeline runs and before any charge.