---
title: "Export a Sheet's rows as CSV or XLSX."
method: POST
path: "/v1/sheets/{id}/export"
tags: ["sheets"]
---

# Export a Sheet's rows as CSV or XLSX.

`POST /v1/sheets/{id}/export`

## Request

- Base URL: `http://localhost:8080`
- URL: `http://localhost:8080/v1/sheets/{id}/export`
- Auth: one of:
  - API key in cookie `dvt_session`
  - HTTP bearer

Downloads the sheet's merged row view (flushed warehouse rows with on-page pending
edits applied, the same view the grid shows) as a CSV or XLSX attachment, in ONE
engine fetch of up to 10,000 rows (ADR-0091 D5). `filters` and `sort` take the same
shapes as `POST /v1/sheets/{id}/rows/query`; `columns` selects and orders the
output (default: every declared column; for a linked sheet, the primary key, then the
base relation's columns, then the extension columns -- the grid's order). Linked
sheets run under the requester's own caller token and are refused without one;
off-page pending edits are not hydrated.

Authorization mirrors the table export: `sheet:read`, `data:query` AND `data:export`
(403 otherwise), then read access to the sheet (404 for a caller who cannot read it,
exactly like rows/query). System actors are refused (403) and render sessions never
reach this route (404). Every successful export writes a `data.export` audit entry
(resource type `sheet`; format, row count, truncated).

`style: formatted` (xlsx only; csv is refused 400) bakes the panel's conditional-format
rules as static cell styles, using the table export's serializer (DVT-4196): the client
sends the rules, resolved theme `tokens` and `frozenColumns` (same shapes and bounds as
`POST /v1/data/export`); the server never reads the dashboard spec. The rules evaluate
over the exported rows after formula neutralisation, so a text predicate sees the
`'`-prefixed value of a cell that started with `=`, `+`, `-` or `@`. The truncation
notice row is never styled. Rules x rows over the style budget is 422
`export-too-complex`. Text cells whose first character is `=`, `+`, `-`, `@`,
tab or CR are prefixed with `'` in BOTH formats so a spreadsheet never evaluates
them as formulas. Numbers, booleans and dates are written typed from the sheet's
declared column kinds, never sniffed from strings.

`X-DVT-Truncated: true` marks a result cut at 10,000 rows (or the byte cap); a
truncated XLSX also ends with one plain-text notice row (not counted in
`X-DVT-Row-Count`). If the warehouse statement exceeds the ~20 s export budget, or
the server's 24 s engine deadline or 28 s whole-request deadline passes, the response is 422 `sheet_export_timeout`
-- never a 504 and never a partial file. A linked sheet whose pending edits were
truncated is refused 503 rather than exported without them.

## Request body

- ExportSheetRequest: POST /v1/sheets/{id}/export request body. Unknown fields are rejected.
  - `format` 'csv' | 'xlsx', required
  - `style` 'data' | 'formatted': `formatted` (xlsx only; 400 for csv) bakes `conditionalFormat` as static cell styles (DVT-5185).
  - `conditionalFormat` ExportConditionalFormatRule[]: The sheet panel's conditional-format rules; same shape and budgets as `ExportRequest.conditionalFormat`. Formatted style only.
    - `where` ExportCellPredicate, required: Mirrors the web CellPredicate (DVT-4196) evaluated by a formatted export's conditional-format rules and target matching: either a leaf comparison (`field`/`op`/`value`) or an `all`/`any` combinator over nested predicates. A rule with more than 500 predicate nodes is rejected with 400 (see the request-level budgets on `conditionalFormat`). Nesting deeper than 5 levels is not rejected: nodes past that depth simply never match, exactly as in the renderer.
      - `field` string: Result column this leaf tests. Absent → the field the rule is being evaluated for (the owning column hint's field, or — for a table-wide rule applied per column — the column currently under test).
      - `op` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'between' | 'in' | 'notIn' | 'contains' | 'isNull' | 'isNotNull' | 'topN' | 'bottomN': Comparison operator for this leaf. Absent/empty on a leaf never matches.
      - `value` unknown
      - `all` ExportCellPredicate[]: True only when every nested predicate matches (AND).
      - `any` ExportCellPredicate[]: True when any nested predicate matches (OR).
    - `apply` ExportCellStyle, required: Mirrors the web CellStyle (DVT-4196) — the paint a matching conditional- format rule applies. Baked to a static XLSX fill/font on export; never an Excel conditional-formatting rule (the DVT-4195 decision).
      - `fill` unknown
      - `textColor` unknown
      - `weight` 'normal' | 'medium' | 'bold': Only "bold" renders bold in XLSX; "medium" is accepted but not bold.
      - `italic` boolean: Tri-state, mirroring the renderer's object spread: absent leaves whatever an earlier rule (or the column's textStyle) set, `true` turns it on, and an EXPLICIT `false` turns it back off.
      - `underline` boolean: Tri-state — see `italic`.
      - `strikethrough` boolean: Tri-state — see `italic`.
      - `align` 'left' | 'center' | 'right'
    - `target` string: "row" (paint the whole row), "cell" (the default — paint only the field the rule tested), or an explicit field name to paint a different column than the one the predicate tested.
    - `stopIfMatched` boolean: Stop evaluating later rules in the same list once this rule matches a given cell/row.
  - `frozenColumns` integer: Freezes the first N output columns (clamped to the column count). Formatted style only.
  - `tokens` object: The client's RESOLVED theme token map (`{token.name}` -> literal), as in `ExportRequest.tokens`. Formatted style only.
  - `columns` string[]: Declared sheet column names to export, in output order (each at most once). Default: every declared column, in declaration order.
  - `filters` object[]: Up to 20 filter clauses, ANDed together. Each clause must specify exactly one kind: `text` (case-insensitive contains; text/uuid columns), `min`/`max` (number/integer columns — may use either or both), `after`/`before` (date/ timestamp columns; `before` is inclusive of the whole selected day), `eq` (exact match against one scalar), or `in` (exact match against any of 1..1000 scalars). A clause that mixes kinds is rejected with 400. `eq`/`in` apply to text, uuid, integer, boolean and date columns; text matching is exact and case-sensitive. Values may be string, number or boolean — never null — and a string value is at most 256 characters. `in` is capped at 1000 values in aggregate across all clauses. A value that cannot be coerced to the column's type is rejected with 400 and `code: filter_value_invalid`.
    - `column` string, required: The declared sheet column name this clause applies to.
    - `text` string: Case-insensitive substring match (text/uuid columns).
    - `min` number: Lower bound, inclusive (number/integer columns).
    - `max` number: Upper bound, inclusive (number/integer columns).
    - `after` string: Inclusive lower date bound (date/timestamp columns). ISO 8601 date.
    - `before` string: Inclusive upper date bound (date/timestamp columns; inclusive of the whole selected day). ISO 8601 date.
    - `eq` union: Exact match against one scalar (text, uuid, integer, boolean, date columns). Text comparison is case-sensitive. Not null; strings at most 256 characters. An uncoercible value is a 400 with `code: filter_value_invalid`.
      - string
      - number
      - boolean
    - `in` union[]: Exact match against any of the listed scalars (same column kinds and rules as `eq`). 1..1000 values, counted in aggregate across all clauses in the request. Members are never null.
      - union
        - string
        - number
        - boolean
  - `sort` union: Sort on 1-3 columns. Send one `{column, dir}` object (the original single-key shape, still accepted) or an array of 1-3 distinct keys, most significant first. Columns are matched case-insensitively and may not repeat; an empty array or more than 3 keys is rejected with 400. Omit for default (unspecified) ordering.
    - SheetRowSort: One sort key.
      - `column` string, required: A declared sheet column name to sort by.
      - `dir` 'asc' | 'desc', required: Sort direction.
    - SheetRowSort[]
      - `column` string, required: A declared sheet column name to sort by.
      - `dir` 'asc' | 'desc', required: Sort direction.

