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

# Attribution & revenue

> Report won deals and synced leads so revenue and conversions attribute to the campaign that mailed the address.

[Attribution](/results/attribution) closes the loop: you tell Mailr about deals and leads happening in your CRM, and Mailr matches each
address back to the campaign that mailed it (geo-first, org-scoped, within a 180-day window). Two endpoints,
both scoped `webhooks:inbound`, both rate-limited to **60 requests / minute / org**.

<Note>
  The org is always taken from the API key, never from the request body — an `org_id` in the body is ignored.
  Both endpoints tolerate HubSpot/Zapier field shapes: most fields can be sent flat or nested under a
  `properties` object.
</Note>

## Report a won deal

`POST /api/v1/conversions` attributes revenue. Mailr resolves the deal's address to a mailed campaign and upserts
an idempotent conversion; the address is also recorded as an existing customer regardless of whether revenue
attributes. Unmatched deals are discarded.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://app.getmailr.com/api/v1/conversions \
    -H "Authorization: Bearer $MAILR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "address": "3134 River Valley Dr, Austin, TX 78701",
      "deal_id": "deal-9931",
      "deal_amount": "6400.00",
      "deal_stage": "closed_won",
      "deal_name": "Reyes roof replacement",
      "contact_first_name": "Dana",
      "contact_last_name": "Reyes",
      "contact_email": "dana.reyes@example.com",
      "deal_date": "2026-08-19"
    }'
  ```

  ```js Node.js theme={null}
  const res = await fetch("https://app.getmailr.com/api/v1/conversions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MAILR_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      address: "3134 River Valley Dr, Austin, TX 78701",
      deal_id: "deal-9931",
      deal_amount: "6400.00",
      deal_stage: "closed_won",
      deal_name: "Reyes roof replacement",
      contact_first_name: "Dana",
      contact_last_name: "Reyes",
      contact_email: "dana.reyes@example.com",
      deal_date: "2026-08-19",
    }),
  });

  if (!res.ok) throw new Error(`Mailr API returned ${res.status}`);
  console.log(await res.json()); // { matched: true, ... } or { matched: false }
  ```

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

  payload = {
      "address": "3134 River Valley Dr, Austin, TX 78701",
      "deal_id": "deal-9931",
      "deal_amount": "6400.00",
      "deal_stage": "closed_won",
      "deal_name": "Reyes roof replacement",
      "contact_first_name": "Dana",
      "contact_last_name": "Reyes",
      "contact_email": "dana.reyes@example.com",
      "deal_date": "2026-08-19",
  }

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

  res.raise_for_status()
  print(res.json())  # {'matched': True, ...} or {'matched': False}
  ```

  ```php PHP theme={null}
  <?php
  $payload = [
      "address" => "3134 River Valley Dr, Austin, TX 78701",
      "deal_id" => "deal-9931",
      "deal_amount" => "6400.00",
      "deal_stage" => "closed_won",
      "deal_name" => "Reyes roof replacement",
      "contact_first_name" => "Dana",
      "contact_last_name" => "Reyes",
      "contact_email" => "dana.reyes@example.com",
      "deal_date" => "2026-08-19",
  ];

  $ch = curl_init("https://app.getmailr.com/api/v1/conversions");
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      "Authorization: Bearer " . getenv("MAILR_API_KEY"),
      "Content-Type: application/json",
  ]);
  curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));

  $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)); // ['matched' => true, ...] or ['matched' => false]
  ```
</CodeGroup>

```json 200 OK — matched theme={null}
{
  "matched": true,
  "attribution_type": "direct",
  "campaign_id": "aa11bb22-0000-0000-0000-0000000000cc",
  "match_method": "geo",
  "match_confidence": "high",
  "conversion_id": "d4e5f6a7-0000-0000-0000-000000000030"
}
```

```json 200 OK — unmatched theme={null}
{ "matched": false }
```

<ParamField body="address" type="string" required>
  The deal's service address. Also accepted as `properties.address` / `deal_address` / `install_address`.
</ParamField>

<ParamField body="deal_id" type="string">
  Your deal id. Drives idempotency. Also `properties.hs_object_id` / `objectId`.
</ParamField>

<ParamField body="deal_amount" type="string | number">
  Deal value. A number or numeric string.
</ParamField>

<ParamField body="deal_date" type="string (ISO-8601)">
  Close date. Drives last-touch attribution within the 180-day window.
</ParamField>

<ParamField body="campaign_id" type="string">
  Optional Mailr campaign id for an exact, address-independent attribution.
</ParamField>

<ParamField body="deal_stage, deal_name, contact_first_name, contact_last_name, contact_name, contact_email" type="string">
  Optional deal/contact details, stored with the conversion.
</ParamField>

## Sync a new lead (indirect)

