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

# Import past jobs

> Stage a batch of completed jobs or customers for a reactivation campaign via POST /api/v1/import/jobs.

`POST /api/v1/import/jobs` stages a batch of past jobs (or customers) for a reactivation campaign. Mailr
geocodes every address and **stages** the batch — nothing is mailed and no campaign is created. The campaign
wizard reads the staged batch later, in the exact shape a CSV upload produces.

**Required scope:** `import:write` · **Limits:** 500 rows / request, 1 MB body, 10 batches / org / UTC day

<Note>
  Geocoding runs *before* any write, so a geocoder outage never leaves a half-written batch and never burns one
  of your 10 daily slots. One request is one batch; there is no cross-batch dedup, and duplicate addresses within
  a batch are geocoded independently (the wizard's selection step is where an operator prunes).
</Note>

<Note>
  **Retry-safe imports.** Pass an optional `Idempotency-Key` request header (any string ≤255 chars — a UUID or
  hash) to make retries safe. A retry carrying a key Mailr has already seen returns the **existing** batch and its
  stored counts with `200` (not `201`) — nothing is re-geocoded and no extra daily slot is spent. A retry that
  lands while the original request is **still processing** (geocoding can run for minutes) gets a
  `409 import_in_progress` with a `Retry-After` header — keep retrying with the same key until you get the
  finished batch. A key longer than 255 chars is a `422`. Keyless imports never dedup: each request stages a
  fresh batch. (This is replay-safety only — addresses are still never deduplicated, within a batch or across
  batches.)
</Note>

## Request

<ParamField header="Idempotency-Key" type="string">
  Optional replay key (≤255 chars). A repeat with the same key returns the existing batch (`200`) instead of
  staging a new one, or `409` while the original request is still processing; keyless requests never dedup.
</ParamField>

<ParamField body="source" type="string" required>
  `jobs` or `customers`.
</ParamField>

<ParamField body="rows" type="array" required>
  1–500 rows.
</ParamField>

<ParamField body="rows[].address" type="string">
  Full mailing address (≤500 chars). A row with **no** address is skipped (not an error). A non-string address
  *is* an error.
</ParamField>

<ParamField body="rows[].external_id" type="string">
  Your id for this job/customer (≤128 chars). Optional.
</ParamField>

<ParamField body="rows[].customer_name" type="string">
  Optional (≤200 chars).
</ParamField>

<ParamField body="rows[].completed_at" type="string (ISO-8601 date)">
  `YYYY-MM-DD` (a datetime's time part is dropped). Garbage is rejected with the offending row named. Optional.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://app.getmailr.com/api/v1/import/jobs \
    -H "Authorization: Bearer $MAILR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "source": "jobs",
      "rows": [
        {
          "external_id": "job-8842",
          "address": "3134 River Valley Dr, Austin, TX 78701",
          "customer_name": "Dana Reyes",
          "completed_at": "2026-05-02"
        },
        {
          "external_id": "job-8843",
          "address": "500 Congress Ave, Austin, TX 78701",
          "customer_name": "Sam Okafor"
        }
      ]
    }'
  ```

  ```js Node.js theme={null}
  const res = await fetch("https://app.getmailr.com/api/v1/import/jobs", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MAILR_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      source: "jobs",
      rows: [
        {
          external_id: "job-8842",
          address: "3134 River Valley Dr, Austin, TX 78701",
          customer_name: "Dana Reyes",
          completed_at: "2026-05-02",
        },
        {
          external_id: "job-8843",
          address: "500 Congress Ave, Austin, TX 78701",
          customer_name: "Sam Okafor",
        },
      ],
    }),
  });

  if (!res.ok) throw new Error(`Mailr API returned ${res.status}`);
  console.log(await res.json()); // { import_id, staged, ... }
  ```

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

  payload = {
      "source": "jobs",
      "rows": [
          {
              "external_id": "job-8842",
              "address": "3134 River Valley Dr, Austin, TX 78701",
              "customer_name": "Dana Reyes",
              "completed_at": "2026-05-02",
          },
          {
              "external_id": "job-8843",
              "address": "500 Congress Ave, Austin, TX 78701",
              "customer_name": "Sam Okafor",
          },
      ],
  }

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

  res.raise_for_status()
  print(res.json())  # {'import_id': ..., 'staged': 2, ...}
  ```

  ```php PHP theme={null}
  <?php
  $payload = [
      "source" => "jobs",
      "rows" => [
          [
              "external_id" => "job-8842",
              "address" => "3134 River Valley Dr, Austin, TX 78701",
              "customer_name" => "Dana Reyes",
              "completed_at" => "2026-05-02",
          ],
          [
              "external_id" => "job-8843",
              "address" => "500 Congress Ave, Austin, TX 78701",
              "customer_name" => "Sam Okafor",
          ],
      ],
  ];

  $ch = curl_init("https://app.getmailr.com/api/v1/import/jobs");
  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 !== 201) {
      throw new RuntimeException("Mailr API returned $status: $body");
  }
  print_r(json_decode($body, true)); // ['import_id' => ..., 'staged' => 2, ...]
  ```
</CodeGroup>

## Response

```json 201 Created theme={null}
{
  "import_id": "f0e1d2c3-0000-0000-0000-000000000abc",
  "staged": 2,
  "skipped_no_address": 0,
  "skipped_geocode_failed": 0
}
```

The three counts are exact and sum to the number of rows you sent:

<ResponseField name="staged" type="integer">
  Rows geocoded and staged.
</ResponseField>

<ResponseField name="skipped_no_address" type="integer">
  Rows with no address.
</ResponseField>

<ResponseField name="skipped_geocode_failed" type="integer">
  Rows whose address couldn't be geocoded.
</ResponseField>

Take `import_id` into the campaign wizard to build a reactivation campaign from the staged rows.

## Status codes

| Status        | `error`                                          | Meaning                                                                                                                   |
| ------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `200`         | —                                                | Replay of a prior `Idempotency-Key`: the existing batch and its stored counts (nothing new created).                      |
| `201`         | —                                                | Batch staged.                                                                                                             |
| `400`         | `invalid_json`                                   | Body isn't valid JSON.                                                                                                    |
| `401` / `403` | `Unauthorized`                                   | Bad key / no org / missing `import:write`.                                                                                |
| `409`         | `import_in_progress`                             | The request that first used this `Idempotency-Key` is still processing. Retry after `Retry-After` seconds for its result. |
| `413`         | `payload_too_large`                              | Body over 1 MB (declared or actual).                                                                                      |
| `422`         | `invalid_payload`                                | Bad `source`, too many rows, or a bad field type/date; `detail` names the row.                                            |
| `429`         | `rate_limited`                                   | Over 10 batches today. `Retry-After` = seconds to 00:00 UTC.                                                              |
| `502`         | `geocoding_failed`                               | Geocoder error. Nothing imported; retry later.                                                                            |
| `503`         | `geocoding_unavailable` / `automations_disabled` | Geocoder down, or the platform kill switch (`Retry-After: 300`). Nothing imported; retry later.                           |
| `500`         | `Internal server error`                          | Unexpected error.                                                                                                         |

<Warning>
  On `429`, `502`, or `503`, **nothing** was staged and no daily slot was consumed — retry the whole batch later.
</Warning>
