## Get a batch run's progress

**get** `/v1/invitation_batches/{batch}`

Retrieves a run's progress: its status, how many recipients stand in
each state, and when the rest are projected to finish.

**Behavior**

- One call regardless of run size: the cost does not grow with the
  recipient count.
- `counts` always carries all five states, zeros included, and always
  sums to `recipient_count`.
- `eta` is an estimate from a rate that moves, and is absent rather than
  zero when none can honestly be quoted.
- Unknown ids, and another brand's; return HTTP 404, the same answer.

### Path Parameters

- `batch: string`

### Returns

- `id: string`

  Run id (`invb_…`).

- `accepted_count: number`

  How many recipients the run will attempt: `recipient_count` minus the rows the submit screen skipped. The same number the submit response returned, and frozen with it.

- `counts: object { cancelled, failed, pending, 2 more }`

  How many of a run's recipients stand in each state. EVERY key is always present, zero included: a client reads five numbers instead of branching on which keys exist, and a state that has not happened yet is `0` rather than missing.

  - `cancelled: number`

    Stopped by a cancel before it was attempted.

  - `failed: number`

    Accepted at submit, then permanently refused before sending; consent withdrawn in between, a superseded or closed chat, a revoked template grant. The row's `reason` says which.

  - `pending: number`

    Not yet attempted. This is the number the ETA projects.

  - `sent: number`

    Handed to the invitation pipeline: an `invitation_id` exists on that row. Delivery to Apple is reported by that invitation's own status and webhooks, not here.

  - `skipped: number`

    Dropped by the SUBMIT screen under `on_invalid: skip_invalid`, with the reason the submit response gave. Frozen at creation; nothing later moves a row into or out of it.

- `created_at: string`

- `recipient_count: number`

  How many recipients the run was SUBMITTED with, frozen at creation; dropped rows included. Always the length of the array you sent, and always `counts`' total.

- `status: "queued" or "running" or "completed" or "cancelled"`

  `queued` (created, nothing attempted yet) → `running` (at least one
  recipient attempted, some still pending) → `completed` (no recipient is
  pending; every one is `sent`, `failed`, `cancelled` or `skipped`), or
  `cancelled` when you stopped it. `completed` does NOT mean every invitation
  was delivered: it means the run has nothing left to attempt, and per-row
  outcomes are in `counts` and the recipient results.

  - `"queued"`

  - `"running"`

  - `"completed"`

  - `"cancelled"`

- `template_id: string`

- `updated_at: string`

  Last change to the run row itself (its `status`). NOT bumped by a per-recipient transition, so it is a run-lifecycle timestamp and not a progress heartbeat; read `counts` for progress.

- `eta: optional object { at, queued_ahead, rate_per_second, 2 more }`

  The completion estimate. Absent when none can honestly be quoted; see the model.

  - `at: string`

    The same estimate as a timestamp on OUR clock; `seconds` from when this response was composed, so a client does not have to correct for its own skew.

  - `queued_ahead: number`

    How many other invitations of yours are queued ahead of this run's remaining rows in the same brand queue. A run does not clear in isolation; another campaign submitted first is counted here, and so are this run's own recipients that have already been handed to the delivery queue.

  - `rate_per_second: number`

    Recipients per second the estimate was taken at: `min(your brand's effective rate, our overall rate / active brands)`.

  - `remaining: number`

    How many of this run's recipients the estimate covers; `counts.pending`.

  - `seconds: number`

    Estimated seconds until the last remaining recipient is released. `0` when nothing is pending.

- `not_before: optional string`

  The run's scheduled start, if you gave it one. A `queued` run with a `not_before` in the future is waiting for its own schedule, not for us.

- `spread_over_seconds: optional number`

  The run's smoothing window, if you gave it one.

### Example

```http
curl https://messages.api.linqapp.com/v1/invitation_batches/$BATCH \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY"
```

#### Response

```json
{
  "id": "invb_0d5c19",
  "status": "running",
  "template_id": "invt_a4f2c718d0",
  "recipient_count": 2,
  "accepted_count": 2,
  "counts": {
    "pending": 1,
    "sent": 1,
    "failed": 0,
    "cancelled": 0,
    "skipped": 0
  },
  "created_at": "2026-08-06T14:00:00Z",
  "updated_at": "2026-08-06T14:00:07Z"
}
```
