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

# Claims

> Create agreement claims, manage attached alerts, and follow their lifecycle.

<Warning>
  This contract belongs to the Worker development preview. It is not served by the public API yet.
</Warning>

## Create a claim

```http theme={null}
POST /v1/claims
Content-Type: application/json

{"agreement_id":"AGREEMENT_ID","title":"Incorrect delivery charges"}
```

The response is a pending claim with a `Location` header. Creation requires an active or archived
agreement. Titles are limited to 200 characters and need not be unique. Creation is not idempotent;
inspect the list after an uncertain response before creating again.

Send `alerts` to add alerts in the same transaction. It takes the same selection and rules as
`POST /v1/claims/{id}/alerts` (see [Review and curate alerts](#review-and-curate-alerts)). If the request fails, for
example because an alert in `alert_ids` is already in another claim, no claim is created. A `filter`
skips alerts that do not qualify; read the created claim for its counts.

```json theme={null}
{
  "agreement_id": "AGREEMENT_ID",
  "title": "Freight surcharges",
  "alerts": { "filter": { "topic_ids": ["TOPIC_ID"], "statuses": ["pending"] } }
}
```

## Review and curate alerts

```http theme={null}
GET /v1/claims?agreement_ids=AGREEMENT_ID
GET /v1/claims/metrics?agreement_ids=AGREEMENT_ID
GET /v1/alerts?claim_ids=CLAIM_ID
POST /v1/claims/CLAIM_ID/alerts
Content-Type: application/json

{"alert_ids":["ALERT_ID"]}
```

Lists use cursor pagination. The filtered alerts endpoint returns canonical alert summaries for currently
attached alerts, including dismissed alerts. Removed and transferred alerts are excluded. Follow `GET /v1/alerts/{id}` for retained evidence and invoice references.

Add/remove/transfer commands take an alert selection: up to 5000 distinct `alert_ids`, or a `filter`
that uses the alert list filters as JSON, such as `{"filter":{"topic_ids":["TOPIC_ID"]}}` to claim a
whole topic. Each command is one transaction and returns `changed_count` and `skipped_count`. With
`alert_ids`, adding an alert already in another claim conflicts; with a `filter` it is skipped. Use
`/alerts/transfer` with a selection and `target_claim_id` to move alerts explicitly.
Use `/alerts/remove` to detach an alert. Claimed alerts return to pending; pending, dismissed,
and credited alerts retain their status. Adding or transferring an alert preserves dismissed and
credited verdicts; pending alerts become claimed. To pursue a dismissed or credited member, reopen it
with `POST /v1/alerts/reopen` (select credited alerts by `alert_ids`; a filter skips them). It stays attached to a pending or in-progress claim, and adding it again
claims it.

`summary` groups aggregates into `total`, `pending`, `claimed`, `credited`, and `dismissed`. Each contains
`alert_count`, `invoice_count`, `topic_count`, and `impact_by_currency` entries with `currency_code` and
an exact decimal-string `amount`. Use `summary.claimed` for the amounts being pursued;
pending, credited, and dismissed alerts remain attached but are excluded from that group.
The app claim amount is claimed plus credited impact; `summary.total` includes all attached statuses.
Credited impact is independent of recorded credits and refunds. Null alert impacts
contribute zero. An empty population has zero counts and an empty currency array.

Invoice and topic counts are distinct within each population. Alerts without a topic do not contribute to `topic_count`. Topics represented in multiple statuses count in each group but only once in `total`. An invoice with both claimed and
dismissed alerts appears in both status groups but only once in `total`, so status invoice
counts are not necessarily additive. Removed or transferred alerts do not contribute.

For example, the same invoice can contain claimed and dismissed alerts:

```json theme={null}
{
  "summary": {
    "total": {
      "alert_count": 4,
      "invoice_count": 3,
      "topic_count": 2,
      "impact_by_currency": [{ "currency_code": "NOK", "amount": "8000" }]
    },
    "pending": {
      "alert_count": 0,
      "invoice_count": 0,
      "topic_count": 0,
      "impact_by_currency": []
    },
    "claimed": {
      "alert_count": 1,
      "invoice_count": 1,
      "topic_count": 1,
      "impact_by_currency": [{ "currency_code": "NOK", "amount": "1000" }]
    },
    "credited": {
      "alert_count": 0,
      "invoice_count": 0,
      "topic_count": 0,
      "impact_by_currency": []
    },
    "dismissed": {
      "alert_count": 3,
      "invoice_count": 3,
      "topic_count": 1,
      "impact_by_currency": [{ "currency_code": "NOK", "amount": "7000" }]
    }
  }
}
```

Claim responses expose the agreement through `agreement: { id, title }`; creation still accepts `agreement_id`.

Empty claims have zero counts and an empty currency array. There is no currency conversion or refund subtraction.
Use `GET /v1/alerts/metrics?claim_ids=CLAIM_ID&statuses=claimed` for amounts restricted to claimed alerts.

## Change lifecycle

```http theme={null}
POST /v1/claims/CLAIM_ID/transition
Content-Type: application/json

{"status":"cancelled"}
```

Cancellation dismisses attached pending and claimed alerts while retaining membership and evidence.
Dismissed and credited alerts keep their verdicts and earlier dismissal feedback. Reopening to `pending` or `in_progress` changes only claim status. A completed claim must
be reopened before cancellation. Only pending/in-progress claims receive alerts; completed claims
lock membership changes, while cancelled claims may remove or transfer alerts.

`PATCH /v1/claims/{id}` edits only the title. To permanently delete a claim, use
`POST /v1/claims/{id}/delete` with an explicit `alert_outcome` of `pending` or `dismissed`.
Deletion removes the claim and its dependent records; alerts, dismissal snapshots, and activity
remain. The deleted claim is no longer retrievable.

Queries require Read permission and commands require Write. These commands do not start checks.
Deleting with `alert_outcome=dismissed` and a category or note starts a dismissal-context suggestion
run, as dismissing an alert does; cancellation does not, because its `Claim cancelled` note is not
feedback about the agreement. Exports remain a separate workflow.

## Errors and retries

| Result | Meaning and retry behavior                                                                                                                               |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid input, including duplicate alert IDs or an incompatible cursor. Correct the request.                                                             |
| `404`  | An addressed resource is missing or belongs to another organization. Repeated deletion returns this result.                                              |
| `409`  | A claim is locked or membership no longer matches the request. Read the current state before retrying. Repeated removal or transfer returns this result. |

Repeating an unchanged title or status is a no-op. Repeating an already-satisfied add creates no
membership or activity; adding a dismissed or credited member preserves its verdict. Creation can
produce another claim when retried and has no idempotency receipt.

Removing a dismissed or credited member, or deleting its claim with `alert_outcome=pending`,
preserves its verdict. Claimed members return to pending. Dismissal snapshots remain available as
evidence. Dismissal feedback is accepted only with `alert_outcome=dismissed`.

## Filter alerts by claim

Use `claim_ids` with alert lists, metrics, or groups to select alerts currently attached to one
or more claims. Comma-separated IDs combine with OR; other filters combine with AND. Missing or
foreign claim IDs match no alerts. Removed attachments are excluded, while dismissed alerts on
cancelled claims remain included unless filtered out by alert status.

```http theme={null}
GET /v1/alerts?claim_ids=CLAIM_ID
GET /v1/alerts/metrics?claim_ids=CLAIM_ID
GET /v1/alerts/groups?claim_ids=CLAIM_ID&group_by=topic
GET /v1/alerts?claim_ids=CLAIM_ID&topic_ids=TOPIC_ID
```

Topic groups contain counts and currency totals for the matching alerts only. Add `topic_ids=null`
(or `has_topic=false`) for matching alerts without a topic, and use `claim_ids=null` for alerts in
no claim. Keep `claim_ids` unchanged when following a cursor.

Explicit alert dismissal keeps the alert attached to its claim. Returning an alert to pending also
retains its attachment to a pending or in-progress claim. Use the claim remove command to detach it.
Reopening an alert on a cancelled claim releases that attachment.

## Refunds and credit-note evidence

A refund records money credited or paid back, excluding VAT. Credit-note attachments are supporting
evidence: they never add to refunded totals unless a refund record is created explicitly.
Refunds can be recorded and edited on completed or cancelled claims without changing claim status.

All paths below follow `/v1/claims/{id}`:

| Method | Path                                    | Purpose                                                |
| ------ | --------------------------------------- | ------------------------------------------------------ |
| GET    | `/refunds`                              | List refunds                                           |
| GET    | `/refunds/{refund_id}`                  | Read a refund and its ETag                             |
| POST   | `/refunds`                              | Create a refund, optionally linking a credit note      |
| PATCH  | `/refunds/{refund_id}`                  | Edit amount, currency, comment, or credit-note link    |
| DELETE | `/refunds/{refund_id}`                  | Delete the refund, preserving its note attachment      |
| GET    | `/credit-notes`                         | List direct and invoice-derived evidence, deduplicated |
| GET    | `/credit-notes/{credit_note_id}`        | Read the relationship and its ETag                     |
| POST   | `/credit-notes`                         | Attach evidence, optionally creating a refund          |
| POST   | `/credit-notes/{credit_note_id}/remove` | Remove direct evidence with an explicit refund outcome |

Both lists use the standard cursor pagination (50 by default, maximum 100). Refunds sort by creation
time and ID descending; credit notes sort by invoice creation time and ID descending. Cursors are
bound to the organization and parent claim. Find candidate credit notes with the invoice list, scoped to the claim's `suppliers`:
`GET /v1/invoices?categories=credit_note&supplier_ids=SUPPLIER_IDS&search=TERM&sort=issued_date`. It
returns the completed, nondeleted credit notes of those suppliers, which is what attachment checks; a
note already attached to this claim returns `409`. A claim without suppliers has no candidates.

### Record and edit a refund

```http theme={null}
POST /v1/claims/CLAIM_ID/refunds
Content-Type: application/json

{"amount":"125.375","currency_code":"EUR","comment":"Supplier repayment","credit_note_id":"CREDIT_NOTE_ID"}
```

Creation returns `201`, `Location`, and `ETag`. Amounts are positive decimal strings (up to 100
characters), stored without rounding. Currency is required and normalized to three uppercase letters.
Neither amount nor currency is inferred from the linked note or organization. Links may cross
currencies: linking evidence never changes an existing refund's amount or currency. Each refund is its
own resource; there is no batch command. To record refunds and complete a claim, write each refund,
then call the transition command.

The refund resource contains `id`, `amount`, `currency_code`, nullable `comment`, nullable
`credit_note_id`, nullable `created_by_user_id`, `created_at`, and `updated_at`.

```http theme={null}
PATCH /v1/claims/CLAIM_ID/refunds/REFUND_ID
If-Match: "ETAG_FROM_GET"
Content-Type: application/json

{"currency_code":"KWD","comment":null,"credit_note_id":null}
```

PATCH preserves omitted fields. Null clears the comment or link; amount and currency cannot be null.
An unchanged edit is a no-op. Optional `If-Match` protects edits and deletion against stale amount,
currency, comment, or link state. Unlinking or deleting a refund preserves the direct note attachment.
Legacy refunds without currency retain the existing organization-currency fallback (NOK if unset);
an edit persists that currency explicitly. New writes always supply currency.

### Attach and remove evidence

```http theme={null}
POST /v1/claims/CLAIM_ID/credit-notes
Content-Type: application/json

{"credit_note_id":"CREDIT_NOTE_ID","create_refund":true}
```

`create_refund` defaults to false. When true, attachment and refund creation are atomic: the refund
uses the absolute net credit-note amount and the note's currency. Missing/zero amounts or missing
currency reject automatic refund creation; evidence-only attachment remains available. Supplier and
credit-note eligibility rules still apply. An attachment can link one refund, and a refund can link
one note; the API does not allocate a note globally across claims.

Evidence contains a canonical `credit_note` invoice summary, `matched_invoices`, and nullable
`direct_attachment`. `matched_invoices` lists the claim invoices the note is matched to as `{id, title}`
references, where `title` is the invoice number, ordered by invoice number (missing numbers last) and ID. The attachment contains `attached_at`, `refund_created_by_attachment`, and the
nullable linked `refund`. A note can have both direct and invoice-derived evidence and appears once.

```http theme={null}
POST /v1/claims/CLAIM_ID/credit-notes/CREDIT_NOTE_ID/remove
If-Match: "ETAG_FROM_CREDIT_NOTE_GET"
Content-Type: application/json

{"refund_outcome":"delete_generated"}
```

Use `keep` to preserve any linked refund. `delete_generated` may delete only the refund generated
by that attachment and requires a current strong ETag from the relationship GET. Missing, weak, or
wildcard preconditions return `428`; a stale ETag returns `412`. Removing direct evidence never
changes invoice matches, so the note may remain visible as invoice-derived evidence. Removal returns `204`.

Duplicate attachment or conflicting links return `409`. Repeated refund deletion or direct-attachment
removal returns `404`. Creation is non-idempotent; inspect the current state after an uncertain
response before retrying. There is no idempotency receipt or automatic currency conversion.

### Refund totals

Claim list/detail responses and claim metrics include a separate `refund_summary`:

```json theme={null}
{
  "refund_summary": {
    "refund_count": 2,
    "amount_by_currency": [
      { "currency_code": "EUR", "amount": "125.375" },
      { "currency_code": "NOK", "amount": "500" }
    ]
  }
}
```

Totals aggregate refund records independently of alerts and credit-note joins, using the same claim
filters. Empty results have count zero and an empty array. The existing `summary.total`,
`summary.pending`, `summary.claimed`, `summary.credited`, and `summary.dismissed` describe alerts and remain unchanged. No outstanding
balance, conversion, or allocation is calculated.

## Claim list presentation

`suppliers` contains `{id, name}` references for the agreement's suppliers, sorted by name and ID.
Legacy claims without agreement suppliers fall back to suppliers of currently attached alerts on
nondeleted, nonduplicate invoices. Supplier filters and supplier-name search use this same population,
including empty claims with agreement suppliers. Multiple suppliers never duplicate a claim or its totals.

```http theme={null}
GET /v1/claims?supplier_ids=SUPPLIER_ID&search=transport&sort=updated_at&direction=desc
GET /v1/claims/metrics?supplier_ids=SUPPLIER_ID&search=transport
GET /v1/claims?statuses=in_progress&sort=updated_at&direction=desc
```

For status sections, use the metrics status counts and fetch each section with `statuses` and its own
cursor. Grouping loaded rows is also possible, but a single page does not represent every status's
complete contents. `creator_user_ids` selects creators. Use `mine=true` to select the current user's shared teams,
or `team_ids` to select teams explicitly; claims match through their agreement.

## Claim documents

```http theme={null}
GET /v1/claims/CLAIM_ID/documents?limit=100
```

Lists the source documents of the completed, nondeleted invoices whose alerts are attached to the
claim, one item per invoice: `{"invoice":{"id":"…","title":"10442"},"documents":[…]}`. `title` is the
invoice number, and `documents` are `InvoiceDocument` entries, primary first, as in
`GET /v1/invoices/{id}/documents`. Invoices without documents are omitted. Items are ordered by invoice
number (missing numbers last) and ID, with standard cursor pagination. A missing or foreign claim
returns `404`. Credit-note evidence is listed by `GET /v1/claims/{id}/credit-notes`; read a note's files
with `GET /v1/invoices/{id}/documents`. Download a file with `GET /v1/documents/{id}/download`.

## Claim exports

Download CSV, XLSX or PDF with one authenticated request. Every format uses the same endpoint and
returns file bytes directly with a download filename. Read permission is sufficient.

```http theme={null}
GET /v1/claims/CLAIM_ID/export?format=pdf&include_pending=false&include_credited=true
```

The response is `200`, with `Content-Disposition: attachment` and the appropriate content type.
There is no export resource, workflow, polling, or saved artifact. Repeating the request generates
a fresh file from a consistent database snapshot.

* `format` is required: `csv`, `xlsx`, or `pdf`.
* Claimed findings are always included; dismissed findings are never exported.
* `include_pending` and `include_credited` accept `true` or `false` and default to `false`.
  Pending findings join claimed findings in the outstanding bucket; credited findings appear separately.
* Only currently attached alerts are included; removed/transferred alerts stay excluded. Invoice
  eligibility and claim lifecycle status do not hide the selected attached findings.
* CSV contains alert rows with topic, invoice, status, currency, exact decimal amounts, explanation,
  and citations. Formula-like user-authored cells are prefixed with an apostrophe.
* XLSX contains those alert rows and a separate refund-summary sheet. Amounts and invoice numbers
  use text cells to preserve precision and leading zeros. Spreadsheet software may round imported
  CSV numbers automatically; use XLSX when exact spreadsheet values matter.
* PDF summarizes each topic once using stored descriptions and key questions, with up to two invoice
  examples per topic and up to two selected sources per topic/example. Examples represent selected
  statuses and prefer different invoices. Counts and amounts cover the entire selected population;
  examples do not change totals. Links lead to the full supporting detail in the app. Use CSV/XLSX
  for every alert row. Refunds stay separate. Headings are English; source text retains its language.
* Missing alert impacts contribute zero. Amounts keep their original currencies. Only refund records
  contribute to refunded totals; credit-note evidence never adds money.

For example, save the file directly using your usual bearer credential and organization header:

```bash theme={null}
curl --fail --get "$API_URL/v1/claims/$CLAIM_ID/export" \
  --header "Authorization: Bearer $API_KEY" \
  --header "X-Organization-Id: $ORGANIZATION_ID" \
  --data-urlencode 'format=pdf' \
  --data-urlencode 'include_pending=false' \
  --data-urlencode 'include_credited=true' \
  --output claim.pdf
```

Invalid format/flag values return `400`; missing, deleted or inaccessible claims return `404`.
Exporting never changes the claim. Source invoice and credit-note files remain available through
`GET /v1/documents/ID/download`; the claim export does not combine them into a ZIP.

PDF amounts use currency-specific decimal places and round half up for display, after aggregation. API, CSV, and XLSX amounts retain their exact precision.

## My records and teams

Use `mine=true` or explicit `team_ids` to filter by shared team rules. See
[My records and teams](/api-preview/conventions#my-records-and-teams) for matching rules,
empty results, errors, and pagination.
