Skip to content

Exports

An export is a grid query with a file at the end of it. The call is asynchronous, the link you get back is short-lived on purpose, and the artifact behind it lives a good deal longer than the link does — which is the pair of facts most integrations get the wrong way round.

Ask, then poll

Every export is asynchronous. There is no synchronous variant and no size below which you get the file inline — a rendered workbook is not something to hold a connection open for.

POST /v1/exports
curl -X POST https://ptx-api.plantactic.com/v1/exports \
  -H "Authorization: Bearer ptx_live_4e91c8..." \
  -H "Idempotency-Key: acme-2026-06-board-pack" \
  -H "Content-Type: application/json" \
  -d '{
    "datasetId": "9f2c4b1e-7a83-4d02-9c15-6b0e2a1d4f77",
    "format":    "xlsx",
    "query":     { "horizon": "2026-06" }
  }'
202 Accepted
HTTP/1.1 202 Accepted
Location: /v1/exports/7c4d2e18-5b60-4a39-9d81-0f3a6c25be84

{
  "exportId":    "7c4d2e18-5b60-4a39-9d81-0f3a6c25be84",
  "datasetId":   "9f2c4b1e-7a83-4d02-9c15-6b0e2a1d4f77",
  "format":      "xlsx",
  "status":      "queued",
  "createdAt":   "2026-06-30T09:41:07Z",
  "completedAt": null,
  "byteSize":    null,
  "url":         null,
  "expiresAt":   null
}

The Location header is the resource to poll. status starts queued and reaches succeeded or failed; those two are terminal, so your loop has somewhere to stop.

GET /v1/exports/{exportId}
{
  "exportId":    "7c4d2e18-5b60-4a39-9d81-0f3a6c25be84",
  "datasetId":   "9f2c4b1e-7a83-4d02-9c15-6b0e2a1d4f77",
  "format":      "xlsx",
  "status":      "succeeded",
  "createdAt":   "2026-06-30T09:41:07Z",
  "completedAt": "2026-06-30T09:41:12Z",
  "byteSize":    124724,
  "url":         "https://…?X-Amz-Expires=600&X-Amz-Signature=…",
  "expiresAt":   "2026-06-30T09:51:12Z"
}

url and expiresAt are null until the artifact exists, and a failed export carries the reason it failed rather than a link.

query takes the same shape the metrics endpoint takes, with one difference worth knowing: query.horizon is required here. The metrics endpoint defaults a missing horizon to the current month; this one refuses, because an export is a document somebody will file and "whenever it was run" is a poor thing to have to reconstruct.

The three formats

  • json — the same grid the metrics endpoint returns, as a file. Useful when the grid is larger than you want to hold in a response.
  • xlsx — a workbook. Numbers arrive as numbers rather than pre-formatted strings, so the sheet is worth something to whoever opens it.
  • pdf — a paginated report, landscape, chunked across pages so no column is clipped.

Format availability is decided at deployment, not in the contract. json and xlsx are always there. pdf needs a configured route to the renderer, and where that is absent the request is refused immediately and by name — rather than accepted and failed later, which would cost you a poll to learn the same thing. Ask for what you want and handle the refusal; do not probe.

Two lifetimes

These are different numbers and confusing them is the usual integration bug.

  • The link lives ten minutes. url is presigned and short by design. It is a bearer of the file: anyone holding it can fetch the file, so it should not be pasted into a ticket or an email.
  • The artifact lives seven days. After that it is gone and the export cannot be re-linked.

So within seven days, an expired link is not a problem — poll the export again and you get a fresh one. Do not store the URL; store the exportId, which is stable, and mint a link when someone actually wants the file.

If you need the artifact to outlive the week, download it and put it somewhere you control. Nothing here is a document store.

How it says no

four distinct refusals
# a format that does not exist
422  format must be one of [pdf, xlsx, json].

# a real format this deployment cannot produce
422  The pdf export is not available yet. json, xlsx are.

# the query is not optional here
422  query.horizon must be a month like 2026-12.

# an export id that is not yours, or not real
404  No export 7c4d2e18-….

Each carries its own type URI, so these are distinguishable without reading the prose. Errors and retries covers the shape.

One rough edge, stated rather than hidden: naming a datasetId that does not exist or is not yours is not refused at this point. You get a 202, and the failure surfaces on the poll as a message about the dataset having no active version — which is true of a dataset that does not exist, and misleading about why. Resolve the dataset before you export it if that distinction matters to your error handling.

What an export costs

An export runs a grid query and is metered exactly like one — same units, same formula, covered in Compute units.

The difference is that you are not told. A metrics response carries X-Plantactic-Compute-Units; an export returns 202 before the work happens, so there is no header to put it in and none is added later. If you are instrumenting spend from response headers, exports are invisible to that instrumentation and you will under-count.