Docs · Developer reference
VeraStream partner JSON export · finance · AP · integration-stage

The audit-workpaper JSON export — auth model, HTTP shape, and a partner-faithful sample envelope.

A developer-facing reference for GET /api/audit-workpapers/export — the same endpoint your nightly GL, ERP, or audit pipeline hits to pull workpaper output into something it can sign off on. Token-gated on production deployments; open on preview so the operator can cURL it directly. The envelope below is byte-faithful to what the wire produces — copy it into your parser, not a hand-shape.

The endpoint, in one line

One verb, one route, one envelope

A single GET that returns a JSON attachment. No POST, no PUT, no GraphQL — the wire shape is intentionally trivial so a finance SRE can hit it with curl and a downstream ETL can parse it with a three-field zod schema. Auth is a single header, validation is calendar-aware, and pagination is window-shaped.

Method
GET
Path
/api/audit-workpapers/export
Auth
x-internal-token(required when POLSIA_INTERNAL_TOKEN is set on the deployment)
Content-Type
application/json; charset=utf-8
Content-Disposition
attachment; filename="audit-workpapers-<from>-<to>.json"
Cache-Control
private, max-age=0, no-store

Authentication

Token-gated on production; open on preview

The endpoint mirrors the admin token-gate pattern at /security: when the deployment's POLSIA_INTERNAL_TOKEN environment variable is set, every caller must send the matching x-internal-token header. On a preview / unset env the route is open so the operator can hit it directly without configuring one. There are no API keys to mint and no OAuth flow to negotiate — one env var, one header, exact match.

Required when set
x-internal-token
Set in env
POLSIA_INTERNAL_TOKEN
On mismatch
401 {"error":"unauthorized"}

When POLSIA_INTERNAL_TOKEN is unset the route is open. The operator uses that mode on preview deploys to verify the envelope shape end-to-end before wiring the token into the production deploy.

Request shape

Three valid query params, one conditional header

All three query params are optional. from/to narrow by calendar window; status narrows by whether the operator has produced an artefact (token issued) for that workpaper. status omitted means both. There is no detector filter and no format selector on this wire — the shape is intentionally fixed so a partner integrator parses the same envelope regardless of which detector fired.

ParamTypeBehaviour
fromISO 8601 date (YYYY-MM-DD) or full timestampInclusive lower bound — filters `createdAt >= from`. Accepts a calendar date or a full timestamp (regex-guarded against malformed values; `new Date(...)` is re-checked).
toISO 8601 date (YYYY-MM-DD) or full timestampCalendar-date upper bound — widened server-side to half-open `[gte, lt + 1 day)` so callers passing `to=2026-08-19` still capture rows written at 23:59:59Z on that day.
statusDELIVERED | PENDING (omit = both)DELIVERED filters `attachmentToken != null` (rows the operator has produced an artefact for). PENDING filters `attachmentToken == null` (still in queue). Omit for both.
HeaderRequiredValue
x-internal-tokenOnly when POLSIA_INTERNAL_TOKEN is set on the deploymentThe configured token value. Mismatched or missing returns 401 {"error":"unauthorized"}. On a preview / unset env the route is open so the operator can hit it directly.
AcceptNo (default ok)application/json — the response is always JSON; the wire does not accept other formats.

Validation errors all return 400 with a single-field body: {"error":"invalid_from"}, {"error":"invalid_to"}, or {"error":"invalid_status"} — exact-match the body in your error handler. A mismatched or missing token returns {"error":"unauthorized"} at status 401.

Pagination & limits

1000 rows per response; paginate by window

The endpoint caps each response at 1000 workpapers — a partner-grade dump, not a full ledger snapshot. To pull more, narrow the from/to calendar window and concatenate the envelopes downstream. The meta.dateRange field echoes the window you sent, so a downstream pipeline can reconstruct the full range from the response alone without a separate audit log.

Cap
1000 workpapers per response
Strategy
Narrow the calendar window per call (e.g. one month at a time) and merge the envelopes downstream.

Sample request

A curl you can paste verbatim

The token is passed via the x-internal-token header; the response is written to disk as a downloadable JSON file. The window in the example matches the response preview below so the meta.dateRange lines up character-for-character.

curl
bash · zsh
curl -sS \
  -H "x-internal-token: $POLSIA_INTERNAL_TOKEN" \
  "${NEXT_PUBLIC_APP_URL}/api/audit-workpapers/export?from=2026-01-01&to=2026-08-19&status=DELIVERED" \
  -o audit-workpapers.json

Sample response

A JSON envelope byte-faithful to the wire

The shape below is what GET /api/audit-workpapers/export?from=2026-01-01&to=2026-08-19&status=DELIVERED actually returns — the same zod schema the route validates against. Copy it into your parser; the field names, the nesting, and the four-entry priority-ordered lineItems array are all stable.

