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

# Exports & disputes

> Run asynchronous export jobs to generate downloadable reconciliation reports, and open, evidence, and resolve disputes against exceptions.

This guide covers two operator workflows that both end in a downloadable or resolved artifact: **export jobs** (queue a report, poll until it succeeds, download the file) and **disputes** (open a dispute against an exception, attach evidence, then close it won or lost). Both are tenant-scoped from the JWT.

## Export jobs

***

Exports are asynchronous. You create a job scoped to a context, poll its status by ID, and download the file once it reaches `SUCCEEDED`. Statuses are `QUEUED`, `RUNNING`, `SUCCEEDED`, `FAILED`, `EXPIRED`, and `CANCELED`.

### Create an export job

`POST` to the context's export-jobs collection. Responds `202 Accepted` with the job ID and a poll URL.

```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/export-jobs" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reportType": "MATCHED",
    "format": "CSV",
    "dateFrom": "2025-01-01",
    "dateTo": "2025-01-31",
    "sourceId": "550e8400-e29b-41d4-a716-446655440000"
  }'
```

```json theme={null}
{
  "jobId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "QUEUED",
  "statusUrl": "/v1/export-jobs/550e8400-e29b-41d4-a716-446655440001"
}
```

* `reportType` — one of `MATCHED`, `UNMATCHED`, `VARIANCE`, `EXCEPTIONS` (aliases `MATCHES` and `UNMATCHED_TRANSACTIONS` are normalized).
* `format` — `CSV`, `JSON`, or `XML` (normalized to uppercase).
* `dateFrom` / `dateTo` — optional `YYYY-MM-DD`; `dateFrom` defaults to 30 days before `dateTo`, and `dateTo` defaults to tomorrow (UTC).
* `sourceId` — optional source filter.

<Note>`SUMMARY` and `PDF` are **not** supported for async export jobs and are rejected with `400`. The date window is also capped at a maximum span (an over-range request is rejected rather than silently clamped).</Note>

### Poll job status

Read the top-level job route (the `statusUrl` from creation).

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/export-jobs/{jobId}" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "reportType": "MATCHED",
  "format": "CSV",
  "status": "SUCCEEDED",
  "recordsWritten": 4250,
  "bytesWritten": 524288,
  "fileName": "matched_report_2025-01-31.csv",
  "createdAt": "2025-01-15T10:30:00Z",
  "startedAt": "2025-01-15T10:30:05Z",
  "finishedAt": "2025-01-15T10:35:00Z",
  "expiresAt": "2025-01-16T10:30:00Z",
  "downloadUrl": "https://storage.example.com/exports/matched_report.csv?token=abc"
}
```

`error` is present only when `status` is `FAILED`; `downloadUrl` appears only once the job has `SUCCEEDED` and the file is still available. You can list a context's jobs with `GET /v1/contexts/{contextId}/export-jobs`, list all jobs with `GET /v1/export-jobs`, and cancel a queued/running job with `POST /v1/export-jobs/{jobId}/cancel`.

### Download the file

Returns a presigned URL, the original file name, a SHA-256 checksum, and the URL's remaining lifetime in seconds.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/export-jobs/{jobId}/download" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "downloadUrl": "https://storage.example.com/exports/report.csv?token=abc",
  "fileName": "matched_report.csv",
  "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "expiresIn": 3600
}
```

<Warning>Export files are purged after `expiresAt` (default 7 days). Once a job is `EXPIRED` the file is no longer downloadable — re-run the export to regenerate it.</Warning>

## Disputes

***

A dispute is opened against a specific **exception** when a reconciliation discrepancy needs to be contested. Its lifecycle is `DRAFT` → `OPEN` → `PENDING_EVIDENCE` → `WON` / `LOST`.

### Open a dispute

`POST` to the exception's disputes collection.

```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/disputes" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "category": "BANK_FEE_ERROR",
    "description": "Transaction amount differs from invoice"
  }'
```

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "exceptionId": "550e8400-e29b-41d4-a716-446655440001",
  "category": "BANK_FEE_ERROR",
  "state": "OPEN",
  "description": "Transaction amount differs from invoice",
  "openedBy": "user@example.com",
  "evidence": [],
  "createdAt": "2025-01-15T10:30:00Z",
  "updatedAt": "2025-01-15T10:30:00Z"
}
```

`category` is one of `BANK_FEE_ERROR`, `UNRECOGNIZED_CHARGE`, `DUPLICATE_TRANSACTION`, or `OTHER`. The opening principal is recorded in `openedBy`.

### Submit evidence by URL

Attach a reference to an already-hosted evidence file plus a describing comment.

```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/disputes/{disputeId}/evidence" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "comment": "Attached bank statement showing correct amount",
    "fileUrl": "https://storage.example.com/evidence/doc123.pdf"
  }'
```

### Upload an evidence file

Stream the raw file bytes directly to tenant-scoped object storage — the comment travels as a query parameter and the file as the request body. Responds `201 Created` with the updated dispute. Allowed content types are `application/pdf`, `image/png`, `image/jpeg`, and `text/csv`; the body is capped at 10 MiB.

```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/disputes/{disputeId}/evidence/upload?comment=Bank%20statement%20showing%20correct%20amount" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/pdf" \
  --data-binary @statement.pdf
```

Each stored evidence item is returned on the dispute's `evidence` array:

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "disputeId": "550e8400-e29b-41d4-a716-446655440001",
  "comment": "Bank statement showing correct amount",
  "submittedBy": "user@example.com",
  "fileUrl": "https://storage.example.com/evidence/doc123.pdf",
  "submittedAt": "2025-01-15T10:30:00Z"
}
```

<Note>The upload endpoint fails closed with `503` when object storage is not configured, rejects oversize bodies with `413`, and rejects content types outside the allowlist with `415`. The tenant and dispute are always resolved from the JWT and path — never from the body.</Note>

### Close a dispute

Record the outcome. `won` sets the terminal state to `WON` or `LOST`, with a required `resolution` note.

```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/disputes/{disputeId}/close" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "won": true,
    "resolution": "Counterparty acknowledged the error and issued correction"
  }'
```

You can list disputes with `GET /v1/disputes` (filter by `state`, `category`, date range; sort and cursor-paginate) and fetch one with `GET /v1/disputes/{disputeId}`.

## Response codes

***

| Status | Meaning                                                                          |
| ------ | -------------------------------------------------------------------------------- |
| `200`  | Export/dispute data returned                                                     |
| `201`  | Evidence file uploaded                                                           |
| `202`  | Export job accepted                                                              |
| `400`  | Invalid input (unsupported report type/format, bad date range, invalid category) |
| `404`  | Context, export job, exception, or dispute not found                             |
| `409`  | Invalid dispute state transition                                                 |
| `413`  | Evidence file exceeds the 10 MiB cap                                             |
| `415`  | Evidence content type not in the allowlist                                       |
| `422`  | Malformed field                                                                  |
| `503`  | Export or evidence storage not configured                                        |
