Skip to content
YourMail

API reference

Email API error codes

{ error: { type, message, docs_url } }

All API errors use a consistent JSON shape — including a request that misses a route entirely. Every error has a machine-readable type you can switch on, a human-readable message safe to display, and a docs_url linking to that type's entry on this page.

Why use this

Things occasionally go wrong — a bad address, a rate limit, an unverified domain. Because every error comes back in the same predictable shape with a clear type, your code can react to each case on purpose rather than guessing from a message. This page is the reference for what each error means and how to handle it.

For example

When you're sending too fast you get a 429 with a Retry-After value — so your code can wait that many seconds and retry, instead of guessing or giving up.

Response shape

// Every error response body has this shape:
{
  "error": {
    "type": "rate_limited",
    "message": "Rate limit exceeded. Retry after 2 seconds.",
    "docs_url": "https://yourmail.dev/docs/errors#rate_limited"
  }
}

// On 429 rate_limited, the response also includes:
// Retry-After: 2   (seconds until the rate bucket refills)

Handling errors with the SDK

The SDK throws a YourMailError for any non-2xx response. It exposes status, type, message, docsUrl, and — when the type is rate_limited — retryAfter (seconds, from the Retry-After response header).

import { YourMail, YourMailError } from "yourmail";

const yourmail = new YourMail(process.env.YOURMAIL_API_KEY!);

try {
  await yourmail.send({ ... });
} catch (err) {
  if (err instanceof YourMailError) {
    console.error(err.status);    // HTTP status, e.g. 429
    console.error(err.type);      // machine-readable type, e.g. "rate_limited"
    console.error(err.message);   // human-readable description
    console.error(err.docsUrl);   // link to this page's entry for that type

    // Only present on rate_limited (from the Retry-After response header)
    if (err.retryAfter !== undefined) {
      console.log(`Retry after ${err.retryAfter}s`);
    }
  }
}

Error reference

All 11 error types and their typical HTTP status codes:

400validation_error(400, 403, 409, 413, 422)

Missing or invalid field; body, attachment, or total size too large; batch too large. At 403 it is a plan ceiling (e.g. your plan's webhook endpoint limit); at 409 a state conflict, such as cancelling a message that has already sent.

What to do — Fix the request. Check the message for the specific field or limit — a 403 here is a plan limit, not an API key problem.

401authentication_error(401, 403)

401: API key is absent, malformed, or not recognised. 403: the key is valid but send-only and this endpoint needs full access, or sending is paused for the account.

What to do — For a 401, check you're sending Authorization: Bearer <key> with a valid key from the dashboard. For a 403, use a full-access key — unless the message says sending is paused. That is not a key problem: our team paused the account, it does not lift on its own, and the message names who to contact.

403domain_error(403, 409, 502)

The from address uses a domain that is not verified or is not owned by this account — or the domain is verified but suspended because your plan allows fewer domains than your account holds. On POST /v1/domains, 409 means the domain is already added — to this account or to another one — and 403 can also mean the name can't be added at all; 502 (there, or on POST /v1/domains/:id/verify) means our email provider could not be reached.

What to do — Verify the domain in the dashboard before sending from it. If it's suspended by plan, upgrade or reactivate it in the dashboard (swapping it for another domain on your current plan). A 502 is on our side and transient — retry with backoff.

429quota_exceeded

Daily or monthly send quota is exhausted.

What to do — Wait until the quota period resets. retryAfter is not set — retry after midnight UTC or the monthly reset.

429rate_limited

Per-second send rate exceeded, or a daily cap was hit. The rate bucket is metered per message, not per request, so a batch of 50 spends 50 tokens: Free is 2 messages/sec with a burst of 10, all paid tiers 14/sec (the SES account rate) with a burst of 50. This is a reputation guard, not a billing limit — it's separate from your monthly quota, which returns quota_exceeded. Two daily caps also return this type: the 20/day test-mode cap, and the account-wide SES daily capacity guard. A burst of requests sent in parallel can also return it with a 1-second Retry-After: they contend on the same counter rows, and that clears immediately.

What to do — Read the Retry-After response header (or err.retryAfter in the SDK) and wait that many seconds before retrying. The SDK retries a per-second rate limit for you by default; a daily cap answers with the seconds until UTC midnight, which it surfaces rather than waiting out.

403deliverability_blocked

Your account's sending has been paused by the deliverability circuit breaker due to a high bounce or complaint rate.

What to do — Check the Deliverability page in the dashboard. Reduce bounces and complaints; sending resumes automatically when rates recover. Contact support if you need urgent assistance.

403account_pending_review

Your account is under review. Until the review is lifted it can send to addresses at its own verified domains, to the account email, and to the AWS SES simulator — but not to anyone else.

What to do — Carry on integrating: sends to your own verified domain, your account email and the SES simulator all work now, and test mode (from no-reply@yourmail.dev) is unaffected by the wall. Your plan limits and the new-account ramp still apply. Email support@mathersoftware.com to ask about the review.

422sandbox_recipient

You sent from the sandbox sender (no-reply@yourmail.dev, which puts you in test mode) to a recipient other than your signup email or an AWS SES simulator address. This is narrower than it sounds: the server normally rewrites a disallowed recipient to your signup inbox rather than rejecting it, so this specific error only fires when your account has no signup email on file to rewrite to AND the send names no simulator address, which is always a valid test-mode recipient. The separate 20/day test-mode cap is a 429 rate_limited, not this error.

What to do — Add a signup email to your account (or use the dashboard's "Send test email" button, which already has one), or send to an SES simulator address instead. To go live, send from an address on your verified domain instead of the sandbox sender.

422suppressed_recipient

A recipient is on this account's suppression list. The message names each blocked address and why it is listed — a hard bounce, a spam complaint, a one-click unsubscribe, or one you added by hand. An unsubscribe blocks bulk: true sends only; every other reason blocks all mail to that address.

What to do — Remove the address under Metrics → Deliverability → Suppressed recipients, or with DELETE /v1/suppressions/{address}. For an unsubscribe, do that only with fresh consent — the block on bulk mail is the recipient's own choice, and transactional mail to them is already going through.

404not_found

No email with the given ID exists in this account — or the request went to a path or HTTP method the API doesn't serve, in which case the message names the endpoints it does.

What to do — For an ID, check it: IDs are account-scoped, so a valid ID from another account returns 404. For a route, check the path and method against the message.

500internal_error

Unexpected server error.

What to do — Retry with exponential backoff. Resend a message only with the same idempotencyKey, and list API keys, webhooks or domains before retrying a create — those are not de-duplicated. If it persists, contact support.

Retry-After header — only present on rate_limited responses. The value is the number of seconds until the rate-limit bucket refills. The SDK surfaces this as err.retryAfter (a number). It is not set for quota_exceeded.

Last updated