Skip to content

Exports

An export writes every row of a REST read to a file in the background, so a large pull is one request instead of hundreds of pages. Start a job, poll it, then download the file from a signed URL.

samples/typescript/exports.ts
// A large result set as a file: start an export job, poll until it succeeds,
// then download the CSV from its signed URL (valid 15 minutes, no credentials).
import { setTimeout as sleep } from "node:timers/promises";
import { Status302 } from "@status302/sdk";
const status302 = new Status302({ apiKey: process.env.STATUS302_API_KEY, baseUrl: process.env.STATUS302_API_URL });
let job = await status302.post("/v1/exports", { body: { kind: "events", format: "csv", filters: { sort: "startsAt" } } });
while (job.status === "queued" || job.status === "running") {
await sleep(1000);
job = await status302.get("/v1/exports/{exportId}", { path: { exportId: job.id } });
}
if (job.status !== "succeeded" || !job.downloadUrl) throw new Error(`export ${job.status}: ${job.error ?? ""}`);
const csv = await (await fetch(job.downloadUrl)).text();
console.log(`${job.rows} rows, ${job.bytes} bytes${job.truncated ? " (cut at the plan's export limit)" : ""}`);
console.log(csv.split("\n").slice(0, 3).join("\n"));

POST /v1/exports with the kind of data, the file format and the read’s filters:

{
"kind": "sales",
"format": "jsonl",
"filters": { "country": "US", "confidence": ["inferred_high"], "minPrice": 10000 }
}
kind Rows of Required filters Scopes
events GET /v1/events catalog:read
sales GET /v1/sales/feed, or GET /v1/events/{eventId}/sales with eventId catalog:read, sales:read
series GET /v1/events/{eventId}/series eventId catalog:read, market:read
list A saved list’s rows with their metrics (the spreadsheet view) listId catalog:read
  • format is csv (with a header row) or jsonl (one JSON object per line, the same shape as the read’s items).
  • filters are the read’s query parameters, with the same validation, defaults and plan clamps. cursor and limit aren’t allowed: an export always covers every row. Unknown filter names answer 400 listing the allowed ones.

The API checks the request right away by reading the first row as your key: invalid filters answer 400, and a missing scope, plan or quota answers 403, 402 or 429 exactly as the read would. Otherwise it answers 202 with the queued job. Creating an export counts as one request against your quota; the rows it writes don’t.

GET /v1/exports/{exportId} returns the job. Poll every few seconds until status leaves queued and running:

status Meaning
queued Waiting for a worker.
running Writing the file.
succeeded Done: rows, bytes and downloadUrl are set.
failed error says why. Jobs retry transient failures by themselves before failing.
expired The file was deleted, 7 days after it was written.

GET /v1/exports lists your organization’s exports, newest first.

downloadUrl is a signed URL that needs no credentials and is valid for 15 minutes from the response that returned it. Download it with any HTTP client; fetch the job again for a fresh URL. Files are kept for 7 days (expiresAt).

The file name is status302-<kind>-<id>.csv or .jsonl; CSV files are UTF-8 with a header row, JSON Lines files are application/x-ndjson. Amounts follow the API’s money conventions in both formats.

Exports need the Pro plan or higher. Pro exports stop at 1,000 rows; Elite and Enterprise exports are unlimited. When a file was cut at the limit, truncated is true. A single export runs for at most 15 minutes.