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

# Stage past jobs or customers for a reactivation campaign

> Push a batch of up to 500 completed jobs (or customers). 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.

Geocoding happens before any write, so a geocoder outage never leaves a
half-written batch and never burns a daily slot. Rows with no address are
skipped (not an error); rows that fail geocoding are staged as
`geocode_failed`. Duplicate addresses within a batch are allowed and
geocoded independently — the wizard's selection step is where an operator
prunes.

Limits: at most **500 rows per request**, a **1 MB request body**, and
**10 batches per org per UTC day**.

Retries are replay-safe: send an `Idempotency-Key` header and a repeat
with the same key returns the existing batch (200, not 201) instead of
staging a new one, without re-geocoding or spending a daily slot. A
retry that lands while the original request is still processing gets a
409 `import_in_progress` (with `Retry-After`) — keep retrying with the
same key until the finished batch comes back. Keyless imports never
dedup — addresses are never deduplicated either way.




## OpenAPI

````yaml /api-reference/openapi.yaml post /api/v1/import/jobs
openapi: 3.1.0
info:
  title: Mailr Developer API
  version: 1.0.0
  description: >
    The Mailr Developer API lets a contractor's own CRM or automation stack
    drive

    Mailr directly with a per-tenant bearer key — no vendor polling, no Zapier
    in

    the middle. You push **events** when a job or deal changes, Mailr matches
    them

    against the automations you configured in the app and mails per-recipient

    Street View postcards; you push **won deals** and **leads** for revenue

    attribution; you **subscribe** an endpoint to receive captured leads back

    (HMAC-signed); and you **stage past jobs** for a reactivation campaign.


    ## Authentication


    Every endpoint here authenticates with an organization-scoped API key

    presented as `Authorization: Bearer ak_…`. The key alone identifies the

    tenant — you never pass an `org_id`, connection id, or batch id in the body
    or

    query; anything you could name is derived from the key. Create keys in the

    Mailr app under your workspace name (bottom-left) -> Manage account ->

    API keys (workspace Owner only).


    ## Scopes


    Keys may declare scopes. Policy (v1): if a key declares **any** scopes, it
    must

    include the scope an operation requires; a key that declares **no** scopes
    is

    treated as unrestricted and passes every check. The scopes in use are

    `events:write`, `import:write`, and `webhooks:inbound`. Each operation below

    documents its required scope under `x-required-scope`.


    ## Base URL


    Production is `https://app.getmailr.com`. All paths below are relative to
    it.


    ## Versioning


    The public API is versioned under `/api/v1/*` — every endpoint documented

    here lives under it. The Mailr Zapier app targets `/api/v1/*`; build new

    integrations against the same prefix.


    ## Idempotency


    Write endpoints are idempotent on a caller-supplied key: `dedup_id` on
    events,

    `deal_id`/address on conversions and leads. Imports are **not**
    deduplicated:

    one request is one batch, and duplicate addresses within or across batches
    are

    staged independently.

    Replaying the same request is safe and is reported back to you (e.g.

    `duplicate: true`, `idempotent: true`, or `reason: "duplicate"`).


    ## Auth-path errors (all endpoints)


    Besides each endpoint's own statuses, every authenticated endpoint can

    return two RETRYABLE auth-path errors: `429` `{ "error": "rate_limited" }`

    when the pre-auth per-IP limit (60/min/IP) trips, and `503`

    `{ "error": "auth_unavailable" }` when the auth provider is temporarily

    unreachable (your key was NOT checked). Both carry `Retry-After`. Neither

    means your key is bad — only a `401` does.


    ## Delivery cadence


    There is **no reconciliation sweep**. Mailr does not reach 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.
  contact:
    name: Mailr
    url: https://app.getmailr.com
servers:
  - url: https://app.getmailr.com
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Events
    description: Push CRM/automation events that trigger campaigns (Developer API v1).
  - name: Imports
    description: >-
      Stage a batch of past jobs/customers for a reactivation campaign
      (Developer API v1).
  - name: Leads
    description: Receive captured leads (subscribe) and poll recent leads.
  - name: Attribution
    description: >-
      Report won deals and synced leads so revenue attributes to the campaign
      that mailed the address.
  - name: Customers
    description: Register existing customers for address suppression.
  - name: Account
    description: Identify the key's organization and list its automations.
