# Retrieve delivered records


The same endpoint supports incremental delivery and replay of an already-readable range.

```http
GET /v1/live/pull
Authorization: Bearer <API_KEY>
```

For a guided first request, see [Get your first authorized data](/api/api-data).

## Hosts and authentication

| Environment | API origin |
| --- | --- |
| Staging | `https://api.staging.humanbased.ai` |
| Production | `https://api.humanbased.ai` |

Use a full organization API key issued for the selected environment. Pass it in the `Authorization` header with the exact `Bearer ` prefix. Do not include credentials in the query string.

The key must be active, unexpired, permit data delivery, and cover the requested subscription. The subscription must belong to the key's organization and be active. A browser session, CLI sign-in session, masked key, or Campaign ID is not a replacement.

Authentication does not expand the data's usage terms. Confirm permitted use separately, including restrictions on sharing, training, retention, or redistribution that apply to the data.

## Reading modes and recovery

Choose the mode explicitly before making a request.

| | Replay | Incremental delivery |
| --- | --- | --- |
| Request | Include `from=beginning`. | Omit `from`. |
| Purpose | Read or reconstruct the subscription's readable range. | Obtain records beyond the shared reading position. |
| Pagination | Return the latest `next_cursor` as `cursor`; preserve mode and filters. | Repeat the request while `has_more` is true; the server holds the position. |
| Shared position | Does not move it. | Advances it for the whole subscription, including other keys and consumers. |
| Metering | Creates no new metered pull charge; `receipt` is null. | May charge for newly delivered records according to the subscription. |
| Filters | Optional task and campaign filters narrow the permitted range. | Task and campaign filters are rejected. |

For **zero-priced subscriptions**, replay can include eligible delivered records beyond the incremental reading position. For **priced subscriptions**, replay stays within the previously acquired range. A full pull means this readable scope; it does not bypass payment or grant access to all campaign submissions.

Replay is suitable for recovering a lost local copy. Retrying it does not advance the feed. The endpoint does not promise a frozen snapshot: new deliveries or corrections may affect what is readable over time.

Incremental delivery is stateful even though it uses HTTP `GET`. Save each complete response before requesting more. If a response is lost, the server may already have advanced the shared position. Another incremental request can move on to the next page. Recover by replaying the acquired range and reconciling records against your local copy. For campaign submissions, use `submission_id` to identify repeat records; do not use timestamps as cursors.

## Query parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `subscription_id` | string | Yes | The active subscription to read. It is not a Campaign ID. |
| `limit` | integer | No | Maximum records per page: 1–200. Default: 50. |
| `from` | string | No | Set to `beginning` for replay. Omit for incremental delivery. No other value is supported. |
| `cursor` | string | No | For replay, use the previous response's opaque `next_cursor`. Omit on the first page. |
| `task_ids` | string | No | Comma-separated task IDs; replay only. Narrows the subscription scope. |
| `campaign_ids` | string | No | Comma-separated campaign IDs; replay only. Narrows the subscription scope. |

The two filters are applied together when both are present. Each list accepts up to 200 IDs; additional entries are truncated by the current implementation, so split larger requests yourself. Whitespace is trimmed and empty entries are ignored. An entirely empty filter means **no filter**, not “return nothing”. Validate IDs before sending them.

A filter outside the subscription's scope normally returns an empty page; it never expands authorization. Preserve the same filters on every replay page. Treat cursors as opaque, and never generate one from a Task ID, Submission ID, or timestamp.

## Example: replay one page

Set `HB_API_KEY` securely and set `HB_SUBSCRIPTION_ID` to your own production subscription before running this request:

```sh
curl --silent --show-error --get \
  'https://api.humanbased.ai/v1/live/pull' \
  --header "Authorization: Bearer $HB_API_KEY" \
  --data-urlencode "subscription_id=$HB_SUBSCRIPTION_ID" \
  --data-urlencode 'from=beginning' \
  --data-urlencode 'limit=5'
```

