Skip to main content

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 the subscribers:read scope — 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​

StatusMeaning
pendingAccepted, waiting to be picked up
processingRows are being processed
completedAll rows attempted (terminal)
failedThe 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​

FieldTypeRequiredDescription
listIdstringYesUUID of the target subscriber list.
filefileYesThe CSV file. Must have a text/csv content type or a .csv extension. Maximum size 50 MB.
sourcestringNoImport preset: csv (default), klaviyo, or braze.

Import presets​

The preset controls how the CSV's columns are interpreted:

  • csv — the standard column contract (default when source is 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​

StatusMeaning
400Missing listId or file, or the file is not a CSV
401Missing or invalid bearer token
404The list does not exist in your workspace
413File larger than 50 MB
422Unknown 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
}
FieldTypeDescription
import_job_idstringThe job UUID.
statusstringpending, processing, completed, or failed.
processed_rowsintRows attempted so far.
total_rowsintTotal rows in the file. Absent until processing starts.
failed_rowsintRows that failed validation and were skipped.
error_messagestringTerminal 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​

ParameterDescription
cursorThe next_cursor of the previous page. Omit it for the first page.
limitPage size, 1–1000 (default 100).
updated_sinceRFC 3339 timestamp: only subscribers updated at or after it.
list_idOnly members of this list.
form_idOnly subscribers this subscription form created.
segment_idOnly 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..."
}
FieldDescription
consent_statusMarketing 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.
provisionaltrue for a subscriber a subscription form created before your own system identified them.

Error responses​

StatusCodeWhen
401UNAUTHORIZEDNo server key, or the key is unknown or revoked
403FORBIDDENThe key does not carry subscribers:read
404NOT_FOUNDsegment_id names no segment of your workspace
422VALIDATION_ERRORA 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 are true / false. consent_status is the email channel's marketing status: subscribed, unsubscribed or suppressed.
  • 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_status and created_at are 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_LIMITED beyond that).

The dashboard uses these routes, with a session token:

Method & pathPurpose
POST /api/v1/subscriber-exportsStart an export. Optional body: {"list_id": "…"}, {"segment_id": "…"} or {"form_id": "…"}. 202 with the job.
GET /api/v1/subscriber-exportsRecent exports, newest first, in the offset envelope.
GET /api/v1/subscriber-exports/:id/downloadThe 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​

ParamTypeRequiredDescription
fromRFC 3339NoWindow start on occurred_at. Defaults to all history.
toRFC 3339NoWindow end. Defaults to now.
cursorstringNoOpaque cursor from the previous page's next_cursor.
limitintNoEvents 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​

StatusMeaning
401Missing, malformed, or revoked credential
403The credential is a client SDK key — export requires a server key
422Malformed from / to, limit out of range, or malformed cursor