Skip to main content

Data Export

Stateway lets you export the data your tenant owns — process definitions, decision models, process instances, and the audit log — as a single downloadable file, whenever you want, without contacting support. This guide covers the export API end to end: requesting an export, checking its status, downloading the artifact, and what is (and isn't) included.

The Async Model​

Generating an export can take a while for a tenant with a lot of history, so the API never makes you wait for it. The flow has three steps:

  1. POST /v1/exports — kicks off the export and returns immediately.
  2. GET /v1/exports/{id} — poll this until the export reaches a terminal status.
  3. GET /v1/exports/{id}/download — once completed, this redirects you to the file.

Request:

curl -X POST https://api.stateway.io/v1/exports \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{}'

Response:

{
"data": {
"id": "018f2a6e-4b2a-7c31-9c3f-8e2b1a6d9f01",
"status": "pending",
"scope": {
"sections": ["definitions", "decisions", "instances", "audit_log"],
"from": null,
"to": null
},
"size_bytes": null,
"checksum_sha256": null,
"counts": null,
"error": null,
"created_at": "2026-09-30T12:00:00.000Z",
"started_at": null,
"completed_at": null,
"expires_at": null
}
}

The response (202 Accepted) always has this shape — the same fields are returned by GET /v1/exports and GET /v1/exports/{id}:

FieldDescription
idThe export's identifier (UUID)
statuspending | running | completed | failed | expired
scopeThe resolved scope this export was run with — sections, from, to
size_bytesSize of the .ndjson.gz artifact in bytes; null until completed
checksum_sha256SHA-256 checksum of the downloaded .ndjson.gz file; null until completed
countsRows emitted per section; null until completed
errorFailure cause when status is failed (see Errors below). May also be set on a pending export that was interrupted and automatically requeued — decide the export's outcome by status, not merely by whether error is present.
created_atWhen the export was requested
started_atWhen generation began; null until it does
completed_atWhen the export reached completed or failed; null until then
expires_atWhen the artifact will be removed and the export moves to expired; null until completed

Status Lifecycle​

pending → running → completed → expired
↘ failed
running → pending (interrupted once — requeued automatically)
running → failed (interrupted again after the automatic requeue)

pending and running are transient — a background process picks up the export and generates the artifact. completed and failed are terminal for the export's outcome; a completed export later transitions to expired once its artifact is removed (see Limits). If the process generating an export is interrupted (for example, by a restart), the export automatically returns to pending and is retried once; if it's interrupted again after that retry, it ends in failed rather than being requeued a second time (see Asynchronous Failure Causes).

Requesting an Export​

The POST /v1/exports body is entirely optional — an empty body (or {}) exports everything, unbounded:

FieldDescription
sectionsOptional array of "definitions", "decisions", "instances", "audit_log". Defaults to all four when omitted.
fromOptional ISO 8601 timestamp — inclusive lower bound applied to sections with a time window.
toOptional ISO 8601 timestamp — inclusive upper bound applied to sections with a time window.

from must not be after to, or the request fails with 400 VALIDATION_ERROR. If provided, sections must be a non-empty array — an empty array is also rejected with 400 VALIDATION_ERROR.

Request (a scoped export — only instances and audit log, last 30 days):

curl -X POST https://api.stateway.io/v1/exports \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"sections": ["instances", "audit_log"],
"from": "2026-09-01T00:00:00.000Z",
"to": "2026-09-30T23:59:59.000Z"
}'

Response:

{
"data": {
"id": "018f2a71-9d3a-7f22-b6f1-3a9c2e7d5b02",
"status": "pending",
"scope": {
"sections": ["instances", "audit_log"],
"from": "2026-09-01T00:00:00.000Z",
"to": "2026-09-30T23:59:59.000Z"
},
"size_bytes": null,
"checksum_sha256": null,
"counts": null,
"error": null,
"created_at": "2026-09-30T12:05:00.000Z",
"started_at": null,
"completed_at": null,
"expires_at": null
}
}