application/json
attachment · JSON
{
  "workpapers": [
    {
      "id": "ck8a3p7qm2v1j9f4z6y0tbnx",
      "company": "Northwind Trade Holdings",
      "workEmail": "[email protected]",
      "createdAt": "2026-08-19T12:34:56.000Z",
      "status": "DELIVERED",
      "totals":   { "rowCount": 1234, "flagCount": 27 },
      "lineItems": [
        { "severity": "CRITICAL", "count": 0 },
        { "severity": "HIGH",     "count": 3 },
        { "severity": "MEDIUM",   "count": 8 },
        { "severity": "LOW",      "count": 16 }
      ]
    }
  ],
  "meta": {
    "dateRange":   { "from": "2026-01-01", "to": "2026-08-19" },
    "status":      "DELIVERED",
    "totals":      { "records": 1, "rowCount": 1234, "flagCount": 27 },
    "generatedAt": "2026-08-19T13:00:00.000Z"
  }
}

Why is CRITICAL always 0?

The lineItems array in the wire always has four entries — one per severity — in priority order: CRITICAL, HIGH, MEDIUM, LOW. Persistence at /api/audit/report-request folds CRITICAL into HIGH (highCount = severityCounts.HIGH + severityCounts.CRITICAL), so the exported CRITICAL row always round-trips as 0 and the HIGH count already includes what would be CRITICAL in the live summary. meta.totals.flagCount = highCount + mediumCount + lowCount — the same number the operator queue shows.

Partner-grade caveats

Four boundary conditions, all stated up-front

The caveats below are the partner-grade boundary conditions a finance / AP integrator will discover on the second sprint of wiring this into a nightly pipeline. Each is documented in the route's top-of-file comment so the wire and the docs agree.

  1. CRITICAL folds into HIGH. Persistence at /api/audit/report-request folds CRITICAL into HIGH (highCount = severityCounts.HIGH + severityCounts.CRITICAL), so the exported CRITICAL line item always round-trips as 0 and the HIGH count already includes what would be CRITICAL in the live summary.
  2. flagCount = HIGH + MEDIUM + LOW. As a direct consequence, meta.totals.flagCount is highCount + mediumCount + lowCount — the same number the admin queue shows.
  3. Per-finding detail is not in the structured store. Per-finding line items (vendor / amount / date / reason) are not persisted in the structured store; this endpoint surfaces severity-bucket aggregates only. For finding-level evidence, use the per-report download token issued to the report recipient.
  4. 1000-row cap. The route caps each response at 1000 rows (a partner-grade dump). Callers needing more should paginate by narrowing the from/to calendar window.

Frequently asked

The five questions the partner integrator asks first

Auth model, the (lack of) detector and format params, the CRITICAL fold, the response cap, and whether the wire is safe to wire into a nightly GL / ERP / audit pipeline. Plain HTML answers — no JavaScript required to read.

What auth model does the audit-workpaper export endpoint use?

Token-gated. When the deployment has POLSIA_INTERNAL_TOKEN set, every caller must send x-internal-token: <value>- where the value matches the configured token exactly. Mismatched or missing returns 401 {"error":"unauthorized"}. On a preview / unset env no token is required, so the operator can hit the endpoint directly without configuring one. This is the same control-plane pattern that protects the admin token-gated routes and the partner share-token artefacts (see /security).

Are detector or format query params supported?

No — and this page is honest about that. The endpoint accepts only from, to, and status. There is no detector filter (the wire returns all eight detectors aggregated into the four severity buckets) and no format selector (the wire is always application/json; charset=utf-8 with a content-disposition: attachment filename pattern of audit-workpapers-<from>-<to>.json and cache-control: private, max-age=0, no-store). If your pipeline needs a detector slice, filter the resulting JSON envelope client-side; if it needs CSV, row-flatten the envelope yourself.

Why is the CRITICAL line item always 0 on the exported workpaper?

By design. Persistence at /api/audit/report-request folds CRITICAL into HIGH (highCount = severityCounts.HIGH + severityCounts.CRITICAL) so the exported CRITICAL line item round-trips as 0 and the count under HIGH already includes what would be CRITICAL in the live summary. meta.totals.flagCount is therefore highCount + mediumCount + lowCount — the same number the admin queue shows. The lineItems array always has the four severities in priority order so every workpaper parses to the same shape.

How big can one export response be, and how do I paginate?

The endpoint caps each response at 1000 rows (a partner-grade dump, not a full ledger snapshot). When you need more, paginate by narrowing the from/to calendar window — fetch January, then February, then March, and concatenate the envelopes downstream. The meta.dateRange object on each envelope echoes the window you sent so downstream code can reconstruct the full range from the response alone.

Is this safe to wire into a nightly GL / ERP / audit pipeline?

Yes. The endpoint sits behind the same control plane as the rest of the partner exports — token-gated, no-store cacheable, attachment-dispositioned so a browser download and a curl pipe both land on disk, and the envelope shape is byte-stable. For the encryption posture that wraps the artefact (TLS 1.2+ in transit, AES-256 at rest, signed share tokens on the per-report downloads) see /security. Per-finding line items (vendor / amount / date / reason) are not in the structured store — the endpoint surfaces severity-bucket aggregates only; for finding-level evidence use the per-report download token issued to the report recipient.

Read the wire on your own ledger

Drop a CSV at /audit — or run the same wire continuously at /pricing.

The same detector pipeline that produces the workpaper envelope runs on your AP ledger at /audit — no signup, no sales call required to see first findings. Pick Continuous or Enterprise at /pricing to launch the same eight detectors continuously against your live ERP. For the encryption + token-gated posture that wraps the endpoint, see /security.