> ## 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.

# Go-live checklist

> Eight things to confirm before pointing production traffic at the Mailr Developer API.

Work through these eight before you send production traffic. Each maps to behavior enforced by the API.

<Steps>
  <Step title="Mint an org-scoped key with the right scopes">
    Create an `ak_` key in the Mailr app under your workspace name → **Manage account** → **API keys** (Owner-only). Give it `events:write`
    and/or `import:write` and/or `webhooks:inbound` as needed — or leave it unscoped (unscoped keys pass every
    check). A key scoped to only `import:write` **cannot** post events, and vice versa.
  </Step>

  <Step title="Verify the key resolves to the right org">
    ```bash theme={null}
    curl https://app.getmailr.com/api/v1/me -H "Authorization: Bearer $MAILR_API_KEY"
    ```

    Confirm `org_id` / `org_name` is the tenant you expect. A `403` here means either the key is missing the
    `webhooks:inbound` scope that `/api/v1/me` requires, or it isn't tied to a billing owner. If you scoped the
    key to `events:write` only, add `webhooks:inbound` (or leave the key unscoped) to use this check.
  </Step>

  <Step title="Bind your automations">
    Send at least one **live** event so your `event_type` appears in the app's binding picker (dry-run events write nothing, so they don't register the event type),
    then set an automation's trigger to **Custom API** and bind it to that `event_type` (and `object_type` if
    you want to pin one). Nothing mails until a binding exists — an unbound event returns
    `matched_automations: 0`.
  </Step>

  <Step title="Dry-run your real payload">
    POST your actual event with `"dry_run": true` and confirm the outcome is `reason: "queued"` for the
    automation you expect. Dry run exercises the full binding/filter/address/dedup path but writes nothing.
  </Step>

  <Step title="Get your addresses and property keys right">
    Ensure every reserved key is spelled exactly (`street`, `city`, `state`, `zip`, `client_name`,
    `client_email`, `client_phone`, `amount`, `status`, `mailr_lead_id`) and every other key is `cf_`-prefixed
    — a mis-spelled reserved key is a `422`. Confirm each mailable record has `street` **plus** `city` or `zip`,
    or it reports `no_address`.
  </Step>

  <Step title="Subscribe your lead endpoint and verify signatures">
    `POST /api/v1/webhooks/subscribe`, **store the `signing_secret`**, and implement HMAC verification over
    `"<X-Mailr-Timestamp>.<rawBody>"` with a 300-second freshness window (see
    [Receive leads](/api-reference/receive-leads#signature-verification)). Use the raw request body, not re-serialized
    JSON.
  </Step>

  <Step title="Close the loop guard">
    On any event your automation derives from a Mailr-delivered lead, echo `mailr_lead_id` into
    `record.properties.mailr_lead_id`. Without it, a lead Mailr sourced can start a campaign back at its own
    address — an infinite echo. Confirm such an event reports `reason: "loop_guard"`.
  </Step>

  <Step title="Handle limits, retries, and replay">
    Read `Retry-After` on `429`/`503` and back off. Treat `4xx` as terminal (fix the request); treat
    `429`/`5xx` as retryable. Wire stable idempotency keys (`dedup_id`, `deal_id`) so that your gap-recovery
    plan is simply **replay the window** — there is no reconciliation sweep.
  </Step>
</Steps>

<Note>
  Once all eight pass against a real key using `dry_run` and a low-volume test automation, flip your integration
  to full production traffic. Because every write is idempotent, a cautious first run that replays its first
  window is safe.
</Note>
