> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gavai.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Output formats

> Pick the response encoding that matches your reader — JSON for browsers and SDKs, NDJSON for streaming, CSV for spreadsheets, TSON for token-efficient AI pipelines.

export const ArrowItem = ({children}) => <li className="gv-arrow-item">
    <span className="gv-arrow-item__arrow" aria-hidden="true">{"↳︎"}</span>
    <span className="gv-arrow-item__content">{children}</span>
  </li>;

export const ArrowList = ({children}) => <ul className="gv-arrow-list">{children}</ul>;

export const Def = ({term, type, defaultValue, required, children}) => <>
    <dt className="gv-def-list__term">
      <span className="gv-def-list__name">{term}</span>
      {type && <span className="gv-def-list__type">{type}</span>}
      {required && <span className="gv-def-list__required">required</span>}
      {defaultValue !== undefined && <span className="gv-def-list__default">
          default <code>{defaultValue}</code>
        </span>}
    </dt>
    <dd className="gv-def-list__description">{children}</dd>
  </>;

export const DefList = ({children}) => <dl className="gv-def-list">{children}</dl>;

export const FeatureTable = ({columns, rows, featuredRow, caption, variant}) => <div className={`gv-feature-table ${variant ? `gv-feature-table--${variant}` : ""}`}>
    <table>
      {caption && <caption className="gv-feature-table__caption">{caption}</caption>}
      <thead>
        <tr>
          {columns.map(col => <th key={col.key} scope="col">
              <span className="gv-feature-table__header">{col.label}</span>
            </th>)}
        </tr>
      </thead>
      <tbody>
        {rows.map((row, i) => {
  const rowClass = [i === featuredRow ? "gv-feature-table__row--featured" : "", row.chosen ? "gv-feature-table__row--chosen" : ""].filter(Boolean).join(" ");
  return <tr key={i} className={rowClass}>
              {columns.map(col => {
    const v = row[col.key];
    if (variant === "tradeoff" && Array.isArray(v)) {
      const pill = col.key === "pros" ? "gv-feature-table__pill--pro" : col.key === "cons" ? "gv-feature-table__pill--con" : "";
      return <td key={col.key}>
                      {v.map((item, j) => <span key={j} className={`gv-feature-table__pill ${pill}`}>{item}</span>)}
                    </td>;
    }
    return <td key={col.key}>{v}</td>;
  })}
            </tr>;
})}
      </tbody>
    </table>
  </div>;

export const Eyebrow = ({label, tone = "default"}) => <div className={`gv-eyebrow gv-eyebrow--${tone}`}>
    <span className="gv-eyebrow__bar" aria-hidden="true" />
    <span className="gv-eyebrow__label">{label}</span>
  </div>;

<Eyebrow label="FORMATS" />

The default `application/json` envelope is the right answer for almost every interactive call. Bulk reads, agent pipelines, and spreadsheet exports want different shapes. The platform negotiates four output encodings on read endpoints; pick the one that matches your reader by setting `Accept` or the `format` query parameter. By the end of this page you will know which encoding to ask for, what it looks like on the wire, and what trade-offs come with each.

## The four formats

<FeatureTable
  columns={[
{ key: "format", label: "Format" },
{ key: "media_type", label: "Media type" },
{ key: "best_for", label: "Best for" }
]}
  rows={[
{
  format: "JSON",
  media_type: "`application/json`",
  best_for: "Default. Interactive calls, the official SDKs, anything that wants a single typed envelope back."
},
{
  format: "NDJSON",
  media_type: "`application/x-ndjson`",
  best_for: "Streaming bulk reads. Each record is its own line; a consumer can start processing before the response has finished."
},
{
  format: "CSV",
  media_type: "`text/csv`",
  best_for: "Spreadsheet exports and data-warehouse loads. Flat columnar shape; no nested structures."
},
{
  format: "TSON",
  media_type: "`application/tson`",
  best_for: "Token-efficient passes to a language model. Same logical content as JSON in materially fewer tokens."
}
]}
/>

## Asking for a format

Two equivalent ways. Both honor the same negotiation rules.

```bash theme={null}
# Set the Accept header
curl https://api.gavai.app/v1/workspaces/acme/forms/frm_01H.../submissions \
  -H "Authorization: Bearer gak_live_..." \
  -H "Accept: application/x-ndjson"

# Or set the query parameter
curl "https://api.gavai.app/v1/workspaces/acme/forms/frm_01H.../submissions?format=ndjson" \
  -H "Authorization: Bearer gak_live_..."
```

The query parameter wins if both are set. If neither is set, the server returns `application/json`.

Not every endpoint negotiates every format. Endpoints that return a single resource (`GET /v1/workspaces/{slug}`) only honor `application/json`; alternative formats are for endpoints that return a list. A request for a non-supported format on a single-resource endpoint returns `406 not_acceptable` rather than silently falling back.

