Skip to content

Exports

An export is a file of your people, your quiz responses or your quiz analytics, built in the background and downloadable for up to 30 days (erasing the store’s shopper data removes it earlier). Request one, poll it until it is completed, then follow its download_url; when it is ready the export.completed webhook fires. The same files can be requested from the dashboard under Developer > Exports (every plan, no key needed). The request and response fields are in the reference.

profiles and responses need profiles:read, quiz_analytics needs analytics:read; listing works with any key. A request costs 2 units, polling and listing 1 (see Rate limits).

Two formats

format picks the file: csv (the default) or json. Both are gzipped; the download is named <dataset>-<date>.csv.gz or <dataset>-<date>.json.gz.

csvjson
LayoutA header row then one row per line, behind a UTF-8 byte-order mark so spreadsheets open it with the right encoding. answers, points and formulas are JSON in one cell each.One object: {"dataset", "filters", "generated_at", "items": [...]}. Parse the whole file, then read items.
profiles rowsOne row per person and quiz with the person’s latest completed result for that quiz: contact, counts, spend, then answers, points, formulas, top_match, total_points, result_page.One item per person, shaped like a GET /v1/profiles row: the person and the quizzes they took, without their answers.
responses rowsOne row per captured session, finished or not.The same fields per session, with answers, points and formulas as nested objects instead of JSON strings; a cell that is empty in the CSV is "" here.
quiz_analytics rowsOne row per quiz over the range.One item per quiz, shaped like a GET /v1/analytics/quizzes row (money as {amount, currency}).

Pick csv for spreadsheets and warehouse loaders, and whenever you need answers per person: only the CSV profiles file carries them. Pick json when you already parse the API’s objects and want the same shapes without a CSV parser, or for responses, where the answers arrive as objects you can read directly.

Request a file, poll it, download it

Terminal window
curl -X POST https://api.octaneai.com/v1/exports \
-H "Authorization: Bearer $OCTANE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"dataset": "profiles", "quiz_id": "quiz_7c9e6679742540de944be07fc1f90ae7", "from": "2026-08-01", "to": "2026-08-31"}'
# then, until "status" is "completed":
curl https://api.octaneai.com/v1/exports/exp_3f2504e04f8911d39a0c0305e82c3301 \
-H "Authorization: Bearer $OCTANE_API_KEY"
# then, with no Authorization header:
curl -o profiles.csv.gz "<download_url>"

The request answers 202 with the export in pending status and a Location header pointing at it:

{
"id": "exp_3f2504e04f8911d39a0c0305e82c3301",
"dataset": "profiles",
"format": "csv",
"source": "api",
"status": "pending",
"filters": { "quiz_id": "quiz_7c9e6679742540de944be07fc1f90ae7", "from": "2026-08-01", "to": "2026-08-31" },
"row_count": null,
"expires_at": "2026-10-01T12:00:00Z",
"download_url": null,
"urls": { "self": "https://api.octaneai.com/v1/exports/exp_3f2504e04f8911d39a0c0305e82c3301" }
}

What the filters mean depends on the dataset: for profiles and responses, from and to are UTC days on the person’s last visit or on the session; for quiz_analytics they are the analytics range in tz (UTC when absent), the last 30 days when both are absent. quiz_id narrows every dataset to one quiz. search, for profiles only, is the body of a People search without its quiz_id, from and to, which stay on the export (see A filtered people file).

  • status goes pending, running, then completed or failed. On completion row_count and bytes are set; on failure error reads “The file could not be built” (or “Could not queue the export” when the job never started).
  • Reading a completed export answers a fresh download_url on every call, good for 15 minutes (download_url_expires_at); the list never carries links. The URL is signed on its own: do not send your API key to it.
  • One file of a kind builds at a time per store: requesting a dataset that is still being built answers 409 conflict; poll that one instead.
  • After expires_at (30 days from the request) the export answers 404 and leaves the list.
  • The admin who requested a file from the dashboard is emailed when it is ready; a file requested with a key is not, so subscribe to export.completed or poll.

The same request as JSON

