
# 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.

- **`Accept` lists `application/json`** (explicitly, with a non-zero `q`; `*/*` does not count): `200 OK` with a JSON body. Use this form from a browser or any client that cannot read a cross-origin redirect: read `url`, then open it.

  | Field | Type | Description |
  |---|---|---|
  | `url` | string | Relative link on the same host, `/downloads/audit-exports/{environment_id}/{export_id}/{token}`. |
  | `expires_at` | string (date-time) | When the link stops working (15 minutes after it was issued). |

- **Any other `Accept`**: `302 Found` with an empty body. `Location` is the same relative link.

- The link is valid for 15 minutes and works once: the token is spent on the first request, even if the transfer is interrupted. Call this operation again for a new link.
- The link needs no credentials; the token is the only credential. Keep `Location` (or `url`) secret and do not log it.
- The file is encrypted at rest with the environment's key, so no storage presigned URL is used; KeenAuth decrypts and streams it.

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

```bash
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:

```bash
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:

```bash
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](/docs/policies/personal-data) 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.
