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

# Exports

> Get your complete filtered dataset as a CSV or JSONL download, generated as an async job.

Lists page at up to 100 rows per request, which is right for an app and wrong
for a BI load. **Exports** produce one file with the complete filtered dataset
of an entity (processes, actuaciones or defendants), up to 100,000 rows, and
hand you a download URL.

```
POST https://api.legal.usecroma.com/v1/exports
```

Because a large portfolio takes longer than a single request should hold, an
export runs as an **async job**: the same request can resolve three ways, and
you choose which fits your app.

<Note>
  **Small exports feel synchronous.** By default the request waits up to 20
  seconds and, if the file is ready, returns `200 { data }` with the URL. The
  other modes are opt-in, for when you'd rather not hold the connection open.
</Note>

## The request

```json theme={"dark"}
{
  "entity": "processes",
  "format": "csv",
  "filters": { "status": "TRACKING", "date_from": "2024-01-01" }
}
```

| Field          | Required | Meaning                                                                                                                                                                                 |
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `entity`       | yes      | `processes`, `actions` or `defendants`.                                                                                                                                                 |
| `format`       | no       | `csv` (default; UTF-8 with BOM so Excel renders accents) or `jsonl` (one JSON object per line, best for scripts).                                                                       |
| `filters`      | no       | Same names as the list endpoints' query params, in one flat object. Filters that don't apply to the chosen entity are ignored. See [ExportFilters](/legal/api-reference/create-export). |
| `callback_url` | no       | Public HTTPS URL to `POST` the result to. Turns on [callback mode](#mode-3-callback-webhook).                                                                                           |

Unknown fields in the body (or in `filters`) are rejected with `400`.

The result, once the file exists:

```json theme={"dark"}
{
  "data": {
    "url": "https://files.usecroma.com/legal-exports/processes-2026-08-21.csv",
    "entity": "processes",
    "format": "csv",
    "rows": 1280,
    "bytes": 418233,
    "truncated": false,
    "expires_at": "2026-08-22T14:03:10.000Z"
  }
}
```

* `url` is unguessable but not authenticated: anyone holding it can download
  the file. It is guaranteed until `expires_at` (24 hours after completion)
  and removed shortly after; download promptly and don't post it anywhere
  public.
* `truncated: true` means the 100,000-row cap was hit and the file is **not**
  the complete dataset. Narrow the filters (for instance by date range) and
  export again.

## The three ways to get the result

| Mode                      | You send                           | You get back                                      |
| ------------------------- | ---------------------------------- | ------------------------------------------------- |
| **Wait inline** (default) | nothing extra, or `Prefer: wait=N` | `200 { data }` if it finishes in time, else `202` |
| **Poll**                  | `Prefer: wait=0`                   | `202` now, then `GET /jobs/:id` until done        |
| **Callback**              | `callback_url` in the body         | `202` now, then a `POST` to your URL when done    |

All three are backed by the **same job**, so pick per request.

***

## Mode 1: Wait inline (default)

Just call the endpoint. The request holds open until the file is ready (up to
**20 seconds** by default) and returns `{ data }`.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl -X POST https://api.legal.usecroma.com/v1/exports \
    -H "Authorization: Bearer $CROMA_LEGAL_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "entity": "defendants", "format": "jsonl" }'
  ```

  ```ts TypeScript theme={"dark"}
  const res = await fetch("https://api.legal.usecroma.com/v1/exports", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CROMA_LEGAL_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ entity: "defendants", format: "jsonl" }),
  });

  if (res.status === 200) {
    const { data } = await res.json(); // data.url is ready to download
  } else if (res.status === 202) {
    // not ready in time; poll the status URL (see Mode 2)
  }
  ```
</CodeGroup>

Control how long to wait with the standard
[`Prefer: wait=N`](https://www.rfc-editor.org/rfc/rfc7240#section-4.3) header
(seconds, clamped to **55**):

```bash theme={"dark"}
curl -X POST https://api.legal.usecroma.com/v1/exports \
  -H "Authorization: Bearer $CROMA_LEGAL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Prefer: wait=50" \
  -d '{ "entity": "processes", "format": "csv" }'