The response below illustrates the wire shape. The identifiers, values, and delivered fields are examples, not downloadable sample data. `data.items[].data` varies with the delivery's output schema.

```json
{
  "data": {
    "items": [
      {
        "submission_id": "784210006031",
        "task_id": "784210005001",
        "campaign_id": "784210001001",
        "template_id": "784210002001",
        "data": {
          "case_id": "returns-014",
          "decision": "escalate",
          "reason": "The supplied evidence does not establish an exception."
        },
        "quality_score": 0.85,
        "quality_grade": "A",
        "unit_price_usd": 0.02,
        "source": "web",
        "created_at": "2026-10-01T09:30:00+00:00"
      }
    ],
    "next_cursor": null,
    "has_more": false,
    "count": 1,
    "receipt": null
  },
  "success": true,
  "errorCode": 0,
  "errorMessage": "SUCCESS"
}
```

The item can have a nonzero price while replay's receipt is null. The price describes the subscription's resolved per-record price; it is not proof that this request charged that amount.

## Success response

The HTTP response uses a standard envelope. Require `success: true` before treating `data` as a successful page.

| Field | Type | Meaning |
| --- | --- | --- |
| `success` | boolean | Whether the operation succeeded. |
| `errorCode` | integer | `0` on success. |
| `errorMessage` | string | `SUCCESS` on success; diagnostic text on failure. |
| `data.items` | array | The returned records. An empty array is a valid result. |
| `data.count` | integer | Number of records on this page. |
| `data.has_more` | boolean | Whether another page was available when this page was read. |
| `data.next_cursor` | string or null | Continuation marker. In replay mode, pass it back unchanged while `has_more` is true. |
| `data.receipt` | object or null | Metering result for a priced incremental read; null for replay and reads without a charge. |