Terminal window
curl -X POST https://api.octaneai.com/v1/exports \
-H "Authorization: Bearer $OCTANE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"dataset": "responses", "format": "json", "quiz_id": "quiz_7c9e6679742540de944be07fc1f90ae7", "from": "2026-08-01", "to": "2026-08-31"}'

The file, once gunzipped, cut to one item:

{
"dataset": "responses",
"filters": { "quiz_id": "quiz_7c9e6679742540de944be07fc1f90ae7", "from": "2026-08-01", "to": "2026-08-31" },
"generated_at": "2026-09-01T12:00:09Z",
"items": [
{
"session_id": "sess_9b2d4a5e6f7a4b8c9d0e1f2a3b4c5d6e",
"quiz": "Find your routine",
"quiz_id": "quiz_7c9e6679742540de944be07fc1f90ae7",
"version": 2,
"status": "completed",
"started_at": "2026-08-14T12:00:00Z",
"completed_at": "2026-08-14T12:04:00Z",
"channel": "email",
"device": "mobile",
"country": "US",
"answers": { "image_choice-48su5": { "label": "Skin type?", "kind": "image", "value": "dry", "values": ["Dry"], "image_urls": [null] } },
"points": [{ "dimension_id": "dry", "name": "Dry", "total": 14 }],
"formulas": [{ "component_key": "formula-tidtk", "label": "Hydration score", "value": 72 }],
"top_match": "Dry",
"total_points": "20",
"result_page": "Your match"
}
]
}

The CSV columns

The header never changes, whatever the filters.

  • profiles: person_id (prof_), removed, removed_at, email, phone, first_seen_at, last_seen_at, sessions, completed, returning, orders, spent, currency, quiz, quiz_id (quiz_), version, completed_at, answers, points, formulas, top_match, total_points, result_page. One row per person and quiz; one row per person when quiz_id is set.
  • responses: session_id (sess_), quiz, quiz_id, version, status, started_at, completed_at, channel, device, country, answers, points, formulas, top_match, total_points, result_page. Labels are those of the version the session ran on.
  • quiz_analytics: quiz_id, name, status, views, starts, completions, completion_rate, email_opt_ins, phone_opt_ins, orders, revenue, currency, attributed_share, avg_time_to_complete_ms, median_time_to_complete_ms.

answers, points and formulas hold the same values the people API returns in a compact shape: answers is an object keyed by component key with label, kind, value, values and, for an image choice, image_urls; points is [{dimension_id, name, total}]; formulas is [{component_key, label, value}], numbers as numbers. The three cells are empty when the person has no completed result for the quiz.

import json
# one row of a profiles file, as csv.DictReader yields it
answers = json.loads(row["answers"]) if row["answers"] else {}
print(answers["image_choice-48su5"]["values"]) # ['Dry']

A filtered people file

Add search to a profiles export and the file holds exactly the people the search matches, however many. A live People search is built for pages of results and has 10 seconds per statement; a filter that matches a large share of your people runs past that and answers 422 validation_error whose export field carries the url and ready-to-send body of the export request that builds the same people as a file. An export takes no search slot and has no time limit, so anything you plan to process in bulk starts here.

Terminal window
curl -X POST https://api.octaneai.com/v1/exports \
-H "Authorization: Bearer $OCTANE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"dataset": "profiles",
"quiz_id": "quiz_7c9e6679742540de944be07fc1f90ae7",
"search": {
"has_marketing_consent": true,
"match": "latest_completed_per_quiz",
"groups": [{ "all": [
{ "quiz_id": "quiz_7c9e6679742540de944be07fc1f90ae7", "kind": "choice", "op": "in", "page_key": "skin", "component_key": "image_choice-48su5", "option_labels": ["Dry"] }
] }]
}
}'

quiz_id, from and to inside search answer 422 validation_error (“put quiz_id, from and to on the export, not on search”); search with another dataset is refused the same way; a quiz you do not own inside a term answers 404.

Errors you will meet

Every one is a Problem body: 403 insufficient_scope when the key lacks the dataset’s scope (required names it); 404 not_found for an unknown, foreign or expired export, or a foreign quiz in quiz_id or in search; 409 conflict while an export of that dataset is still being built; 422 validation_error for an unknown dataset or format, an unknown tz, to before from, or quiz_id, from or to inside search.