Imports & Exports API
Bulk data movement in and out of your workspace: import subscribers into a list from a CSV file (with presets for Klaviyo and Braze exports), export your subscribers to another system, and export your behavioral event history.
Authentication
The two surfaces use different credentials:
-
Subscriber imports are authenticated with a bearer token, like the dashboard APIs:
Authorization: Bearer YOUR_ACCESS_TOKEN -
Subscriber export and event export require a server API key (
hober_srv_…). The subscriber export needs a key with thesubscribers:readscope — see GET /v1/subscribers/export and GET /v1/events/export below.
Subscriber imports
Imports run as asynchronous jobs: upload a CSV, get a job ID back
immediately, then poll the job until it completes. Rows that fail validation
are counted in failed_rows without aborting the rest of the import.
Import job statuses
| Status | Meaning |
|---|---|
pending | Accepted, waiting to be picked up |
processing | Rows are being processed |
completed | All rows attempted (terminal) |
failed | The job hit an unrecoverable error (terminal) — see error_message |
POST /api/v1/lists/import
Starts a subscriber import into an existing list. The request is
multipart/form-data.
Form fields
| Field | Type | Required | Description |
|---|---|---|---|
listId | string | Yes | UUID of the target subscriber list. |
file | file | Yes | The CSV file. Must have a text/csv content type or a .csv extension. Maximum size 50 MB. |
source | string | No | Import preset: csv (default), klaviyo, or braze. |
Import presets
The preset controls how the CSV's columns are interpreted:
csv— the standard column contract (default whensourceis omitted).klaviyo— accepts a Klaviyo profile export as-is: Klaviyo's headers are aliased to the standard fields, extra columns are packed into subscriber attributes, and Klaviyo consent statuses are translated conservatively.braze— accepts a Braze user export as-is, with the equivalent header mapping.
Importing a subscriber that already exists merges into the stored record
(see upsert): attributes the file doesn't mention are kept.
With the klaviyo and braze presets a blank cell means "no value here" and
never erases a stored attribute. To remove one, use the standard csv contract
and set the key to null in the attributes JSON column, e.g. {"tier": null}.
Response — 202 Accepted
{
"import_job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending"
}
Error responses
| Status | Meaning |
|---|---|
400 | Missing listId or file, or the file is not a CSV |
401 | Missing or invalid bearer token |
404 | The list does not exist in your workspace |
413 | File larger than 50 MB |
422 | Unknown source value |
GET /api/v1/lists/import/:jobId
Fetches the status of an import job.
Response — 200 OK
{
"import_job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "processing",
"processed_rows": 1500,
"total_rows": 5000,
"failed_rows": 10
}
| Field | Type | Description |
|---|---|---|
import_job_id | string | The job UUID. |
status | string | pending, processing, completed, or failed. |
processed_rows | int | Rows attempted so far. |
total_rows | int | Total rows in the file. Absent until processing starts. |
failed_rows | int | Rows that failed validation and were skipped. |
error_message | string | Terminal error reason. Present only on failed jobs. |
404 — no import job with that ID in your workspace.
Subscriber exports
GET /v1/subscribers/export
Pages through your subscribers for a system that keeps a copy of them, such as a CRM. Unsubscribed and suppressed people are included, with their status, so the other system can keep its suppression list in step.
GET /v1/subscribers/export?limit=500&updated_since=2026-10-01T00:00:00Z
Authorization: Bearer hober_srv_…
Subscribers come in (updated_at, id) order. To sync incrementally, keep the
largest updated_at you have seen and pass it as updated_since on the next
run: a subscriber whose record changes moves to the end of the order.
Query parameters
| Parameter | Description |
|---|---|
cursor | The next_cursor of the previous page. Omit it for the first page. |
limit | Page size, 1–1000 (default 100). |
updated_since | RFC 3339 timestamp: only subscribers updated at or after it. |
list_id | Only members of this list. |
form_id | Only subscribers this subscription form created. |
segment_id | Only current members of this segment. |
Filters combine. Keep requesting pages until next_cursor is empty. A page
filtered by segment_id can hold fewer than limit items and still carry a
next_cursor, when the segment is small compared with your subscriber base.
Response — 200 OK
{
"items": [
{
"id": "0f8e7d6c-5b4a-4392-8171-6e5d4c3b2a19",
"external_id": "user-1042",
"email": "ana@example.com",
"consent_status": { "email": "subscribed", "push": "unsubscribed" },
"created_at": "2026-09-30T14:02:11.120Z",
"updated_at": "2026-10-12T08:45:03.551Z",
"provisional": false,
"attributes": { "plan": "growth", "interests": ["running", "yoga"] }
}
],
"next_cursor": "MjAyNi0xMC0xMlQwODo0NTowMy41NTFafDBmOGU3ZDZj..."
}
| Field | Description |
|---|---|
consent_status | Marketing status per channel: subscribed, unsubscribed or suppressed (a bounce, complaint or manual block). An all key means a block that covers every channel. A channel that is missing has no recorded consent, so marketing to it is not allowed. |
provisional | true for a subscriber a subscription form created before your own system identified them. |
Error responses
| Status | Code | When |
|---|---|---|
401 | UNAUTHORIZED | No server key, or the key is unknown or revoked |
403 | FORBIDDEN | The key does not carry subscribers:read |
404 | NOT_FOUND | segment_id names no segment of your workspace |
422 | VALIDATION_ERROR | A malformed cursor, limit, updated_since or filter id |
CSV subscriber export
Owners and admins can download every subscriber as a CSV file from Subscribers → Export CSV in the dashboard. Exports run in the background: start one, and it appears in the list when it is ready.
- Columns:
external_id, email, consent_status, created_at, then one column per defined attribute (Settings → Attributes), named by its key. Multi-choice values are joined with;, booleans aretrue/false.consent_statusis the email channel's marketing status:subscribed,unsubscribedorsuppressed. - Unsubscribed and suppressed people are included, with their status.
- Re-import: the file uses the standard import contract. A column named
after a defined attribute is read back into that attribute;
consent_statusandcreated_atare ignored, so importing the file never grants consent. - Availability: each file can be downloaded for 24 hours, then it is
deleted. One export runs at a time per workspace, and a workspace can start
10 a day (
429 EXPORT_RATE_LIMITEDbeyond that).
The dashboard uses these routes, with a session token:
| Method & path | Purpose |
|---|---|
POST /api/v1/subscriber-exports | Start an export. Optional body: {"list_id": "…"}, {"segment_id": "…"} or {"form_id": "…"}. 202 with the job. |
GET /api/v1/subscriber-exports | Recent exports, newest first, in the offset envelope. |
GET /api/v1/subscriber-exports/:id/download | The CSV file. 409 EXPORT_NOT_READY before it finishes, 410 EXPORT_EXPIRED after 24 hours. |
Every export start is recorded in the workspace audit log.
Event exports
GET /v1/events/export
Bulk export of the workspace's behavioral event history, paginated with an opaque cursor. This endpoint requires a server API key — publishable client SDK keys cannot read event history:
Authorization: Bearer hober_srv_YOUR_SERVER_KEY
This is the same endpoint documented on the Events page — see there for the event field semantics and ingestion counterpart.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
from | RFC 3339 | No | Window start on occurred_at. Defaults to all history. |
to | RFC 3339 | No | Window end. Defaults to now. |
cursor | string | No | Opaque cursor from the previous page's next_cursor. |
limit | int | No | Events per page, 1–1000. Defaults to 1000. |
Response — 200 OK
{
"items": [
{
"id": "6a7b8c9d-0e1f-2a3b-4c5d-6e7f8a9b0c1d",
"subscriber_id": "8a2f6c1e-1234-4abc-9def-567890abcdef",
"anonymous_id": null,
"event_name": "order_placed",
"platform": "web",
"properties": { "total_price": "19.99" },
"occurred_at": "2026-07-01T12:00:00Z",
"received_at": "2026-07-01T12:00:01Z",
"source": "server"
}
],
"next_cursor": ""
}
Rows are ordered by (occurred_at, id), so pages are stable under
concurrent ingestion. Keep requesting with the returned next_cursor until
it comes back empty — an empty next_cursor means the final page.
Error responses
| Status | Meaning |
|---|---|
401 | Missing, malformed, or revoked credential |
403 | The credential is a client SDK key — export requires a server key |
422 | Malformed from / to, limit out of range, or malformed cursor |