> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getmailr.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Mailr is a direct-mail marketing platform for US home-services contractors. Each postcard is personalized with an AI-enhanced image of the recipient's own home and a unique QR code that leads to a personalized landing page. Campaigns can target a neighborhood the customer draws on a map, the homes around past jobs they upload or import, or fire automatically from their CRM. Mailr does not mail an uploaded recipient list. When answering questions, prefer the exact steps and UI labels from these docs, and direct users to app.getmailr.com to sign in. For anything involving account-specific data, billing disputes, or mail that appears lost, direct the user to support@getmailr.com.

# Errors & rate limits

> Error body shape, the two error-code conventions in the Mailr API, per-endpoint rate limits, and retry guidance.

## Error body

Errors return a JSON body with a stable machine code in `error` and, where available, a human-readable
`detail` naming the offending field:

```json theme={null}
{ "error": "invalid_payload", "detail": "event_type is required" }
```

<Warning>
  Two code styles coexist across the API, both in the `error` field:

  * **snake\_case machine codes** on the v1 and rate-limited endpoints: `invalid_payload`, `invalid_json`,
    `payload_too_large`, `rate_limited`, `automations_disabled`, `geocoding_unavailable`, `geocoding_failed`,
    `internal_error`.
  * **Human sentences** on the older endpoints: `Unauthorized`, `No address found in payload`,
    `Too many requests`, `Failed to subscribe`, `Internal server error`.

  Match on the specific string you expect per endpoint (see each endpoint's page and the **Endpoints** section of this reference) rather
  than assuming one global convention.
</Warning>

## Authentication errors

<ResponseField name="401" type="status">
  Missing, invalid, revoked, or expired key. Never reveals whether a key exists.
</ResponseField>

<ResponseField name="403" type="status">
  The key is valid but resolves to no organization, or is missing the scope the operation requires.
</ResponseField>

<ResponseField name="503" type="status">
  The authentication provider was temporarily unreachable — your key was **not** checked and its validity is
  unknown. Body `{ "error": "auth_unavailable" }` with a `Retry-After` header. Retry with backoff; do not treat
  this as a bad key.
</ResponseField>

On `POST /api/v1/events` the auth body is `{ "error": "unauthorized" }`; elsewhere it's
`{ "error": "Unauthorized" }`. Same meaning, different casing.

## Rate limits

Limits are per-org, counted in a **fixed** window that resets on the boundary (not a sliding window).
Over-limit responses carry a `Retry-After` header (seconds).

| Endpoint                        | Limit      | Window  | `Retry-After`        |
| ------------------------------- | ---------- | ------- | -------------------- |
| `POST /api/v1/events`           | 120        | 60 s    | `60`                 |
| `POST /api/v1/import/jobs`      | 10 batches | UTC day | seconds to 00:00 UTC |
| `POST /api/v1/conversions`      | 60         | 60 s    | `60`                 |
| `POST /api/v1/tracking/lead`    | 60         | 60 s    | `60`                 |
| `POST /api/v1/webhooks/trigger` | 60         | 60 s    | `60`                 |

The events limit and the trigger limit are **separate buckets** — using one doesn't consume the other. A
`dry_run` event costs the same quota as a live one.

Separately, a **per-IP** limit of 60 requests / minute / IP is applied **before** authentication: a flood of
bad-key requests is throttled at the edge before it ever reaches key verification, so abusive traffic can't
degrade auth for real tenants. This pre-auth `429` has body `{ "error": "rate_limited" }` and a `Retry-After`
header — back off and retry; it says nothing about your key.

## Request size

Every write endpoint caps the request body and returns `413 payload_too_large` — with a `detail` naming the
byte ceiling — when the declared `Content-Length` **or** the actual body exceeds it. `POST /api/v1/events` caps
at 256 KB; every other write endpoint (`import/jobs`, `conversions`, `tracking/lead`, `webhooks/trigger`,
`customers/webhook`) caps at 1 MB. A `413` is terminal — trim the payload and retry.

## Retryable vs terminal

<Note>
  **Retry** (transient — the request was not applied): `429 rate_limited` / `Too many requests`,
  `503 automations_disabled`, `503 geocoding_unavailable`, `503 auth_unavailable` (auth provider unreachable —
  the key was not checked), `502 geocoding_failed`, `500`, and `409 import_in_progress` (the original request
  with your `Idempotency-Key` is still running — retry with the same key for its result). Honor `Retry-After`
  where present and back off. Because writes are idempotent, retrying is always safe.

  **Do not retry unchanged** (terminal — fix the request): `400`, `401`, `403`, `413`, `422`, `404`. Correct the
  payload, key, scope, or automation id first. One exception: on `POST /api/v1/webhooks/trigger` the platform
  kill switch returns `400` with `error: "Automations are globally disabled"`. That one **is** transient — retry
  it rather than dropping the record. (`POST /api/v1/events` reports the same condition as a `503`.)
</Note>

## Recovering from gaps

There is no reconciliation sweep — Mailr won't pull missed records from your CRM. If deliveries failed or you
suspect a gap, **replay the affected events**. Idempotency (`dedup_id`, `deal_id`, address) means replaying a
whole time window is free: already-processed items come back as `duplicate` / `idempotent` and nothing is
double-mailed.
