> ## Documentation Index
> Fetch the complete documentation index at: https://docs.unifygtm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Request data with the Bulk API

> Use the Bulk API to request large Sequence datasets from Unify.

## Overview

Use the Bulk API to request large datasets from Unify without waiting
for a long-running HTTP request to finish. The Bulk API is asynchronous: you
create a query job, poll the job until it is finished, and then page through the
materialized results.

Use the Bulk API when you need to export or sync many records. For small operational reads, use the standard API for
the resource instead.

<Info>
  The Bulk API is currently in preview. Endpoint behavior, filters, and response
  shapes may change before general availability.

  The Bulk API is for requesting data from Unify. To create or update records in
  Unify, see [Send records via API](/developers/guides/send-data/overview).
</Info>

## Supported resources

The Bulk API currently supports these resources:

| Resource                  | Create a query job                               | Fetch results                                              |
| :------------------------ | :----------------------------------------------- | :--------------------------------------------------------- |
| Sequence enrollments      | `POST /sequences/v1/enrollments/jobs/query`      | `GET /sequences/v1/enrollments/jobs/{job_id}/results`      |
| Sequence enrollment steps | `POST /sequences/v1/enrollment-steps/jobs/query` | `GET /sequences/v1/enrollment-steps/jobs/{job_id}/results` |

You can also list and inspect jobs for each resource:

```http theme={null}
GET /sequences/v1/{resource}/jobs
GET /sequences/v1/{resource}/jobs/{job_id}
```

In these paths, `{resource}` is either `enrollments` or `enrollment-steps`.

## Authentication

