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.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.
Supported resources
The Bulk API currently supports these resources:| Resource | Create a query job | Fetch results |
|---|---|---|
| Object records | POST /data/v1/objects/{object_name}/query-jobs | GET /data/v1/objects/{object_name}/query-jobs/{job_id}/results |
| Events | POST /data/v1/events/query-jobs | GET /data/v1/events/query-jobs/{job_id}/results |
| Sequence enrollments | POST /sequences/v1/enrollments/query-jobs | GET /sequences/v1/enrollments/query-jobs/{job_id}/results |
| Sequence enrollment steps | POST /sequences/v1/enrollment-steps/query-jobs | GET /sequences/v1/enrollment-steps/query-jobs/{job_id}/results |
object_name, use a standard object
such as company or person, or the API name of a custom object.
Each resource exposes the same set of job-management endpoints under its
query-jobs path:
{base} for each resource is:
| Resource | Base path |
|---|---|
| Object records | /data/v1/objects/{object_name} |
| Events | /data/v1/events |
| Sequence enrollments | /sequences/v1/enrollments |
| Sequence enrollment steps | /sequences/v1/enrollment-steps |
Authentication
The Bulk API requires a user-backed API key. Generate an API key in Settings → Developers and include it with each request:How the Bulk API works
Create a query job
Submit a query job for the resource you want to export. For sequences and
events the request body is optional and defaults to an empty filter. For
object records, a The response includes the
query with a select is required.job_id, current status, and expires_at time: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 finish within a few seconds.A finished job includes
total_rows: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.Create a query job
Create-job endpoints are resource-specific:- Events and sequences take an optional
filterobject that narrows the dataset. - Object records take a required
queryobject that selects which attributes to return and, optionally, filters and sorts the records.
Filter sequences and events
For sequences and events, omit the body to export everything, or pass afilter
to narrow the dataset. For example, scope a request to exclude a specific set of
IDs:
Query object records
Object record jobs take aquery with a required select that lists the
attributes to return. Use where to filter by attribute value, sort_by to
order results, and metadata to filter by base record timestamps:
select— Required. Each key is an attribute API name on the queried object. Usetrueto return the attribute directly, or{ "select": ... }to expand a single-reference attribute and select attributes on the referenced object. Nested selects may be at most three levels deep. Selected attributes are sparse: a result row only includes keys for selected attributes that have a value, so attributes with no value are omitted rather than returned asnull.where— Filter by one or more attribute conditions.{ "equals": ... }matches an attribute directly; a nested object filters through a single-reference attribute on the referenced object.sort_by— Sort by a base record field (id,created_at, orupdated_at) inASCENDINGorDESCENDINGorder.metadata— Filter by base record timestamps (created_at/updated_at). Pairing acreated_at/updated_atmetadatafilter with the matchingsort_byenables incremental queries that page through every record changed since a prior checkpoint.
where and select shape mirrors the standard object records API. See
Standard objects and attributes for the
built-in attributes you can select and filter on.
Timestamps in the object Bulk API are millisecond precision. Both the
timestamps returned in results and the values you supply to
metadata and
where filters are interpreted at millisecond precision.Filter fields
Available filter fields depend on the resource.Sequence filters
Sequence enrollment and enrollment-step jobs share a common set of filters:| Filter | Description |
|---|---|
updated_at | Range filter on the time records were last updated. |
started_at | Range filter on when the enrollment or step started. |
ended_at | Range filter on when the enrollment or step ended. |
sequence | Filter by sequence ID. |
person | Filter by person ID. |
company | Filter by company ID. |
mailbox | Filter by mailbox ID or email address. |
id | Include (in) or exclude (not_in) specific row IDs. |
is_blocked, is_bounced, is_canceled, is_opted_out, is_paused, is_replied | Boolean state filters. |
status. Enrollment-step jobs additionally accept
status, enrollment (filter by enrollment ID), type, and step_number.
Range filters such as updated_at accept gt, gte, lt, and lte bounds.
gt is mutually exclusive with gte, and lt with lte. Set filters such as
id, status, and type accept in and/or not_in arrays.
Event filters
Events are immutable, so the natural cursor istimestamp, which is also the
only datetime field you can filter on.
| Filter | Description |
|---|---|
timestamp | Range filter on when the event occurred. |
id | Include (in) or exclude (not_in) specific event IDs. |
type | Filter by event type (page, track, identify). |
visitor | Filter by visitor ID. |
session | Filter by session ID. |
company | Filter by revealed company ID. |
person | Filter by revealed person ID. |
domain | Filter by the domain on which the event occurred. |
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. |
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 by sending aGET to its query-jobs path:
| 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. |
Cancel a job
Cancel an in-progress job to stop it from being processed:409 Conflict with job_not_cancelable.
Fetch results
Fetch results for a finished job:JSON results
JSON is the default response format:| Limit | Value |
|---|---|
Default page_size | 1000 |
Maximum page_size | 2000 |
| Maximum raw payload size | 8 MiB |
413 with
results_page_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.page_size is 10000.
Result shapes
Each item indata (or each NDJSON line) is a result row whose shape depends on
the resource. The examples below are representative — fields may be added before
general availability, so parse defensively and ignore unrecognized keys.
Object records
Object records
Object record rows use the same
object / id / attributes envelope as the
standard records API. attributes contains exactly the attributes named in your
select; expanded single-reference attributes are nested as their own envelope.
See Standard objects and attributes for
the built-in attributes.Events
Events
Events are flat, denormalized rows — they are not wrapped in the
object / attributes envelope used by object records. type is the
canonical event type (page, track, or identify). Page context
(domain, path, referrer_domain, referrer_path), the URL query, UTM
parameters, and any custom track-event properties are merged into a single
properties object; properties is null when none are present.The revealed company and person are embedded as nested object records and
are null when the visitor is unresolved. Unlike a top-level object-records
select (which returns only the attributes you ask for), an embedded record
carries the full set of standard scalar attributes, with unset values
returned as null. The person’s own company is a { "id": ... } reference
rather than an inlined record.Sequence enrollments
Sequence enrollments
Enrollment rows include status flags and embed the related
sequence,
mailbox, and person. reply_email_message is null until the person
replies.Sequence enrollment steps
Sequence enrollment steps
Step rows embed a summary of their parent
enrollment and, for email steps,
the sent email_message with open and click counts. email_message is null
for non-email steps.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 |
| Page too large | 413 | results_page_too_large |
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 |
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 (and, for object records, a
focused
select) to keep result sets small and exports fast. - Prefer incremental syncs. Use checkpoints such as
updated_at(ortimestampfor events) 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.
Build your own connector
Want to move bulk data into your own warehouse or tool? Use our example connectors as a starting point for building integrations on top of the Bulk API.Bulk API connector examples
Reference implementations for building your own connectors on top of the Bulk API.
What’s next
Data API reference
Review the Data API for object, attribute, and record operations.
Send records via API
Learn how to create and update records in Unify.