Vena Table Export Endpoint
The Vena Table snapshot export APIs (POST /exports, GET /exports/{exportId}/status, GET /exports/{exportId}/data) are in beta. Contract and behavior may change before GA.
In this article, we explain how the Vena Table export endpoints create an asynchronous snapshot of a Vena Table, how to filter that snapshot with whereClause, and how size limits apply. The goal of these endpoints is to provide table data as a durable, paginated export object that downstream systems can consume safely — without streaming an unbounded live query in a single HTTP response.
Through combining create, status, and data calls, you can:
- Submit a scoped export job
- Wait until the snapshot is ready
- Page through rows in JSON or CSV
Overview
Unlike the Hierarchy and Intersections endpoints that return one page of live model data per request, Vena Table export is asynchronous:
| Step | Endpoint | Purpose |
|---|---|---|
| POST /api/public/v1/exports | Enqueue a snapshot build; returns a flat resource with id immediately |
| GET /api/public/v1/exports/{exportId}/status | Wait until "COMPLETED" (JSON string) |
| GET /api/public/v1/exports/{exportId}/data | Read pages with offset / limit (JSON or CSV) |
Snapshots include expiresTime (createdTime + 24 hours). After expiry, status and data requests return 404 EXPORT_EXPIRED; create a new export.
Authentication: Basic Auth (API user + key), same as other Public Export API routes.
License: The Public Export API feature must be enabled for your organization. If it is not, these endpoints return 422 text/plain (The Public Export API feature is not enabled for this customer.), not 403 JSON.
Base URL:
https://{hub}.vena.io/api/public/v1Replace {hub} with your region (for example us1, us2, eu1, ca3).
Size limits of an export snapshot
These limits apply to one export job (the whole snapshot), not to a single HTTP page:
| Limit | Default | Where enforced | What happens if exceeded |
|---|---|---|---|
| Total rows per snapshot | 20,000,000 | At create time (pre-flight COUNT) | 400 EXPORT_ROW_LIMIT_EXCEEDED — no job is enqueued. Narrow whereClause / columnNames and retry. |
| Zero-row result | allowed | Create | Job is still enqueued. After COMPLETED, /data returns empty data, recordsReturned=0, nextPage=null. |
| Internal part size | 50,000 rows / gzip part | Build time (server config) | Transparent to API clients; used for storage pagination |
Page limit (standard) | max 50,000 | Data request | 400 INVALID_PARAMETER if above max |
Page limit (paid Public Export API) | max 100,000 | Data request | 400 INVALID_PARAMETER if above max |
Default page limit | 50,000 | When limit omitted | First page returns up to 50,000 rows |
| Snapshot TTL | 24 hours | Read path | 404 EXPORT_EXPIRED after expiresTime |
Important: The 20,000,000-row cap is the combined row count across all snapshot parts (totalRecords). Use whereClause to keep result sets under this limit. An empty string "" exports all permitted rows and is only recommended for small tables.
A build-time safety net aborts if streamed rows exceed the cap after create (status becomes "ERROR"; /data returns 400 EXPORT_UNAVAILABLE). If the job is already "COMPLETED" but a snapshot part cannot be read, /data returns 500 text/plain (generic server error), not EXPORT_UNAVAILABLE. Prefer relying on the create-time check and a tight whereClause.
Using whereClause
whereClausewhereClause is a required body field on create. It is a VenaQL WHERE expression that scopes which table rows are included in the snapshot.
| Value | Behavior |
|---|---|
Missing / null | 400 INVALID_PARAMETER on whereClause |
"" (empty string) | Export all rows the caller may access (small tables only) |
| Non-empty string | Must be valid VenaQL syntax; validated synchronously before the job is enqueued |
Syntax length is not validated in v1 — keep expressions practical and readable.
Why use whereClause?
whereClause?Vena Tables can be large and without a filter you may hit the 20,000,000-row snapshot cap or export more data than your downstream system needs. UsewhereClauseto:
- Stay under the row cap
- Export only the period, region, or entity you care about
- Reduce build time and storage
Example create request
{
"exportType": "VENA_TABLE",
"modelId": "123456789012345678",
"tableId": "987654321098765432",
"whereClause": "Region = 'North America' AND Year = '2024'",
"columnNames": ["Region", "Year", "Revenue", "Cost"]
}exportType must be the exact token VENA_TABLE (case-sensitive, no surrounding whitespace). Blank, unknown, lowercase (vena_table), or padded (VENA_TABLE) values return 400 INVALID_PARAMETER.
To export all columns on the table, omit columnNames. Unknown JSON properties on the body are ignored.
Common whereClause patterns
whereClause patternsEquality
Region = 'East'AND / OR
Region = 'East' AND Year = '2024'
Year = '2024' OR Year = '2025'IN list
Year IN ('2023', '2024', '2025')NULL checks — use IS NULL / IS NOT NULL
NULL is not a value in SQL. Comparisons with = or != against NULL evaluate to UNKNOWN, which filters out all rows. Syntax validation can still pass, but the query returns 0 rows. The export is created successfully, but the snapshot is empty
| Avoid | Use instead |
|---|---|
Field = null | Field IS NULL |
Field != null | Field IS NOT NULL |
Field NOT IN ('a', null) | Field NOT IN ('a', 'b') AND Field IS NOT NULL |
Field BETWEEN null AND 100 | Field <= 100 (or an explicit non-null lower bound) |
Field1 = 'x' AND Field2 = null | Field1 = 'x' AND Field2 IS NULL |
Always quote string literals
Unquoted words are treated as column references, not strings:
| Avoid | Use instead |
|---|---|
Field = yes | Field = 'yes' |
Field = 123 | Field = '123' (Vena Table cells are stored as strings) |
LIKE wildcards
% matches any sequence; _ matches any single character. Escape literals when needed:
Field LIKE '10\_000' ESCAPE '\'Invalid whereClause
whereClauseInvalid VenaQL returns 400 INVALID_PARAMETER with param.path = ["whereClause"] before any job is created. Fix the expression and create again.
Endpoint 1: Create export (Beta)
POST /api/public/v1/exports
Creates an asynchronous snapshot. The response returns immediately with status SUBMITTED.
Success response
{
"id": "111222333444555666",
"status": "SUBMITTED",
"exportType": "VENA_TABLE",
"modelId": "123456789012345678",
"tableId": "987654321098765432",
"whereClause": "Region = 'East' AND Year = '2024'",
"columnNames": ["Region", "Year", "Revenue"],
"createdTime": "2026-08-05T12:00:00.000Z",
"expiresTime": "2026-08-06T12:00:00.000Z"
}Save id for status and data path {exportId}. expiresTime is createdTime + 24 hours. Fields are rebuilt from the persisted job/step (not a nested request echo). errorReason is omitted on create because status is always SUBMITTED.
Example
curl -s -X POST "https://{hub}.vena.io/api/public/v1/exports" \
-u "${API_USER}:${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"exportType": "VENA_TABLE",
"modelId": "123456789012345678",
"tableId": "987654321098765432",
"whereClause": "Region = '\''East'\'' AND Year = '\''2024'\''",
"columnNames": ["Region", "Year", "Revenue"]
}'Common errors on create
| Situation | HTTP | error.type |
|---|---|---|
Missing / invalid whereClause | 400 | INVALID_PARAMETER |
Invalid exportType (not exact VENA_TABLE) | 400 | INVALID_PARAMETER |
Unknown column in columnNames | 400 | INVALID_PARAMETER |
| Model not found | 400 | INVALID_PARAMETER (param.path = modelId) |
| Table not found / not on model | 400 | INVALID_PARAMETER (param.path = tableId) |
| Pre-flight row count > 20,000,000 | 400 | EXPORT_ROW_LIMIT_EXCEEDED |
| Missing model/table read permission | 403 | FORBIDDEN |
| Public Export API not licensed | 422 | text/plain (no JSON error.type) |
Zero matching rows is not an error — the export is created and completes with an empty snapshot.
Endpoint 2: Get export status (Beta)
GET /api/public/v1/exports/{exportId}/status
Poll until the snapshot is ready. Typical interval: 2–5 seconds. Do not call /data until status is "COMPLETED".
The response is a JSON string (not an object). Internal ETL NOT_STARTED is never exposed — it is returned as "SUBMITTED".
Status values
| Status | Meaning |
|---|---|
SUBMITTED | Job received; waiting to enter the queue |
QUEUED | In the processing queue |
RUNNING | Snapshot is being built |
COMPLETED | Ready — call /data |
ERROR | Build failed — do not fetch data; submit a new export |
EXPIRED | TTL elapsed — create a new export |
Example responses
"RUNNING""COMPLETED""ERROR"Example
curl -s "https://{hub}.vena.io/api/public/v1/exports/${EXPORT_ID}/status" \
-u "${API_USER}:${API_KEY}"Common errors on status
| Situation | HTTP | error.type |
|---|---|---|
Unknown {exportId} | 404 | NOT_FOUND |
Invalid {exportId} format | 404 | NOT_FOUND |
| Past TTL | 404 | EXPORT_EXPIRED |
| Missing model permission | 403 | FORBIDDEN |
| Public Export API not licensed | 422 | text/plain |
Endpoint 3: Get export data (Beta)
GET /api/public/v1/exports/{exportId}/data
Returns one page of snapshot rows. Prerequisite: status is "COMPLETED". Only the user who created the export may download data.
Query parameters
| Parameter | Default | Notes |
|---|---|---|
offset | 0 | Zero-based absolute row index |
limit | 50000 | Max 50,000 (standard) or 100,000 (paid) |
Accept | application/json | Or text/csv for CSV page only |
JSON response (default)
{
"data": [
["North America", "2024", "1500000"],
["EMEA", "2024", "2300000"]
],
"metadata": {
"headers": ["Region", "Year", "Revenue"],
"recordsReturned": 2,
"nextPage": "https://us1.vena.io/api/public/v1/exports/111222333444555666/data?offset=2&limit=2",
"totalRecords": 12000
}
}Each data item is a positional string array aligned with metadata.headers. Page metadata includes headers, recordsReturned, nextPage, and totalRecords — not offset, nextOffset, or expiresTime (TTL is on create). Follow metadata.nextPage until it is null. The sum of recordsReturned across pages equals totalRecords.
CSV response
Set Accept: text/csv. The body is header + up to limit data rows for the requested offset. There is no JSON metadata in the body — use JSON when you need nextPage or totalRecords.
Pagination pattern
GET .../data?offset=0&limit=50000
GET .../data?offset=50000&limit=50000
...
Stop when metadata.nextPage is nullExample
# JSON (default)
curl -s "https://{hub}.vena.io/api/public/v1/exports/${EXPORT_ID}/data?offset=0&limit=50000" \
-u "${API_USER}:${API_KEY}"
# CSV
curl -s "https://{hub}.vena.io/api/public/v1/exports/${EXPORT_ID}/data?offset=0&limit=50000" \
-u "${API_USER}:${API_KEY}" \
-H "Accept: text/csv"Common errors on data
| Situation | HTTP | error.type |
|---|---|---|
| Still building | 400 | EXPORT_NOT_READY |
Build failed (ERROR / CANCELLED) | 400 | EXPORT_UNAVAILABLE |
Bad offset / limit | 400 | INVALID_PARAMETER |
| Different user than creator | 403 | FORBIDDEN |
| Missing model permission | 403 | FORBIDDEN |
| Unknown / invalid id | 404 | NOT_FOUND |
| Expired id | 404 | EXPORT_EXPIRED |
Unsupported Accept | 406 | — |
| Public Export API not licensed | 422 | text/plain |
Part download/parse after COMPLETED | 500 | text/plain (generic server error) |
Recommended client flow
1. POST /exports with whereClause that keeps rows ≤ 20,000,000
2. Read response.id (and exportType / modelId / tableId / whereClause / columnNames)
3. Poll GET /exports/{id}/status every 2–5s until the JSON string is "COMPLETED"
4. GET /exports/{id}/data?offset=0&limit=50000
5. Follow metadata.nextPage until null (align data rows with metadata.headers)
6. Finish before expiresTime (24h) or create a new exportError envelope
All JSON errors use:
{
"error": {
"type": "NOT_FOUND",
"title": "Not Found",
"message": "The export 111222333444555666 was not found. Ensure the exportId is correct and try again."
}
}Validation errors may include error.param (type: BODY or QUERY, path: field name). License failures and generic 500s are returned astext/plain, rather than this envelope. Branch on error.type for JSON errors, instead of relying on HTTP status alone.
Updated 20 days ago
