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

# Manage agreements

> Create agreements, manage prices and source documents, and change status through REST.

Use a personal API key with **Write** access for this walkthrough. Write includes Read and covers
agreement edits, prices, document links, and upload authorization. Read is sufficient for agreement
details, matched invoices, metrics, selectors, and downloads.

Send `X-Organization-Id` on every API request to select an organization allowed by the key and your
current membership. Create keys under **Settings → Personal → API keys**; see
[authentication and API keys](/api-reference/authentication) for role limits and organization access.
This walkthrough uses existing supplier and recipient records.

## Create an agreement and select parties

Creation accepts every editable agreement field: `title`, `status`, `supplier_ids`, `recipient_ids`, `tag_ids`,
`effective_date`, `expiration_date`, `applicability`, `renewal`, `instructions`, `alert_settings`, and `matching_settings`. Status defaults to `draft`.
Set `status` to `active` to create an active agreement with a nonblank title. Agreements, prices and
document relationships remain editable in every status, including `archived`. For example:

```json theme={null}
{
  "title": "Support services",
  "status": "active",
  "supplier_ids": [],
  "recipient_ids": [],
  "effective_date": "2026-01-01",
  "expiration_date": "2026-12-31",
  "applicability": null,
  "renewal": { "mode": "none" }
}
```