## Response `200`

The serialized export file (CSV or XLSX) as an attachment.

## Other responses

- `400`: Invalid body, an unknown/duplicate `columns` entry, an invalid filter or sort, an invalid `style`/`conditionalFormat`/`tokens`/`frozenColumns`, or `style: formatted` with `format: csv`. This route's own 400s are untyped (no `X-Dvt-Error-Code` header); an `eq`/`in` filter value the engine cannot coerce to the column type is the typed `filter_value_invalid` (problem `code` extension).
- `401`: Missing, invalid, or expired session.
- `403`: The caller's role does not permit this action.
- `404`: Resource does not exist.
- `409`: The sheet has no writeback connection, its connection is not writeback, or a linked sheet's base-relation posture / caller token check failed (the same Problem types as `POST /v1/sheets/{id}/rows/query`).
- `413`: The request body exceeds 2 MiB.
- `422`: `https://docs.dvt.dev/errors/export-too-complex` -- a `style: formatted` export's conditional-format rules x row count exceed the server's style-evaluation budget. Otherwise `https://docs.dvt.dev/errors/sheet_export_timeout` (`code: sheet_export_timeout`) -- the export's warehouse statement exceeded its ~20 s budget, the engine call hit its 24 s deadline, or the whole request (linked-path preparation, engine call and file serialisation) passed its 28 s deadline. Narrow the filters and retry. Only the timeout sets `X-Dvt-Error-Code`; `export-too-complex` does not.
- `429`: Two causes, distinguished by the Problem `type` (DVT-3464 split this from a single shared `errors/rate-limited`, which made an engine-capacity shed indistinguishable from an actual rate limit): - `https://docs.dvt.dev/errors/rate-limited` — a rate or concurrency limiter for this operation rejected the request before it reached the engine. See `components/responses/RateLimited` for the general per-actor SEC-2 tier; some operations stack an additional operation-specific limiter (the query submit rate limit, the per-org running-job cap, or the trial query ceiling). - `https://docs.dvt.dev/errors/engine-at-capacity` — the engine's own admission control (ADR-0053) shed the request before it reached the warehouse — a dvt engine limit, not a warehouse or rate problem (DVT-765). `suggestion` names the actual wait in seconds and, when the engine reports a scope, whether the per-org or global engine concurrency cap was hit. Both carry `Retry-After` (seconds); back off for at least that long before retrying.
- `501`: Row reads are not yet supported for Redshift connections.
- `502`: The analytics engine could not be reached (`https://docs.dvt.dev/errors/engine-unavailable`) or returned an error (`https://docs.dvt.dev/errors/engine-error`). One 502 on this shape is NOT an engine fault and says so: when Snowflake refuses the caller's-rights LOGIN, the Problem type is `https://docs.dvt.dev/errors/caller-connect-refused` — no session was established and the caller's SQL was never reached, so the remediation is a fresh dvt session rather than the engine logs (DVT-4345).
- `503`: Row reads are not configured, or the sheet has too many pending edits to serve a merged view (including a truncated pending overlay on a linked sheet) -- retry after the next flush tick. The `Retry-After` header carries the seconds to wait.

---

[API](https://skmtc.dev/dvt/apis/dvt-rest-api.md) · [All operations](https://skmtc.dev/dvt/apis/dvt-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dvt/apis/dvt-rest-api/revisions/00016f504165?raw)