```

* **`200`**: done. Body is `{ "data": … }` as above.
* **`202`**: not done within the wait. Body is a [job envelope](#the-job-envelope);
  follow its `status_url` to poll. The `X-Job-Id` header carries the job id.

<Note>
  Always handle both `200` and `202`. A `202` is not an error; it just means
  "still generating, come back for it." A portfolio of tens of thousands of
  actuaciones routinely takes longer than the default 20-second wait.
</Note>

***

## Mode 2: Poll

Send `Prefer: wait=0` to get a `202` immediately, then `GET` the job's
`status_url` until it reaches a terminal state.

<CodeGroup>
  ```bash cURL theme={"dark"}
  # 1. Start the export (returns 202 immediately)
  curl -i -X POST https://api.legal.usecroma.com/v1/exports \
    -H "Authorization: Bearer $CROMA_LEGAL_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Prefer: wait=0" \
    -d '{ "entity": "actions", "filters": { "since": "2026-01-01" } }'

  # 2. Poll the status_url from the response (or the Location header)
  curl https://api.legal.usecroma.com/jobs/run_abc123def456 \
    -H "Authorization: Bearer $CROMA_LEGAL_API_KEY"
  ```

  ```ts TypeScript theme={"dark"}
  // 1. Start the export
  const start = await fetch("https://api.legal.usecroma.com/v1/exports", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CROMA_LEGAL_API_KEY}`,
      "Content-Type": "application/json",
      Prefer: "wait=0",
    },
    body: JSON.stringify({ entity: "actions", filters: { since: "2026-01-01" } }),
  });
  const { job } = await start.json();

  // 2. Poll until terminal
  async function poll(statusUrl: string) {
    while (true) {
      const res = await fetch(statusUrl, {
        headers: { Authorization: `Bearer ${process.env.CROMA_LEGAL_API_KEY}` },
      });
      const body = await res.json();
      if (body.job.status === "completed") return body.data;
      if (["failed", "canceled", "expired"].includes(body.job.status)) {
        throw new Error(body.error?.message ?? body.job.status);
      }
      // honor Retry-After; defaults to 2s while running
      const wait = Number(res.headers.get("Retry-After") ?? 2) * 1000;
      await new Promise((r) => setTimeout(r, wait));
    }
  }

  const { url } = await poll(job.status_url);
  ```
</CodeGroup>