The Bulk API requires a user-backed API key. Generate an API key in
[Settings → Developers](https://app.unifygtm.com/dashboard/settings/integrations/api-keys)
and include it with each request:

```http theme={null}
X-Api-Key: <YOUR_API_KEY>
```

All examples use main unify API base URL:

```text theme={null}
https://api.unifygtm.com
```

## How the Bulk API works

<Steps titleSize="h3">
  <Step title="Create a query job">
    Submit a query job for the resource you want to export. The request body is
    optional and defaults to an empty filter.

    ```bash theme={null}
    curl -X POST 'https://api.unifygtm.com/sequences/v1/enrollments/jobs/query' \
      -H 'X-Api-Key: ${UNIFY_API_KEY}' \
      -H 'Content-Type: application/json' \
    ```

    The response includes the `job_id`, current `status`, and `expires_at` time:

    ```json theme={null}
    {
      "job_id": "<string>",
      "status": "IN_PROGRESS",
      "expires_at": "2026-05-13T12:00:00Z"
    }
    ```
  </Step>

  <Step title="Poll job metadata">
    Poll the job metadata endpoint until the job reaches a terminal status.
    Avoid tight polling loops; use a steady interval or exponential backoff. Most
    Jobs should finish within a few seconds.

    ```bash theme={null}
    curl 'https://api.unifygtm.com/sequences/v1/enrollments/jobs/<string>' \
      -H 'X-Api-Key: ${UNIFY_API_KEY}'
    ```

    A finished job includes `total_rows`:

    ```json theme={null}
    {
      "job_id": "<string>",
      "status": "FINISHED",
      "total_rows": 123,
      "error_code": null,
      "created_at": "2026-05-12T12:00:00Z",
      "expires_at": "2026-05-13T12:00:00Z",
      "canceled_at": null
    }
    ```
  </Step>

  <Step title="Fetch results">
    After the job status is `FINISHED`, fetch results by page. Results are
    immutable for a finished job, so each page is stable for that job.

    ```bash theme={null}
    curl 'https://api.unifygtm.com/sequences/v1/enrollments/jobs/<string>/results?page=1&page_size=1000' \
      -H 'X-Api-Key: ${UNIFY_API_KEY}' \
      -H 'Accept: application/json'
    ```

    ```json theme={null}
    {
      "total": 123,
      "page": 1,
      "page_size": 1000,
      "data": [
        {
          "id": "<string>",
          "updated_at": "2026-05-12T11:58:00Z"
        }
      ]
    }
    ```
  </Step>

  <Step title="Store a checkpoint">
    For recurring syncs, store a checkpoint such as the newest `updated_at` value
    you processed. Use that checkpoint in the next query job so you only request
    new or changed records.
  </Step>
</Steps>

## Create a query job

Create-job endpoints are resource-specific:

```http theme={null}
POST /sequences/v1/enrollments/jobs/query
POST /sequences/v1/enrollment-steps/jobs/query
```

Use filters to narrow the dataset. For example, you can scope a request to exclude
specific set of IDs:

```json theme={null}
{
  "filter": {
    "id": { "not_in": ["<string>", "<string>"] }
  }
}
```

## Filter fields

Available filter fields depend on the resource.

Common filters include:

| Filter       | Description                                   |
| :----------- | :-------------------------------------------- |
| `updated_at` | Filter by the time records were last updated. |
| `person`     | Filter by person.                             |
| `company`    | Filter by company.                            |
| `id.in`      | Include only specific record IDs.             |
| `id.not_in`  | Exclude specific record IDs.                  |

Bulk API endpoints also support endpoint-specific filters.

## Job statuses

A job can have one of the following statuses:

| Status        | Meaning                                                        |
| :------------ | :------------------------------------------------------------- |
| `IN_PROGRESS` | The job has been created and results are still being prepared. |
| `FINISHED`    | The job completed successfully and results are available.      |
| `FAILED`      | The job failed. Check `error_code` on the job metadata.        |
| `CANCELED`    | The job was canceled before completion.                        |
| `EXPIRED`     | The job or its results are no longer available.                |

Jobs and results are available until `expires_at`, which is currently 24 hours
after job creation. Download all required result pages before that time.

## List jobs

List jobs for a resource:

```http theme={null}
GET /sequences/v1/{resource}/jobs
```

Query parameters:

| Parameter | Description                                                    |
| :-------- | :------------------------------------------------------------- |
| `limit`   | Number of jobs to return. Defaults to `100`; maximum is `100`. |
| `cursor`  | Opaque pagination cursor from the previous response.           |
| `status`  | Optional job status filter.                                    |

Example request:

```bash theme={null}
curl 'https://api.unifygtm.com/sequences/v1/enrollments/jobs?limit=100&status=FINISHED' \
  -H 'X-Api-Key: ${UNIFY_API_KEY}'
```

Example response:

```json theme={null}
{
  "jobs": [
    {
      "job_id": "<string>",
      "status": "FINISHED",
      "total_rows": 123,
      "error_code": null,
      "created_at": "2026-05-12T12:00:00Z",
      "expires_at": "2026-05-13T12:00:00Z",
      "canceled_at": null
    }
  ],
  "next_cursor": null
}
```

## Fetch results

Fetch results for a finished job:

```http theme={null}
GET /sequences/v1/{resource}/jobs/{job_id}/results?page=1&page_size=1000
```

Result pagination is page-based. Rows are ordered by most recently updated first
with a stable tie-breaker, so pages remain stable for a finished job.

### JSON results

JSON is the default response format:

```http theme={null}
Accept: application/json
```

JSON limits:

| Limit                    | Value   |
| :----------------------- | :------ |
| Default `page_size`      | `1000`  |
| Maximum `page_size`      | `2000`  |
| Maximum raw payload size | `8 MiB` |

If a JSON page is too large, request a smaller `page_size` or use NDJSON.

### NDJSON results

Use NDJSON for larger streamed pages by providing the following
header in your request.

```http theme={null}
Accept: application/x-ndjson
```

Example request:

```bash theme={null}
curl -N 'https://api.unifygtm.com/sequences/v1/enrollments/jobs/<string>/results?page=1&page_size=10000' \
  -H 'X-Api-Key: ${UNIFY_API_KEY}' \
  -H 'Accept: application/x-ndjson'
```

NDJSON responses return one JSON object per line, streaming one line at a time. Pagination metadata is
returned in response headers instead of a JSON envelope:

```http theme={null}
x-total: 123
x-page: 1
x-page-size: 10000
```

The maximum NDJSON `page_size` is `10000`.

## Job states

The results endpoint validates the job state before returning data:

| Job state     | HTTP status | Error code          |
| :------------ | :---------- | :------------------ |
| `IN_PROGRESS` | `409`       | `results_not_ready` |
| `FAILED`      | `409`       | `results_failed`    |
| `CANCELED`    | `409`       | `results_canceled`  |
| `Expired`     | `410`       | `results_expired`   |

Only fetch results after the job status is `FINISHED`.

## Rate limiting

Bulk API rate limits are applied by operation type. Query job creation is
limited to roughly **100 jobs per day**, so create jobs only when you need a
new snapshot and use filters or checkpoints to keep each job focused.

| Operation               | Rate limit               |
| :---------------------- | :----------------------- |
| Create query jobs       | \~100 jobs per day       |
| query/list job status   | \~10 requests per second |
| list/stream job results | \~5 requests per second  |
| cancel in flight jobs   | \~1 request per second   |

Treat other job management requests, such as checking job status, listing jobs,
and canceling jobs, as low-frequency control-plane calls. When polling job
status, use a steady interval or exponential backoff instead of a tight loop.
Status checks are intended for roughly 5 requests per second, and most jobs
finish quickly enough that slower polling is usually sufficient.

When fetching results, page through data with bounded concurrency. If you receive
`429 Too Many Requests`, wait before retrying, honor the `Retry-After` header to avoid additional rate limiting.

## Best practices

* **Request only the data you need.** Use filters to keep result sets small and
  exports fast.
* **Prefer incremental syncs.** Use checkpoints such as `updated_at` so
  recurring jobs only request new or changed records.
* **Design for retries.** Store the `job_id`, job status, and processing
  checkpoint in your system.
* **Process results idempotently.** Use stable record IDs so retrying a page
  does not create duplicate downstream records.
* **Use NDJSON for large pages.** NDJSON streams results and supports larger
  pages than JSON.
* **Download before expiration.** Jobs expire at `expires_at`; create a new job
  if you need the data again after expiration.

## What's next

<CardGroup cols={2}>
  <Card title="Data API reference" href="/developers/api/data/overview" icon="book" horizontal>
    Review the Data API for object, attribute, and record operations.
  </Card>

  <Card title="Send records via API" href="/developers/guides/send-data/overview" icon="webhook" horizontal>
    Learn how to create and update records in Unify.
  </Card>
</CardGroup>