`POST /api/v1/tracking/lead` records a lead your CRM captured elsewhere as an **indirect** lead against the mailed
campaign (`source = crm_sync`). Unmatched leads are discarded. Idempotency uses `crm_record_id` first, then a
non-empty email or phone. Records with none of those identity fields stay distinct instead of sharing an empty
email key. When a CRM record originated from Mailr, echo its `mailr_lead_id`; Mailr returns
`{ "matched": false, "reason": "loop_guard" }` without re-ingesting it. These leads are **never** delivered back outbound. If the
address already has a direct landing-page lead, the sync collapses onto it and returns `duplicate: true` with
`attribution_type: "direct"` — the existing lead is not downgraded.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://app.getmailr.com/api/v1/tracking/lead \
    -H "Authorization: Bearer $MAILR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "address": "3134 River Valley Dr, Austin, TX 78701",
      "first_name": "Dana",
      "last_name": "Reyes",
      "email": "dana.reyes@example.com",
      "phone": "+15125550143",
      "crm_record_id": "crm-contact-4821",
      "created_at": "2026-08-19T15:04:00Z"
    }'
  ```

  ```js Node.js theme={null}
  const res = await fetch("https://app.getmailr.com/api/v1/tracking/lead", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MAILR_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      address: "3134 River Valley Dr, Austin, TX 78701",
      first_name: "Dana",
      last_name: "Reyes",
      email: "dana.reyes@example.com",
      phone: "+15125550143",
      created_at: "2026-08-19T15:04:00Z",
    }),
  });

  if (!res.ok) throw new Error(`Mailr API returned ${res.status}`);
  console.log(await res.json()); // { matched: true, duplicate: false, ... }
  ```

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

  payload = {
      "address": "3134 River Valley Dr, Austin, TX 78701",
      "first_name": "Dana",
      "last_name": "Reyes",
      "email": "dana.reyes@example.com",
      "phone": "+15125550143",
      "created_at": "2026-08-19T15:04:00Z",
  }

  res = requests.post(
      "https://app.getmailr.com/api/v1/tracking/lead",
      headers={"Authorization": "Bearer " + os.environ["MAILR_API_KEY"]},
      json=payload,
  )

  res.raise_for_status()
  print(res.json())  # {'matched': True, 'duplicate': False, ...}
  ```

  ```php PHP theme={null}
  <?php
  $payload = [
      "address" => "3134 River Valley Dr, Austin, TX 78701",
      "first_name" => "Dana",
      "last_name" => "Reyes",
      "email" => "dana.reyes@example.com",
      "phone" => "+15125550143",
      "created_at" => "2026-08-19T15:04:00Z",
  ];

  $ch = curl_init("https://app.getmailr.com/api/v1/tracking/lead");
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      "Authorization: Bearer " . getenv("MAILR_API_KEY"),
      "Content-Type: application/json",
  ]);
  curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));

  $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)); // ['matched' => true, 'duplicate' => false, ...]
  ```
</CodeGroup>

```json 200 OK — matched theme={null}
{
  "matched": true,
  "duplicate": false,
  "lead_id": "c1d2e3f4-0000-0000-0000-000000000020",
  "campaign_id": "aa11bb22-0000-0000-0000-0000000000cc",
  "attribution_type": "indirect"
}
```

```json 200 OK — unmatched theme={null}
{ "matched": false }
```

## Trigger a specific automation directly

`POST /api/v1/webhooks/trigger?id=<automationId>` fires one named automation — the older mechanism the Zapier app and
HubSpot workflows use. It's still supported, but for new integrations prefer
[`POST /api/v1/events`](/api-reference/send-events): it binds by event type instead of hardcoding an automation id, and
reports per-automation outcomes. Field extraction is highly tolerant of CRM/Zapier mappings; it's idempotent on
`deal_id` (a repeat short-circuits before geocoding), and a `deal_won` automation also attributes revenue as a
side effect.

```json 202 Accepted theme={null}
{
  "success": true,
  "automation": "Post-job reactivation",
  "customer_added": true,
  "queued": true,
  "queue_id": "e5f6a7b8-0000-0000-0000-000000000040",
  "idempotent": false,
  "address": "3134 River Valley Dr, Austin, TX 78701, USA"
}
```

Discover automation ids with `GET /api/v1/automation/list` → `{ "automations": [{ id, name, enabled }] }`.

<Warning>
  A foreign automation id returns the same `404` as a missing one — a key can't probe another org's automation
  ids.
</Warning>

## Registering existing customers

`POST /api/v1/customers/webhook` suppresses addresses from future mailings. Send a single `{ address, notes? }` or
a batch `{ addresses: [...] }`; it upserts on the normalized address per org and returns `{ added, total }`. A
batch is capped at **1000 addresses** — more is a `422`. The endpoint is rate-limited to **60 requests / minute /
org** in its own bucket (separate from the two attribution endpoints above); over-limit responses are `429` with
`Retry-After: 60`.