Set `API_URL`, `API_KEY`, and `ORGANIZATION_ID` for your environment as shown in the [quickstart](/api-preview/quickstart#connect-and-check-access). You can also create an empty draft and edit it later:

```bash theme={null}
curl --fail-with-body "$API_URL/v1/agreements" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" -H 'Content-Type: application/json' \
  --data '{}' > agreement.json
export AGREEMENT_ID=$(jq -r '.id' agreement.json)
curl --fail-with-body "$API_URL/v1/suppliers" -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
curl --fail-with-body "$API_URL/v1/recipients" -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
export SUPPLIER_ID='<existing supplier ID>'
export RECIPIENT_ID='<existing recipient ID>'

jq -n --arg supplier "$SUPPLIER_ID" --arg recipient "$RECIPIENT_ID" '{
  title: "Support services",
  supplier_ids: [$supplier],
  recipient_ids: [$recipient],
  effective_date: "2026-01-01",
  expiration_date: "2026-12-31"
}' > agreement-input.json
curl --fail-with-body -X PATCH "$API_URL/v1/agreements/$AGREEMENT_ID" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" -H 'Content-Type: application/json' \
  --data-binary @agreement-input.json
```

Select at least one existing supplier to match invoices. An empty `supplier_ids` array matches no
invoices, including when the agreement is activated. An empty `recipient_ids` array means any
recipient. Both arrays default to empty. Both `suppliers` and `recipients` responses have the same
complete array shape. Within each selection, any listed ID may match; the two selections combine
with AND.

PATCH preserves omitted fields and clears nullable fields when given null. Each supplied nested
object or membership array replaces the complete field. For example, supplying `recipient_ids`
replaces the entire recipient selection. Agreement list and detail responses include every selected
supplier and recipient as `{ id, name, organization_number }` objects in plain arrays, ordered by ID.
There is no membership count cap or separate membership endpoint. The common request-body size limit applies.

## Organize agreements with tags

List the organization catalogue or create a tag with Write access (which includes reads):

```bash theme={null}
curl --fail-with-body "$API_URL/v1/agreements/tags?search=Priority&limit=50" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
curl --fail-with-body "$API_URL/v1/agreements/tags" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" -H 'Content-Type: application/json' \
  --data '{"name":"Priority"}' > tag.json
export TAG_ID=$(jq -r '.id' tag.json)
jq -n --arg tag "$TAG_ID" '{tag_ids: [$tag]}' > tag-assignment.json
curl --fail-with-body -X PATCH "$API_URL/v1/agreements/$AGREEMENT_ID" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" -H 'Content-Type: application/json' \
  --data-binary @tag-assignment.json
curl --fail-with-body "$API_URL/v1/agreements?tag_ids=$TAG_ID" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
```

Tag creation returns `{ id, name }` with `201`. Repeating a name in different case returns the
existing tag with `200`, preserving its spelling. Names are trimmed and limited to 1–80 characters.
The returned `Location` resolves that ID through the catalogue's `ids` filter.

Agreement list, detail, create and PATCH responses include every assigned tag:

```json theme={null}
{
  "tags": [{ "id": "33333333-3333-4333-8333-333333333333", "name": "Priority" }]
}
```

Use `tag_ids` on creation or PATCH to replace the complete selection. Omission on PATCH preserves
tags; `tag_ids: []` clears assignments without deleting catalogue tags. Every ID must belong to your
organization. Tags update the agreement's ETag without invalidating compliance checks or running
workflows. Reordered or duplicated IDs are a no-op. Tag-only edits also work during background work.

The catalogue uses `{ data, next_cursor }`, default 50/max 100, ordered by case-insensitive name then
ID. Continue with the same filters and returned cursor. `GET /v1/agreements?tag_ids=<id>,<id>` matches
agreements with any selected tag and combines with other filters using AND. Untagged agreements do
not match a tag filter. Catalogue reads need only Read access; creating or assigning tags
requires Write access.

## Configure agreement settings

Agreement detail also includes these editable settings:

```json theme={null}
{
  "instructions": "Use the signed service schedule when interpreting coverage.",
  "alert_settings": {
    "flag_undercharges": false,
    "flag_uncovered_items": true
  },
  "matching_settings": {
    "smart_matching_enabled": false,
    "smart_matching_criterion": null
  }
}
```

`instructions` is the app's context note for interpreting the agreement during compliance checks.
The alert flags control whether checks report undercharges and items outside agreement coverage.
Both default to false. Smart matching requires a nonblank criterion when enabled; the criterion
is limited to 1,000 characters. Supplying either settings object replaces the complete object,
resetting omitted flags to false and the omitted criterion to null. Omitted top-level fields are
preserved by PATCH; `instructions: null` clears the note. Saving never runs checks or matching.

## Read matched invoices

`relationships.invoice_match_count` in agreement responses counts completed, non-deleted invoices
currently matched to the agreement. Read those invoices with Read access:

```bash theme={null}
curl --fail-with-body "$API_URL/v1/invoices?agreement_ids=$AGREEMENT_ID&limit=50" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
```

The response uses `{ data, next_cursor }` and standard invoice summaries. Use the returned cursor
with the same agreement, filters and ordering to continue. Invoice search, party/team filters,
categories, currencies, financial validity, issue/due date ranges and ordering are supported.
`agreement_ids` accepts up to 50 comma-separated agreement IDs and matches any of them. Each invoice
appears once, even when it matches several selected agreements. Unknown, deleted, or foreign agreement
IDs contribute no matches. Other filters combine with AND; `deleted` defaults to false. The same
filter is available on invoice metrics. Reading never recalculates matches when settings change.

## Set invoice applicability

Read GET `/v1/agreements/{id}` to see `applicability`. It is part of the detail response; the list
endpoint returns summaries. These conditions describe which invoices the agreement can apply to.
They support reference fields (including order, buyer, seller, contract and project references)
and delivery fields (including name, address, city and country).

For example, this requires both a buyer reference containing SUPPORT and a Norwegian delivery:

```json theme={null}
{
  "applicability": {
    "match": "all",
    "conditions": [
      { "field": "buyer_reference", "operator": "contains", "value": "SUPPORT" },
      { "field": "delivery_country", "operator": "in", "values": ["NO"] }
    ]
  }
}
```

Use `match: "any"` if either condition may match. Set `applicability: null` to remove the additional
conditions. Supplier/recipient selection and effective/expiration dates remain separate fields.
Values retain their whitespace. Existing conditions may exceed current write limits; unsupported
stored conditions return `409` rather than appearing as no conditions. Saving conditions starts no matching workflow.

The invoice list endpoint has a different purpose: browsing invoices. It also offers currency,
category, issue/due-date ranges, financial validity, agreement-match presence, teams and deletion
filters. Those query filters are not currently supported as stored agreement conditions.

## Add amount and rate prices

```bash theme={null}
curl --fail-with-body "$API_URL/v1/agreements/$AGREEMENT_ID/price-items" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" -H 'Content-Type: application/json' \
  --data '[
    {"description":"Support hour","unit":"hour","currency_code":"NOK","price":{"type":"amount","amount":"125.50"},"vat_rate":"0.25"},
    {"description":"Service fee","price":{"type":"rate","fraction":"0.075","basis":"invoice subtotal"}}
  ]' > price-items.json
export ITEM_ID=$(jq -r '.[0].id' price-items.json)
```

Amounts and fractions are exact decimal strings. `"0.075"` means 7.5%; price rates may exceed one.
Discount, surcharge and VAT fractions are between zero and one. The inactive price variant is
absent. Unknown descriptions, currency, units and other values remain null. Prices must be
nonnegative; invalid precision and reversed validity dates are rejected without rounding.

PATCH `/price-items/{item_id}` keeps the ID. Supplying `price` replaces its whole amount/rate
variant. Positions start at one, default to appending and can be edited independently. Removing a
price uses DELETE and returns `204`; retained evidence from the prior value survives the deletion.
Agreement and price creation accept optional [idempotency keys](/api-preview/conventions#safely-retrying-requests).

POST, PATCH and DELETE on the price collection accept plain arrays of 1–100 entries. Use an array
of one to create a single price. POST returns `201` and PATCH returns `200`, each with a plain array
of affected prices in request order. DELETE returns `204`. Each request succeeds or fails as a whole.
Updates preserve omitted fields; every supplied ID must be unique and belong to this agreement.

```bash theme={null}
# Edit several prices: each object contains id and changed fields.
jq '[.[] | {id, currency_code: "NOK"}]' price-items.json > price-edits.json
curl --fail-with-body -X PATCH "$API_URL/v1/agreements/$AGREEMENT_ID/price-items" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" -H 'Content-Type: application/json' \
  --data-binary @price-edits.json

# Remove selected prices: the request body is an array of IDs.
jq '[.[1].id]' price-items.json > price-deletions.json
curl --fail-with-body -X DELETE "$API_URL/v1/agreements/$AGREEMENT_ID/price-items" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" -H 'Content-Type: application/json' \
  --data-binary @price-deletions.json
```

To protect a collection edit, GET any `/price-items` page without a search filter and send its
`ETag` in `If-Match` on collection PATCH/DELETE. Any `limit` or `cursor` is supported. It covers all
prices, even those on later pages. A search-filtered page or individual price has a different
validator. Collection PATCH returns the new collection `ETag`, so you can use it directly for the
next batch without another read.
An `Idempotency-Key` on POST protects the whole array from duplicate creation when retried.
Each semantic batch advances agreement freshness once and retains prior evidence for all changed
or deleted prices. Prices remain editable on archived agreements.

## Upload and link a source

Use the [direct upload protocol](/api-preview/quickstart#upload-a-file-and-import-it) with a key
that has Write access. After PUT succeeds, use the returned `document_id` to link that source:

```bash theme={null}
export DOCUMENT_ID='<document_id returned by upload>'
jq -n --arg id "$DOCUMENT_ID" '{document_id: $id, role: "terms"}' > document-link.json
curl --fail-with-body "$API_URL/v1/agreements/$AGREEMENT_ID/documents" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" -H 'Content-Type: application/json' \
  --data-binary @document-link.json
curl --fail-with-body "$API_URL/v1/documents/$DOCUMENT_ID/download" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
```

Fetch `download_url` directly without your API bearer token. Linking requires uploaded, nonempty
bytes matching the registered size. It neither copies the file nor starts processing. Repeating the
same link succeeds; changing role or explicit position requires PATCH of the individual relationship.
Roles are `terms`, `price_list`, `uncategorized`, and `excluded`. Positions start at zero and default
to appending. DELETE removes the relationship and preserves the shared Document and retained evidence.

## Import prices from a document

Link the price list as above, preferably with role `price_list`, then import it. The API extracts
its prices and appends them without a review step:

```bash theme={null}
jq -n --arg id "$DOCUMENT_ID" '{document_id: $id, default_currency: "NOK"}' > price-import.json
curl --fail-with-body "$API_URL/v1/agreements/$AGREEMENT_ID/price-imports" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" -H 'Content-Type: application/json' \
  --data-binary @price-import.json > price-import-created.json
export IMPORT_ID=$(jq -r '.id' price-import-created.json)

# Poll until status is completed, failed or cancelled.
curl --fail-with-body "$API_URL/v1/agreements/$AGREEMENT_ID/price-imports/$IMPORT_ID" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
```

PDF, XLSX, XLS, CSV, TSV, DOCX, PPTX, EML, MSG, plain text, PNG and JPEG Documents up to 256 MiB are
supported. For a workbook, `worksheets` limits extraction to named sheets; `instructions` tells the
extraction which table or column holds the agreed prices. `default_currency` fills in amount prices
whose source states no currency. Extraction usually takes a few minutes. Cancel it with
[the workflow run](/api-preview/workflow-runs) in `workflow_run_id`.

A completed import reports `counts` of its rows. Every extracted row has an outcome:

* `created`: appended as a new price, after the existing ones.
* `duplicate`: the same product at the same price as an existing price or an earlier row, so
  nothing was added. Case and punctuation in text don't matter; a different amount, unit,
  currency or validity makes a new price.
* `invalid`: breaks the price rules, for example a negative amount.

```bash theme={null}
# Every extracted row with its outcome and reason, as CSV.
curl --fail-with-body "$API_URL/v1/agreements/$AGREEMENT_ID/price-imports/$IMPORT_ID/download" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" > download.json
curl --fail-with-body "$(jq -r .download_url download.json)" -o prices.csv

# The prices this import created.
curl --fail-with-body "$API_URL/v1/agreements/$AGREEMENT_ID/price-items?price_import_id=$IMPORT_ID" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
```

Existing prices are never changed. Sending the same import again (the same file, even uploaded as
another Document, with the same settings) returns the existing import with `200` instead of
extracting it twice. A failed or cancelled import is retried by importing again. Every imported
price's `source` names its import and the Document it came from, so you can always show where a price came from,
and the price table keeps the values as extracted after you edit the price. Imported Documents are
retained for that reason.

A failed import appends nothing. `extraction_incomplete` means the extraction could not read every
price with confidence: import again with instructions that say where the prices are, or with the
worksheets that hold them.

## Edit safely, activate and archive

Individual agreement, price and document-link reads return an `ETag`. Send that resource's validator
in `If-Match` to protect an edit from concurrent changes:

```bash theme={null}
curl --fail-with-body -D agreement.headers "$API_URL/v1/agreements/$AGREEMENT_ID" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" > agreement.json
export AGREEMENT_ETAG=$(awk 'tolower($1)=="etag:" {gsub("\r", ""); print $2}' agreement.headers)
curl --fail-with-body -X PATCH "$API_URL/v1/agreements/$AGREEMENT_ID" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" -H "If-Match: $AGREEMENT_ETAG" \
  -H 'Content-Type: application/json' --data '{"title":"Annual support services","status":"active"}'
curl --fail-with-body "$API_URL/v1/agreements?statuses=active&valid_on=2026-06-01&limit=50" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
curl --fail-with-body -X PATCH "$API_URL/v1/agreements/$AGREEMENT_ID" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID" \
  -H 'Content-Type: application/json' --data '{"status":"archived"}'
```

A stale validator returns `412 precondition_failed`. Read the current resource before reapplying
the edit. Without `If-Match`, supplied fields apply to the current locked state.

Set `status` through the agreement PATCH endpoint: `active` activates or reactivates, and `archived`
archives the agreement. Status and other edits apply atomically. Agreements, prices and source
relationships remain editable in every status. The resulting active agreement requires a nonblank
title; supplier and recipient selections may remain empty.
Repeating the current status without content changes succeeds without updating timestamps or version.

Saving and activating persist your changes without starting matching, processing, extraction,
compliance checks or renewal execution. Changes to coverage, invoice conditions, dates, renewal terms,
prices and relevant sources advance freshness; unchanged submissions,
equivalent decimals and reordered membership sets do not. Display ordering alone does not invalidate
checks. Existing results retain their prior evidence and are not automatically refreshed.

Lists use `{ data, next_cursor }` with a default limit of 50 and maximum 100. Pass the same filters
and ordering on each page. Available ordering includes creation, title, effective date, expiration
date, first supplier, pending topics, alert progress and open alert impact; nulls sort last.
Every agreement carries an `alert_summary` with its alert counts, pending topics and impact per
status in your organization's currency. `has_alert_statuses=pending` lists agreements with pending
alerts, and `excluded_supplier_ids` and `excluded_tag_ids` leave agreements out. `valid_on` tests the date interval independently of draft/active/archived status,
and absent bounds are open-ended. See the generated endpoint reference for the complete filter schema.

## Delete an agreement

```bash theme={null}
curl --fail-with-body -X DELETE "$API_URL/v1/agreements/$AGREEMENT_ID" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
```

Deletion returns `204`. Use the agreement's current `ETag` as `If-Match` to protect against a
concurrent edit. The agreement and its nested resources then return `404` and disappear from normal
app lists. Historical matches, assessments, prices, source relationships and evidence are retained;
shared Documents are preserved. Deletion returns `409` while agreement work is active, except API
classification and detail suggestions, compliance checks, topic reconciliation, topic proposals and
dismissal-context suggestions. There is no
restore operation; creating a new agreement requires a new idempotency key.

Price-list imports, indexing and compliance-check execution remain separate operations.
Saving settings, linking a source and reading matched invoices never start them.

## Suggest a document role or agreement details

After uploading and linking PDFs, you can ask the API for suggestions with Write access:

| Request                                                                       | Result                                                                    |
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `POST /v1/agreements/{id}/documents/{document_id}/classify`                   | Suggested role, concise reason and optional price-import date context     |
| `POST /v1/agreements/{id}/suggest-details` with `{ "document_ids": ["..."] }` | Proposed agreement fields, extracted parties and canonical party mappings |

Both return `202` with `workflow_run_id`, `workflow_run_item_id`, and a polling URL in `Location`.
Use [workflow reads](/api-preview/workflow-runs) to follow progress and read the item result.

Classification works directly from a PDF, with a preview of at most ten pages. It does not need or
start extraction. Review the proposed `terms`, `price_list`, or `excluded` role and PATCH the
document relationship when you want to apply it. A skipped classification has `role: null`.

For detail suggestions, select 1–10 distinct attached PDFs whose document relationship role is
`terms`. Review and apply classification suggestions first, or set the role yourself. Other roles
return `400 validation_error`.

You can request details immediately: the job uses current extracted Markdown when available and
original PDFs otherwise. The model reads both formats
together. Either extraction strategy is usable, including content whose verification was skipped
or failed. The job does not start extraction or indexing.

Selected source files may total at most 50 MiB. Extracted Markdown is limited to 500,000 combined
UTF-8 bytes, and each original PDF supplied directly is limited to 15 MB (15,000,000 bytes). Extract
larger PDFs first or select smaller files. Oversized model inputs fail without truncation; missing
files fail with `source_unavailable`. Both commands reject non-PDF MIME types with `415`.

Selected artifacts are not pinned: replacement extraction or deletion can remove them before a queued job reads them. Submit a new suggestion request if that job fails with `source_unavailable`.

Detail results contain `suggestions` with title, dates, renewal terms, a smart matching criterion,
supplier and recipient. Unknown values remain null. Each identified party includes the extracted
`name` and `organization_number` under `extracted`, plus canonical `id`, `name`, and
`organization_number` under `canonical`. The API matches or creates parties through its shared
resolver. An organization number alone can match an existing party; creating one requires an
extracted name. A canonical mapping is null when the party cannot be resolved. Cancelling the job
prevents result publication but does not undo canonical parties already resolved.

Review the values, then PATCH only those you choose. Use canonical IDs in `supplier_ids` and
`recipient_ids`. Put a chosen criterion in `matching_settings`, retaining your intended
`smart_matching_enabled` value because nested objects replace the complete field. Use the current
agreement `ETag` in `If-Match` to protect against concurrent edits. The suggestion itself does not
change the agreement, attach parties, enable matching, or import prices.

Equivalent active requests reuse their run. Different active selections return `409` with that run's
reference. Cancel via the workflow endpoint or wait for completion. Repeating a failed or cancelled
request creates a linked retry. Agreement edits remain available during suggestions, and later
document or extraction changes do not invalidate completed results.

## Review suggested instruction edits

When someone dismisses an alert with a category or note, Watchdog reads the feedback and may propose
small edits to the agreement's `instructions`, the context compliance checks read. Each such
dismissal starts an `agreement_context_suggestions` workflow run for the agreement; its item result
lists the `suggestion_ids` it created, which may be none. An organization's runs execute one at a
time, in order. Find them with
`GET /v1/workflow-runs?type=agreement_context_suggestions&resource_type=agreement&resource_id={id}`.

```bash theme={null}
curl --fail-with-body "$API_URL/v1/agreements/$AGREEMENT_ID/context-suggestions" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
```

The list returns every pending suggestion that still applies, in the order it appears in the
instructions, with appends last. `anchor_text` is the exact span it changes and occurs once in the
instructions; it is empty when `operation` is `insert` and the text is appended as a new paragraph.
`replace` swaps the anchor for `proposed_text`, `insert` adds it after the anchor, and `delete`
removes the anchor. `evidence` lists the 20 most recent dismissed alerts behind the suggestion with
their category and note, plus the total.

With Write access, `POST …/context-suggestions/{suggestion_id}/accept` applies the edit as an
ordinary agreement edit and returns the agreement with its new `ETag`; the version advances and the
same limits apply as for PATCH. `POST …/reject` leaves the instructions unchanged and keeps the edit
on record so it is not proposed again without new evidence. Both return `409` when the suggestion
was already reviewed, and accept returns `409` when the instructions no longer contain its anchor
exactly once. Editing the instructions deletes the pending suggestions the edit no longer fits.

## Query matched-invoice counts and spend

Use `GET /v1/invoices/metrics?agreement_ids={id}` with Read access. It aggregates all matching
invoices without fetching individual rows or following pagination.

```bash theme={null}
# Matched invoice count and lifetime spend
curl --fail-with-body "$API_URL/v1/invoices/metrics?agreement_ids=$AGREEMENT_ID" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"

# Last twelve months, assuming the organization's local date is 2026-09-09
curl --fail-with-body \
  "$API_URL/v1/invoices/metrics?agreement_ids=$AGREEMENT_ID&issued_from=2025-09-09&issued_through=2026-09-09" \
  -H "Authorization: Bearer $API_KEY" -H "X-Organization-Id: $ORGANIZATION_ID"
```

These requests can run in parallel. Example lifetime response:

```json theme={null}
{
  "total_count": 17,
  "financially_invalid_count": 0,
  "currencies": [
    {
      "currency_code": "NOK",
      "invoice_count": 17,
      "total_amount_excluding_vat": "6800000.00",
      "total_amount_including_vat": "8500000.00"
    }
  ]
}
```

Use the lifetime `total_count` for **Matched invoices** and `total_amount_excluding_vat` for **Total
spend lifetime**. Use the date-filtered response's `total_amount_excluding_vat` for **Total spend last
12 months**. Credit notes reduce spend. Both dates are inclusive: calculate the start by subtracting
12 calendar months from the organization's current local date, clamping leap day to February 28.
Lifetime includes future-dated and undated invoices; the bounded period excludes them.

Totals remain separate for each currency. A null currency group represents unknown currency, and a
null amount means at least one contributing amount is missing. Do not add different currencies or
treat unknown amounts as zero. An empty set returns zero counts and an empty `currencies` array.

The same search, party/team, currency, category, financial-validity and issue/due-date filters as
the invoice list are supported. Omit table filters to keep the headline cards at agreement
scope; include them if the cards should follow the table selection. `financially_invalid_count`
reports arithmetic validation, not compliance-check coverage. Checked ratios remain deferred.

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