## NDJSON: streaming bulk reads

Newline-delimited JSON: one record per line, no wrapping envelope, no trailing comma management. The response body is a stream — bytes start flowing before the server has materialized the whole result set.

```jsonl theme={null}
{"id":"sub_01H...","submitted_at":"2026-05-09T09:22:11Z","data":{"email":"a@example.com"}}
{"id":"sub_01H...","submitted_at":"2026-05-09T09:22:14Z","data":{"email":"b@example.com"}}
{"id":"sub_01H...","submitted_at":"2026-05-09T09:22:18Z","data":{"email":"c@example.com"}}
```

Two practical notes:

<ArrowList>
  <ArrowItem>**Pagination is in trailers.** The cursor and `has_more` flag arrive as HTTP response trailers, not in the body. A streaming reader inspects the trailer after the last line; a non-streaming reader can fall back to a final line that carries the same metadata under a sentinel `__meta__` key.</ArrowItem>
  <ArrowItem>**Errors mid-stream end the stream.** If an error happens after the first record was sent, the server closes the connection and emits the error as a trailer field. A streaming reader treats an early EOF as a failed read and consults the trailer for the reason.</ArrowItem>
</ArrowList>

## CSV: spreadsheet exports

Flat columnar output, RFC 4180 quoting, UTF-8 with a BOM by default for Excel compatibility (suppress with `?bom=false`). Nested fields are flattened with a dot-separated path:

```csv theme={null}
id,submitted_at,data.email,data.name
sub_01H...,2026-05-09T09:22:11Z,a@example.com,Alex Kim
sub_01H...,2026-05-09T09:22:14Z,b@example.com,Bao Liu
```

Two constraints to know:

<ArrowList>
  <ArrowItem>**Lossy on arrays.** A field that holds an array is serialized as a semicolon-separated string. Round-tripping a CSV export back into a write endpoint is not supported.</ArrowItem>
  <ArrowItem>**Pagination cursor is in a header.** `X-Next-Cursor` carries the next page cursor; the body itself has no envelope to put it in.</ArrowItem>
</ArrowList>

## TSON: token-efficient for AI pipelines

TSON (Typed Structured Object Notation) is a compact encoding designed for passing structured data to language models. It encodes the same logical content as JSON but with materially fewer tokens — list a schema once, then reference fields positionally.

```tson theme={null}
@schema[id submitted_at data.email data.name]
sub_01H... 2026-05-09T09:22:11Z a@example.com "Alex Kim"
sub_01H... 2026-05-09T09:22:14Z b@example.com "Bao Liu"
```

The first line declares the field schema; each subsequent line is a record, with fields in declared order. Strings without whitespace or special characters can be unquoted; quoting only triggers when ambiguity demands it.

The format is *lossy on schema metadata* — a TSON record knows what its fields are called but not what their types are. Use TSON when:

<ArrowList>
  <ArrowItem>**The reader is a language model and you are paying for tokens.** A list of 200 records is roughly 40–60% the token count of the equivalent JSON.</ArrowItem>
  <ArrowItem>**The schema is stable.** TSON's compactness comes from declaring the schema once; if every record carries a different shape, JSON is the right answer.</ArrowItem>
  <ArrowItem>**Round-tripping is not required.** TSON is one-way: read-friendly for a model, not write-friendly back into the API. Convert to JSON before posting back.</ArrowItem>
</ArrowList>

When the platform's MCP servers expose a list endpoint to an AI client, they negotiate TSON automatically. You only request it explicitly when you are passing data to a model through your own pipeline.

## Negotiation rules

Three rules apply across all formats.

<DefList>
  <Def term="Unsupported format → 406">
    A request for `application/x-ndjson` on a single-resource endpoint returns `406 not_acceptable`. The platform does not silently fall back to JSON; it surfaces the mismatch.
  </Def>

  <Def term="format= wins over Accept">
    If both `Accept: text/csv` and `?format=ndjson` are present, NDJSON is returned. The query parameter is the explicit instruction.
  </Def>

  <Def term="Errors are always JSON">
    A non-2xx response carries an `application/json` error envelope regardless of the requested format. A CSV reader needs to handle the case that the response body is JSON when the status is not 2xx.
  </Def>
</DefList>

## Compression

All four formats negotiate gzip via `Accept-Encoding: gzip` and zstd via `Accept-Encoding: zstd`. Compression is recommended for bulk reads — NDJSON and TSON in particular compress well because their line shapes repeat. The platform applies compression only when the encoded body is over 4 KB; smaller responses are sent uncompressed regardless of negotiation.

## Related

<Card title="Pagination" icon="forward" href="/api-reference/pagination">
  How cursors work across all four formats — and where each one carries the cursor.
</Card>