paths:
  /api/v1/import/jobs:
    post:
      tags:
        - Imports
      summary: Stage past jobs or customers for a reactivation campaign
      description: >
        Push a batch of up to 500 completed jobs (or customers). 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.


        Geocoding happens before any write, so a geocoder outage never leaves a

        half-written batch and never burns a daily slot. Rows with no address
        are

        skipped (not an error); rows that fail geocoding are staged as

        `geocode_failed`. Duplicate addresses within a batch are allowed and

        geocoded independently — the wizard's selection step is where an
        operator

        prunes.


        Limits: at most **500 rows per request**, a **1 MB request body**, and

        **10 batches per org per UTC day**.


        Retries are replay-safe: send an `Idempotency-Key` header and a repeat

        with the same key returns the existing batch (200, not 201) instead of

        staging a new one, without re-geocoding or spending a daily slot. A

        retry that lands while the original request is still processing gets a

        409 `import_in_progress` (with `Retry-After`) — keep retrying with the

        same key until the finished batch comes back. Keyless imports never

        dedup — addresses are never deduplicated either way.
      operationId: postImportJobs
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 255
          description: |
            Optional replay key. A retry carrying a key already seen returns the
            existing batch and its stored counts (200, not 201) without
            re-geocoding or spending a daily slot — or 409 while the original
            request is still processing. Over 255 chars → 422. Keyless imports
            never dedup.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportRequest'
            examples:
              jobs:
                value:
                  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
      responses:
        '200':
          description: |
            Replay of a prior `Idempotency-Key` — the existing batch and its
            stored counts are returned (nothing new was created).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportResult'
              example:
                import_id: f0e1d2c3-0000-0000-0000-000000000abc
                staged: 2
                skipped_no_address: 0
                skipped_geocode_failed: 0
        '201':
          description: Batch staged. Counts equal the actual rows.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportResult'
              example:
                import_id: f0e1d2c3-0000-0000-0000-000000000abc
                staged: 2
                skipped_no_address: 0
                skipped_geocode_failed: 0
        '400':
          description: 'Body is not valid JSON. Body `{ "error": "invalid_json" }`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: invalid_json
        '401':
          description: >-
            Missing/invalid/revoked/expired key. Body `{ "error": "Unauthorized"
            }`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Unauthorized
        '403':
          description: >-
            Key resolves to no org, or lacks `import:write`. Body `{ "error":
            "Unauthorized" }`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Unauthorized
        '409':
          description: |
            The request that first used this `Idempotency-Key` is still
            processing (geocoding can run for minutes). Nothing new was staged;
            retry with the same key after `Retry-After` seconds to get the
            finished batch.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: import_in_progress
                detail: >-
                  A request with this Idempotency-Key is still being processed.
                  Retry later for its result.
        '413':
          description: >-
            Request body is over 1 MB (declared `Content-Length` or actual
            bytes). Nothing was imported.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: payload_too_large
                detail: Body must be at most 1000000 bytes.
        '422':
          description: >-
            The batch failed validation (bad source, too many rows, bad field
            type/date).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                tooManyRows:
                  value:
                    error: invalid_payload
                    detail: rows must contain at most 500 rows, got 512
                badDate:
                  value:
                    error: invalid_payload
                    detail: >-
                      rows[3].completed_at must be an ISO-8601 date
                      (YYYY-MM-DD), got "May 2"
        '429':
          description: More than 10 batches created today (UTC).
          headers:
            Retry-After:
              description: Seconds until 00:00 UTC.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: rate_limited
                detail: At most 10 import batches per day. Resets at 00:00 UTC.
        '500':
          description: >-
            Unexpected server error. Body `{ "error": "Internal server error"
            }`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Internal server error
        '502':
          description: Geocoding failed. Nothing was imported; retry later.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: geocoding_failed
                detail: Address lookup failed. Nothing was imported; retry later.
        '503':
          description: >
            Geocoding is temporarily unavailable, or the platform kill switch
            has

            globally disabled automations (`Retry-After: 300`). Nothing was

            imported; retry later.
          headers:
            Retry-After:
              description: >-
                Seconds to wait before retrying (sent with
                `automations_disabled`).
              schema:
                type: string
                example: '300'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                geocodingUnavailable:
                  value:
                    error: geocoding_unavailable
                    detail: >-
                      Address lookup is temporarily unavailable. Nothing was
                      imported; retry later.
                automationsDisabled:
                  value:
                    error: automations_disabled
                    detail: Imports are temporarily disabled. Retry later.
components:
  schemas:
    ImportRequest:
      type: object
      required:
        - source
        - rows
      properties:
        source:
          type: string
          enum:
            - jobs
            - customers
        rows:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: '#/components/schemas/ImportRow'
    ImportResult:
      type: object
      required:
        - import_id
        - staged
        - skipped_no_address
        - skipped_geocode_failed
      properties:
        import_id:
          type: string
          format: uuid
        staged:
          type: integer
          description: Rows geocoded and staged.
        skipped_no_address:
          type: integer
          description: Rows with no address.
        skipped_geocode_failed:
          type: integer
          description: Rows whose address could not be geocoded.
    Error:
      type: object
      description: >
        Error body. `error` is a stable machine code; `detail` (when present) is
        a

        human-readable explanation naming the offending field. Note two families

        of code style coexist across the API: snake_case machine codes on the v1

        and rate-limited endpoints (`invalid_payload`, `rate_limited`,

        `automations_disabled`) and human sentences on the legacy endpoints

        (`Unauthorized`, `No address found in payload`). Both are returned in
        the

        `error` field.
      required:
        - error
      properties:
        error:
          type: string
          example: invalid_payload
        detail:
          type: string
          example: event_type is required
    ImportRow:
      type: object
      properties:
        external_id:
          type:
            - string
            - 'null'
          maxLength: 128
          description: Your id for this job/customer. Optional.
        address:
          type: string
          maxLength: 500
          description: >-
            Full mailing address. A row with no address is skipped (not an
            error).
        customer_name:
          type:
            - string
            - 'null'
          maxLength: 200
        completed_at:
          type:
            - string
            - 'null'
          description: >-
            ISO-8601 date (YYYY-MM-DD; a datetime's time part is dropped).
            Optional.
          example: '2026-05-02'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ak_
      description: 'Organization-scoped Mailr API key. Header: `Authorization: Bearer ak_…`.'

````