When a receipt is present, it contains `usage_event_id`, `amount_usd`, `quantity`, and `balance_available_usd`. Save the receipt with its response. [Usage and reconciliation](#usage-and-reconciliation) shows how to match it against the organization's usage list.

### Record fields

| Field | Type | Meaning |
| --- | --- | --- |
| `submission_id` | string | Source submission identifier; can be empty when unavailable. |
| `task_id` | string | Source task identifier; can be empty. |
| `campaign_id` | string | Source campaign identifier; can be empty. |
| `template_id` | string | Template identifier; empty means unknown. |
| `data` | JSON value | Delivered content after any configured output mapping. Do not parse it as a JSON string a second time. |
| `quality_score` | number | Score mapped from the recorded grade; `0` when no recognized grade is present. |
| `quality_grade` | string | `S`, `A`, `B`, `C`, or `D`; empty when no recognized grade is present. |
| `unit_price_usd` | number | Resolved per-record price for this subscription. It is not a payment receipt. |
| `source` | string | Source label; can be empty. |
| `created_at` | string | Original submission timestamp; can be empty. |

Known grades map to scores as follows: S → 0.97, A → 0.85, B → 0.70, C → 0.50, D → 0.30. These are grade mappings, not calibrated probabilities or measurements of downstream model performance.

Records are paged in delivery order. An older `created_at` in a later page does not by itself indicate a duplicate. IDs are strings; preserve them without numeric conversion. Preserve the source precision of arbitrary numeric values inside `data` when selecting a JSON parser.

<a id="field-schema"></a>
### Get the field schema for `data`

The fields inside each record's `data` depend on its `template_id`. Two reads return that contract, so you can decode records without guessing:

```http
GET /v1/live/task/templates?subscription_id=<SUBSCRIPTION_ID>
GET /v1/live/task/template?template_id=<TEMPLATE_ID>
Authorization: Bearer <API_KEY>
```

The plural form lists every template the subscription's Campaign is configured to deliver, so you can cache them before any record arrives. It returns `subscription_id`, `count`, and `templates`; `templates` is empty when none is configured. The singular form looks up one `template_id` met in a record and is not limited to one subscription. Both need a Key with data access: a Key made only for Campaign configuration gets `403`. On the plural form, a subscription outside the Key's selected scope also gets `403`, and one your organization does not have gets `404`.

| Field | Type | Meaning |
| --- | --- | --- |
| `template_id` | string | Matches `template_id` in pull records |
| `name` | string | Template name |
| `description` | string | What the template collects |
| `kind` | string | Template kind, `form` for form-based work |
| `fields` | array | One entry per field in `data`: `name`, `type`, `required`, `description`, and `enum` when the value comes from a fixed set |

A missing `template_id` or `subscription_id` is refused by request validation with `422` and a message naming the parameter, without an `errorCodeName`. A `template_id` that is present but empty returns `400` with `TEMPLATE_ID_REQUIRED`, and an unknown template returns `404` with `TEMPLATE_NOT_FOUND`.

## Usage and reconciliation

```http
GET /v1/live/usage
Authorization: Bearer <API_KEY>
```

This endpoint lists the usage events your organization has been charged for, newest first. Each priced incremental pull that returns records creates one event, and that event's `usage_event_id` is the one in the pull's receipt. Reading the list charges nothing and moves no reading position.

Use the same host and Key as `GET /v1/live/pull`; the Key needs data access. The list covers the whole organization in that environment, including subscriptions outside the Key's own scope. It takes no `subscription_id`.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | No | Number of events to return. Default: 50. Maximum: 200; a larger value answers `422`. |

The endpoint has no cursor. It returns the newest events and nothing older.

| Field | Type | Meaning |
| --- | --- | --- |
| `data.items` | array | The usage events, newest first. |
| `data.count` | integer | Number of events in `items`. |
| `usage_event_id` | string | The event's identifier. Equal to `receipt.usage_event_id` of the pull that was charged. |
| `subscription_id` | string | The subscription the pull ran under. |
| `dataset_id` | string or null | The dataset the charge is attributed to; null when the price did not come from a dataset. |
| `vertical_id` | string or null | The subscription's vertical; null when it has none. |
| `quantity` | integer | Records served by that pull. |
| `unit_price_usd` | number | Price per record at the time of the pull. |
| `amount_usd` | number | Amount charged: `unit_price_usd` times `quantity`, rounded to six decimal places. |
| `currency` | string | `USD`. |
| `occurred_at` | string | When the charge was recorded. |

The values below are illustrations. A pull response saved with this receipt:

```json
{
  "usage_event_id": "784210009001",
  "amount_usd": 0.1,
  "quantity": 5,
  "balance_available_usd": 49.9
}
```

matches this event in the usage list:

```json
{
  "data": {
    "items": [
      {
        "usage_event_id": "784210009001",
        "subscription_id": "784210003001",
        "dataset_id": null,
        "vertical_id": "784210004001",
        "quantity": 5,
        "unit_price_usd": 0.02,
        "amount_usd": 0.1,
        "currency": "USD",
        "occurred_at": "2026-10-01T09:31:00+00:00"
      }
    ],
    "count": 1
  },
  "success": true,
  "errorCode": 0,
  "errorMessage": "SUCCESS"
}
```

To reconcile, look up each saved receipt's `usage_event_id` in `data.items`, then compare `quantity` and `amount_usd`.

| You find | What it means | What to do now |
| --- | --- | --- |
| An event with the receipt's `usage_event_id` | The charge is recorded once. | Check that `quantity` and `amount_usd` equal the receipt's. |
| No such event, and `count` equals your `limit` | The event may be older than the events you asked for. | Request again with a larger `limit`, up to 200. |
| No such event among the 200 newest | The event is older than this endpoint returns. | Keep the saved receipt as your record, and review the period's charges on the organization's [Billing page](/collaboration/billing). |
| An event with no saved receipt | A pull was charged and its response was not saved, for example because it was lost in transit. | Recover the records by replay, as described in [Reading modes and recovery](#reading-modes-and-recovery). |
| A saved response with `receipt: null` | The read was a replay, or it charged nothing. | Nothing to reconcile. |

<a id="rate-limits"></a>
## Rate limits

Each endpoint allows a set number of requests per API Key, counted separately for each endpoint. The default is 100 requests per minute. Every response says where you stand:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests left in that window |
| `Retry-After` | On a `429`, seconds to wait before the next request |

Over the limit, the gateway answers `429` with `errorMessage` `Too many requests` and no `errorCodeName`. Wait `Retry-After` seconds, then continue more slowly. A paging loop should pause when `X-RateLimit-Remaining` reaches `0`.

## Errors

Use the HTTP status and envelope together. `errorCode` is numeric. `errorCodeName` may be present for some structured errors but is not present for every failure. Some detailed errors are JSON serialized inside `errorMessage`; clients must not assume one universal named-error shape.

```json
{
  "data": null,
  "success": false,
  "errorCode": 401,
  "errorMessage": "Missing API key"
}
```

A data-API refusal that carries detail puts it in `errorMessage` as a JSON string, and `errorCodeName` is absent. For example, a priced read with too little balance:

```json
{
  "data": null,
  "success": false,
  "errorCode": 402,
  "errorMessage": "{\"error\": \"insufficient_balance\", \"available_usd\": 0.0, \"required_usd\": 0.02}"
}
```

When `errorMessage` starts with `{`, parse it and branch on its `error` field: `invalid_cursor` and `filters_need_reread` (`400`), `insufficient_balance` and `entitlement_required` (`402`, the second with `dataset_id`), or `pull_contended` and `billing_unavailable` (`503`).

| HTTP status | Cause or example | Recovery |
| --- | --- | --- |
| `400` | Unsupported `from`, inactive subscription, invalid replay cursor, or filters on incremental mode (`filters_need_reread`). | Correct the parameter or subscription state. Use a cursor returned by this endpoint. |
| `401` | Missing, malformed, unknown, revoked, or expired key. | Correct the full Bearer credential or obtain a replacement through an administrator. |
| `402` | `insufficient_balance` on a priced incremental read. | Have the responsible person check available billing funds. |
| `402` | `entitlement_required` for a price-on-application source. | No self-serve purchase exists for these sources yet, and adding funds does not change that. Contact support with the `dataset_id` from the response. |
| `403` | No data-delivery access or the subscription is outside the key's scope. | Correct the authorized key or subscription scope. |
| `404` | Subscription not found for the caller. | Verify organization, environment, and ID. |
| `422` | Missing required parameter or an invalid limit. | Use the query contract above. |
| `429` | Too many requests to this endpoint with this Key. | Wait `Retry-After` seconds; see [Rate limits](#rate-limits). |
| `503` | Pricing/balance/billing unavailable, or concurrent pull contention (`pull_contended`). | Retry after a short delay; apply the reading mode's recovery rules. |
| `500` or an unfamiliar response | Unexpected service failure or contract mismatch. | Preserve a sanitized diagnostic and contact support; do not report success. |

Honor server-provided retry guidance, including `Retry-After` when present. Keep retries bounded and avoid a tight polling loop.

## Delivery and completion boundaries

Campaign records must reach the eligible delivery set. An accepted review alone is insufficient: the campaign delivery path also waits for the reward to be awarded and for the delivery record to be produced. Subscription scope and quality filters further limit the result. A campaign's internal sink output is not automatically the same as this endpoint's delivered record.

This is a synchronous response, not an export job. There is no operation ID to poll. A successful page proves only that the included records were returned. Reaching `has_more: false` completes the current traversal; it does not establish immutable versioning, a license grant, downstream model quality, or permanent availability of every returned record.

For support, send [hello@humanbased.ai](mailto:hello@humanbased.ai) the environment, request time, HTTP status, and sanitized error text. Exclude API keys and private payloads. For safe agent execution, see [Use an agent with Humanbased](/api/agent-access).
