Download an audit export

GET /v1/audit-exports/{export_id}/download · permission audit-exports:read

Gives a single-use link to the package of a completed export job. API keys (secret or restricted) and dashboard users can download; a dashboard download needs a recent sign-in (step-up, at most 5 minutes old). Every call writes an audit_export.downloaded record to the admin stream before the link is issued.

Request

Parameter In Required Description
export_id path yes Export job id, prefixed with aex_.

A job created with include_ip: true also requires audit-events:read_ip on the downloading key.

Response

The answer depends on the Accept header. Both forms carry Cache-Control: no-store, Referrer-Policy: no-referrer and Vary: Accept, and both write the same audit_export.downloaded record.

Following the link returns 200 OK with Content-Type: application/x-tar, Content-Length and Content-Disposition: attachment; filename="audit-export-aex_….tar". The tar package holds:

File Contents
records.ndjson.gz Every chain record of each covered hour, in sequence order, one JSON object per line: matching records in full, the others as content-free placeholders ("omitted": true) so the hash chain can be rebuilt without revealing them.
manifest.json Filter, counts, file hash and, per covered hour, the signed checkpoint and inclusion proof; canonical JSON (RFC 8785).
manifest.jws ES256 signature over manifest.json; verify it with the public keys in audit-jwks.json.

If the connection ends before Content-Length bytes arrive, the package is incomplete; discard it and request a new link. An invalid, used or expired link returns 404 not_found.

Errors

Every error is an application/problem+json body with a stable code.

HTTP code When
401 unauthenticated The API key is missing, invalid or revoked.
403 insufficient_permissions The key lacks audit-exports:read, the job was created with include_ip and the key lacks audit-events:read_ip (required_permissions lists both), or the caller is neither an API key nor a dashboard user.
404 not_found The job does not exist, belongs to another environment, or the id is malformed. On the link: the token is invalid, used or expired.
409 conflict The job is not completed yet, or its file has expired.
429 rate_limited The rate limit was exceeded; honor Retry-After.
503 service_unavailable A dependency is temporarily unavailable; retry later.

Example

curl -L -o audit-export.tar \
  https://api.eu.keenauth.com/v1/audit-exports/aex_01j9xk3m7q8r2s4t6v8w0y2z4e/download \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY"

With Accept: application/json the link comes back as JSON instead of a redirect:

curl https://api.eu.keenauth.com/v1/audit-exports/aex_01j9xk3m7q8r2s4t6v8w0y2z4e/download \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Accept: application/json"

The SDKs use the JSON form and return url and expires_at. The CLI reads the link itself, writes the package to a file (never to standard output), refuses to overwrite an existing file and prints the size and SHA-256 of what it wrote:

keenauth audit-exports download aex_01j9xk3m7q8r2s4t6v8w0y2z4e --file audit-export.tar

Personal data

This endpoint carries personal data: audit events. Personal data in the KeenAuth APIs describes what is stored, how long it is kept and how it is deleted. Do not send special categories of personal data (health, biometric data, religion, political opinions, sexual orientation) in any field.