definitions and decisions are not time-windowed — every version is always included regardless of from/to (see What's Included).

Listing Exports​

GET /v1/exports lists your tenant's exports, newest first, paginated by cursor — not by offset:

Request:

curl "https://api.stateway.io/v1/exports?limit=20" \
-H "X-API-Key: sw_live_your_key"

Response:

{
"data": [
{
"id": "018f2a71-9d3a-7f22-b6f1-3a9c2e7d5b02",
"status": "completed",
"scope": { "sections": ["instances", "audit_log"], "from": "2026-09-01T00:00:00.000Z", "to": "2026-09-30T23:59:59.000Z" },
"size_bytes": 4213980,
"checksum_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"counts": { "instances": 812, "audit_entries": 5341 },
"error": null,
"created_at": "2026-09-30T12:05:00.000Z",
"started_at": "2026-09-30T12:05:03.000Z",
"completed_at": "2026-09-30T12:06:41.000Z",
"expires_at": "2026-10-02T12:06:41.000Z"
}
],
"meta": { "next_cursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA5LTMwVDEyOjA1OjAwLjAwMFoiLCJpZCI6IjAxOGYyYTcxLTlkM2EtN2YyMi1iNmYxLTNhOWMyZTdkNWIwMiJ9" }
}

limit accepts up to 200 (default 50). When there are more results, meta.next_cursor is set — pass it back as ?cursor= to fetch the next page. When next_cursor is null, you've reached the end.

curl "https://api.stateway.io/v1/exports?limit=20&cursor=eyJjcmVhdGVkQXQi..." \
-H "X-API-Key: sw_live_your_key"

Polling Status​

curl https://api.stateway.io/v1/exports/018f2a71-9d3a-7f22-b6f1-3a9c2e7d5b02 \
-H "X-API-Key: sw_live_your_key"

Returns 200 with the same summary object shown above, whatever the current status is. This is the endpoint to poll — see Email Notification for why you shouldn't rely on anything else to tell you when an export is done.

Downloading the Artifact​

curl -i https://api.stateway.io/v1/exports/018f2a71-9d3a-7f22-b6f1-3a9c2e7d5b02/download \
-H "X-API-Key: sw_live_your_key"
HTTP/1.1 302 Found
location: https://storage.example.com/stateway-prod/tenants/.../export.ndjson.gz?X-Amz-Signature=...

Each call to this endpoint generates a fresh pre-signed URL — nothing is cached, so it's safe to call it again if the previous URL expired before you used it. The pre-signed URL is only valid for a few minutes, so follow the redirect promptly. That window applies to starting the transfer, not to finishing it: once the download is underway it runs to completion, however long it takes, so a large artifact over a slow connection is not a problem. This only works once the export is completed; see Errors for what happens otherwise.

To fetch the file directly (following the redirect) and peek at its manifest line:

curl -L -o export.ndjson.gz https://api.stateway.io/v1/exports/018f2a71-9d3a-7f22-b6f1-3a9c2e7d5b02/download \
-H "X-API-Key: sw_live_your_key"

gunzip -c export.ndjson.gz | head -1

The second command prints the manifest line — see Artifact Format below for its shape.

Artifact Format​

The downloaded file is gzip-compressed newline-delimited JSON (.ndjson.gz) — one JSON object per line, decompressed with any standard gzip tool.

The first line is always a manifest, identified by "type": "manifest":

{"type":"manifest","version":1,"tenant_id":"ten_01j...","generated_at":"2026-09-30T12:06:41.000Z","sections":["definitions","decisions","instances","audit_log"],"window":{"from":null,"to":"2026-09-30T12:06:41.000Z"},"counts":{"definitions":14,"decisions":6,"instances":812,"audit_entries":5341},"stateway_api_version":"1.4.0"}
Manifest fieldDescription
versionManifest format version (currently 1)
tenant_idThe exporting tenant
generated_atWhen the artifact was generated
sectionsSections included, always in canonical order: definitions, decisions, instances, audit_log
window.from / window.toThe effective time window applied to windowed sections
countsRows emitted per section (same shape as the export's counts field)
stateway_api_versionStateway API version that generated the file

Every line after the manifest is a data row, distinguished by its own type: definition, decision, instance, or audit_entry. Sections are always emitted in the canonical order above, regardless of the order you passed in sections.

Note that the audit_log section's counts/manifest key is audit_entries, not audit_log — the other three sections use their own section name as the counts key unchanged (definitions, decisions, instances). Row type is singular for every section (definition, decision, instance, audit_entry).

What's Included in Each Section​

SectionWhat you get
definitionsEvery version of every process definition, including the original source you deployed.
decisionsEvery version of every decision model, including the original source you deployed.
instancesFull instance records, including variables. Any file_ref embedded in a variable has its internal storage details removed — the same protection applied to variables everywhere else in the API. The from/to window filters on the instance's start time.
audit_logEach entry's payload exactly as stored, plus prev_entry_hash/entry_hash — enough to independently reverify the tenant's hash chain from the exported file alone, without calling the API. The from/to window filters on the entry's created_at. See Hash Chain Algorithm for the algorithm.

Verifying the hash chain from the export. An exported audit_entry row has no tenant_id of its own — use the manifest's tenant_id as the tenant identifier when recomputing hashes per the algorithm linked above. That page's field names are camelCase; each corresponds 1:1 to this export's snake_case field of the same meaning: created_at ↔ createdAt, actor_type ↔ actorType, resource_type ↔ resourceType, resource_id ↔ resourceId, prev_entry_hash ↔ prevEntryHash, entry_hash ↔ entryHash.

What's Not Included in the Dump​

Planned, not yet supported:

  • Human tasks
  • Webhook subscriptions and delivery history
  • Timers
  • Service task job records
  • Usage records
  • API keys
  • The bytes of uploaded file attachments (a file_ref's metadata is exported as part of instances, but not the underlying file content)

Permanently excluded: credentials are stored as encrypted secrets and are never part of an export. This is a deliberate, permanent security exclusion — it will not become available in a future release.

Limits​

LimitDefaultNotes
Artifact retention (TTL)48 hoursAfter this, a completed export moves to expired and its file is removed. Configurable per deployment.
Max retained exports per tenant5Counts pending/running/completed exports. Wait for one to expire (or fail) before requesting another.
Rate limit on POST /v1/exports5 per hourApplies only to creating new exports — polling and downloading are not rate-limited by this rule.
Max rows per export500,000Total across all included sections. An export whose scope would exceed this fails explicitly rather than silently truncating the file.

These defaults may be adjusted per deployment; treat them as the current operating limits rather than fixed guarantees.

There is no endpoint to delete an export early — a completed export occupies its retention slot for its entire TTL. If you're at the retained-exports limit, wait for the oldest one to expire (or fail) before requesting a new one.

Errors​

Two distinct families of failure apply here: synchronous HTTP errors on the export endpoints themselves, and asynchronous failures of the export job, which surface through the error field of an otherwise successful status check.

Synchronous HTTP Responses​

StatuscodeEndpoint(s)Meaning
400VALIDATION_ERRORPOST /v1/exportsUnknown section name, or from after to
400VALIDATION_ERRORGET /v1/exportsInvalid cursor
409EXPORT_LIMIT_REACHEDPOST /v1/exportsThe tenant already has the maximum number of retained exports
429RATE_LIMITEDPOST /v1/exportsRate limit for this endpoint exceeded
404NOT_FOUNDGET /v1/exports/{id}, GET /v1/exports/{id}/downloadExport not found, or belongs to another tenant
409EXPORT_NOT_READYGET /v1/exports/{id}/downloadThe export hasn't finished running yet
409EXPORT_FAILEDGET /v1/exports/{id}/downloadThe export failed and has no file to download
410EXPORT_EXPIREDGET /v1/exports/{id}/downloadThe artifact's retention window passed and the file was removed

Asynchronous Failure Causes​

GET /v1/exports/{id} itself still returns 200 in these cases — it successfully reports that the export failed, via status: "failed" and a populated error field shaped { "code": "...", "message": "..." }:

{
"data": {
"id": "018f2a71-9d3a-7f22-b6f1-3a9c2e7d5b02",
"status": "failed",
"scope": { "sections": ["definitions", "decisions", "instances", "audit_log"], "from": null, "to": null },
"size_bytes": null,
"checksum_sha256": null,
"counts": null,
"error": {
"code": "EXPORT_TOO_LARGE",
"message": "The requested export exceeds the maximum number of records allowed in a single export. Narrow the time window or select fewer sections, then request a new export."
},
"created_at": "2026-09-30T12:05:00.000Z",
"started_at": "2026-09-30T12:05:03.000Z",
"completed_at": "2026-09-30T12:05:10.000Z",
"expires_at": null
}
}

Two causes are worth calling out specifically — both fail the export explicitly rather than silently truncating or partially publishing anything:

  • Scope too large (EXPORT_TOO_LARGE) — the requested scope exceeds the row limit (see Limits). Narrow the time window or select fewer sections, then request a new export.
  • Storage quota exceeded (STORAGE_QUOTA_EXCEEDED) — the resulting artifact wouldn't fit within the tenant's storage quota; no bytes are written against your usage. Free up storage or increase your quota, then request a new export.

The full set of codes you may see in error.code:

error.codeMeaningSuggested action
EXPORT_TOO_LARGEThe requested scope exceeds the maximum number of rows allowed in a single exportNarrow the time window or select fewer sections, then request a new export
STORAGE_QUOTA_EXCEEDEDThe artifact wouldn't fit within the tenant's storage quota; nothing was storedFree up storage or increase your quota, then request a new export
STORAGE_UNAVAILABLEThe generated artifact could not be storedRequest a new export
EXPORT_FAILEDThe export failed due to an unexpected errorRequest a new export
EXPORT_ABANDONEDThe export was interrupted twice and was not retried again (see Status Lifecycle)Request a new export

While status is still pending, error.code may briefly show EXPORT_RECYCLED — this means a previous attempt was interrupted and the export is being retried automatically; no action is needed unless it later reaches failed with EXPORT_ABANDONED.

Email Notification​

When an export reaches completed or failed, the tenant's email address on file receives a notification. The email never contains a download link or an embedded access token — it only points you back to the API (GET /v1/exports/{id}, then GET /v1/exports/{id}/download). If the email fails to send, that failure never changes the export's status or error — the export's outcome is already final by the time the notification is attempted; only the notification itself is retried, best-effort.

Polling is the source of truth, not email

This notification is best-effort: it may be delayed, or in rare cases not arrive at all. It is a convenience, not a guarantee — GET /v1/exports/{id} remains the only reliable way to know an export's status. Don't build automation that waits for the email instead of polling; poll the endpoint until the export reaches a terminal status.