`GET /jobs/:id` always returns `200` with the [envelope](#the-job-envelope).
While the job is still running it includes a `Retry-After` header (seconds);
use it to pace your polling. Polling has its own generous
[rate-limit bucket](/legal/rate-limits). Jobs are scoped to your organization;
a job belonging to another organization reads as `404`.

***

## Mode 3: Callback (webhook)

Include a `callback_url` in the request body. You get a `202` right away, and
Croma `POST`s the result to your URL once the file is ready, with no polling.

```bash theme={"dark"}
curl -i -X POST https://api.legal.usecroma.com/v1/exports \
  -H "Authorization: Bearer $CROMA_LEGAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "entity": "processes",
        "format": "csv",
        "callback_url": "https://your-app.com/webhooks/croma-legal"
      }'
```

`callback_url` must be an absolute **HTTPS** URL on a public host (localhost
and private ranges are rejected). When the job finishes, Croma sends a `POST`
to it:

* **Body**: the same [job envelope](#the-job-envelope) as the poll endpoint.
* **`x-croma-job-id`**: the job id.
* **`x-croma-signature`**: `sha256=<hmac>`, an HMAC-SHA256 of the raw request
  body, so you can verify the payload's integrity.

```http theme={"dark"}
POST /webhooks/croma-legal HTTP/1.1
content-type: application/json
x-croma-job-id: run_abc123def456
x-croma-signature: sha256=9f86d081…

{ "job": { "id": "run_abc123def456", "status": "completed", … }, "data": { "url": "…" }, "error": null }
```

<Note>
  Signature verification uses a shared secret issued by Croma; reach out if you
  want it enabled for your callbacks. Respond `2xx` quickly; non-`2xx`
  responses are retried.
</Note>

***

## The job envelope

The `202` response, `GET /jobs/:id`, and the callback body all share one shape:

```json theme={"dark"}
{
  "job": {
    "id": "run_abc123def456",
    "status": "completed",
    "endpoint": "/v1/exports",
    "created_at": "2026-08-21T14:03:10.000Z",
    "finished_at": "2026-08-21T14:03:41.000Z",
    "status_url": "https://api.legal.usecroma.com/jobs/run_abc123def456"
  },
  "data": { "url": "…", "entity": "processes", "format": "csv", "rows": 1280, "bytes": 418233, "truncated": false, "expires_at": "2026-08-22T14:03:41.000Z" },
  "error": null
}
```

* **`data`** is populated only when `status` is `completed`. Otherwise it's
  `null`.
* **`error`** is populated only when the job has `failed`, as
  `{ "type", "code", "message" }` with `code: "job_failed"`.

### Statuses

| `status`    | Terminal? | Meaning                                            |
| ----------- | --------- | -------------------------------------------------- |
| `queued`    | no        | Accepted, waiting to run.                          |
| `running`   | no        | Generating the file.                               |
| `completed` | yes       | Done. Read `data.url`.                             |
| `failed`    | yes       | The export errored. Read `error`; create it again. |
| `canceled`  | yes       | The run was canceled.                              |
| `expired`   | yes       | The run expired before completing.                 |

## What's in the file

Columns are the same whether you pick `csv` or `jsonl`. Nested values
(`metadata`) are serialized as JSON text in CSV cells.

<Tabs>
  <Tab title="processes">
    `id`, `registration_number`, `plaintiff_name`, `defendant_name`,
    `defendant_id`, `office`, `status`, `registration_date`,
    `last_discovery_date`, `is_private`, `latest_action_message`,
    `latest_action_date`, `metadata`.

    The two `latest_action_*` columns carry the most recent actuación of each
    case, so the file reads like the portfolio view in the dashboard.
  </Tab>

  <Tab title="actions">
    `id`, `process_id`, `registration_number`, `message`, `note`,
    `registration_date`, `action_created_at`, `source`, `publication_url`,
    `document_url`, `plaintiff_name`, `defendant_name`, `defendant_id`,
    `defender_name`, `office`, `priority`, `is_private`,
    `last_discovery_date`, `metadata`.

    This is the largest dataset: filter by `since`, `date_from` / `date_to`,
    `office` or `defendant` to stay under the row cap.
  </Tab>

  <Tab title="defendants">
    `id`, `identification_number`, `name`, `process_count`, `metadata`,
    `created_at`.
  </Tab>
</Tabs>

## Headers reference

| Header                       | Where                  | Meaning                                                                                                       |
| ---------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------- |
| `Prefer: wait=N`             | request                | Seconds to wait inline before returning `202`. Default 20, clamped to 55. `wait=0` returns `202` immediately. |
| `Preference-Applied: wait=N` | `200` response         | Echoes the wait that was applied.                                                                             |
| `Location`                   | `202` response         | The job's `status_url`.                                                                                       |
| `Retry-After`                | `202` / running poll   | Suggested seconds before polling again.                                                                       |
| `X-Job-Id`                   | `202` / `200` response | The job id.                                                                                                   |

<CardGroup cols={2}>
  <Card title="POST /v1/exports" icon="code" href="/legal/api-reference/create-export">
    Full request and response schema, with the filters per entity.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/legal/errors">
    How failures and the `error` object work.
  </Card>
</CardGroup>
