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

# Mailr Developer API

> Drive Mailr's per-recipient postcard engine directly from your own CRM or automation stack with a per-tenant bearer key.

The Mailr Developer API lets a contractor's own CRM or automation stack drive Mailr directly — no vendor
polling, no Zapier in the middle. There are four things you can do:

<CardGroup cols={2}>
  <Card title="Send events" icon="zap" href="/api-reference/send-events">
    Push a `job.completed` / `deal.won` / contact event; Mailr matches it against your automations and mails
    a per-recipient Street View postcard.
  </Card>

  <Card title="Receive leads" icon="inbox" href="/api-reference/receive-leads">
    Subscribe an endpoint to receive landing-page leads back, HMAC-signed.
  </Card>

  <Card title="Attribution & revenue" icon="trending-up" href="/api-reference/attribution">
    Report won deals and synced leads so revenue attributes to the campaign that mailed the address.
  </Card>

  <Card title="Import past jobs" icon="upload" href="/api-reference/import-jobs">
    Stage a batch of completed jobs for a reactivation campaign.
  </Card>
</CardGroup>

<Note>
  The OpenAPI spec behind the **Endpoints** section of this reference is the source of truth for every path,
  field, status code and header. These pages explain the concepts; the spec pins the shapes.
</Note>

## Base URL

All paths are relative to:

```
https://app.getmailr.com
```

## Versioning

The public API is versioned under `/api/v1/*`, and every endpoint in these docs uses that prefix. The
[Mailr Zapier app](/integrations/zapier) targets `/api/v1/*`; build new integrations against the same prefix.

## Authentication

Every endpoint authenticates with an **organization-scoped API key** presented as a bearer token:

```
Authorization: Bearer ak_live_xxxxxxxxxxxxxxxxxxxxxxxx
```

The key alone identifies the tenant. You never pass an `org_id`, connection id, or batch id — anything you
could name is derived from the key. Create a key in the Mailr app: click your workspace name (bottom-left) → **Manage account** → **API keys**. Only the workspace Owner can create or revoke keys.

Verify a key against your org with `GET /api/v1/me`:

<CodeGroup>
  ```bash curl theme={null}
  curl https://app.getmailr.com/api/v1/me \
    -H "Authorization: Bearer $MAILR_API_KEY"
  ```

  ```js Node.js theme={null}
  const res = await fetch("https://app.getmailr.com/api/v1/me", {
    headers: { Authorization: `Bearer ${process.env.MAILR_API_KEY}` },
  });

  if (!res.ok) throw new Error(`Mailr API returned ${res.status}`);
  console.log(await res.json()); // { org_id: "org_2abcXYZ", org_name: "Reyes Roofing" }
  ```

  ```python Python theme={null}
  import os
  import requests

  res = requests.get(
      "https://app.getmailr.com/api/v1/me",
      headers={"Authorization": "Bearer " + os.environ["MAILR_API_KEY"]},
  )

  res.raise_for_status()
  print(res.json())  # {'org_id': 'org_2abcXYZ', 'org_name': 'Reyes Roofing'}
  ```

  ```php PHP theme={null}
  <?php
  $ch = curl_init("https://app.getmailr.com/api/v1/me");
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      "Authorization: Bearer " . getenv("MAILR_API_KEY"),
  ]);

  $body = curl_exec($ch);
  $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  curl_close($ch);

  if ($status !== 200) {
      throw new RuntimeException("Mailr API returned $status: $body");
  }
  print_r(json_decode($body, true)); // ['org_id' => 'org_2abcXYZ', 'org_name' => 'Reyes Roofing']
  ```
</CodeGroup>

```json Response theme={null}
{ "org_id": "org_2abcXYZ", "org_name": "Reyes Roofing" }
```

### Scopes

Keys may declare scopes. The policy in v1 is deliberately forgiving:

* A key that declares **no scopes** is treated as unrestricted and passes every check.
* A key that declares **any scopes** must include the scope an operation requires, or the call returns `403`.

<ParamField path="events:write" type="scope">
  Required by `POST /api/v1/events`.
</ParamField>

<ParamField path="import:write" type="scope">
  Required by `POST /api/v1/import/jobs`.
</ParamField>

<ParamField path="webhooks:inbound" type="scope">
  Required by the lead, attribution, customer, trigger, subscribe, `/api/v1/me`, and `/api/v1/automation/list`
  endpoints.
</ParamField>

<Warning>
  Failed auth never reveals whether a given key exists. `401` covers missing/invalid/revoked/expired keys;
  `403` covers a key that resolves to no organization or is missing a required scope.
</Warning>

## Idempotency

Every write is idempotent on a caller-supplied key, so retries and replays are safe:

| Endpoint                        | Idempotency key                                                                  |
| ------------------------------- | -------------------------------------------------------------------------------- |
| `POST /api/v1/events`           | `dedup_id` (namespaced per bound automation)                                     |
| `POST /api/v1/conversions`      | `deal_id`, or (absent one) the normalized address plus the deal's close **date** |
| `POST /api/v1/tracking/lead`    | address (collapses onto an existing lead)                                        |
| `POST /api/v1/webhooks/trigger` | `deal_id`                                                                        |
| `POST /api/v1/import/jobs`      | one request = one batch (no cross-batch dedup)                                   |

The response tells you when a replay was absorbed: `duplicate: true`, `idempotent: true`, or an outcome with
`reason: "duplicate"`.

## Delivery cadence — replay, don't reconcile

<Warning>
  There is **no reconciliation sweep**. Mailr never reaches back into your CRM to pull anything it missed — the
  caller *is* the sync. If a push fails, or you suspect a gap, **replay your events**. Idempotency makes replays
  free, so replaying a whole window is the correct recovery, not the exception.
</Warning>

## What's not in this API

The dashboard's own browser-authenticated endpoints (for example
`/api/integrations/custom/event-config`) are session-authed and are **not** part of this bearer-key API. Only
the `ak_`-key endpoints documented here are public.

## Next steps

<CardGroup cols={2}>
  <Card title="Send your first event" icon="send" href="/api-reference/send-events">
    The envelope, trigger bindings, outcomes, and dry run.
  </Card>

  <Card title="Go-live checklist" icon="rocket" href="/api-reference/go-live">
    Eight things to confirm before production traffic.
  </Card>
</CardGroup>
