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

# Pagination and synchronization

> Page through paywise collections and synchronize incremental changes with the keyset cursor of updated_since lists.

Paginated mutable-resource collections use limit/offset pagination unless you
filter them with `updated_since` (see
[Incremental synchronization](#incremental-synchronization)). `limit`
defaults to `10` and accepts `1`–`100`; `offset` is the zero-based position.
Both are validated, not coerced: a non-integer value (ASCII digits only),
`limit` outside that range, or a negative `offset` returns
`400 validation_error` with `errors[].code = "invalid_parameter"` on the
parameter — a `limit` above 100 is no longer silently capped. Follow the returned `next` URL instead of
rebuilding it, and stop when `next` is `null`.

Sort order uses one query parameter, `ordering`, on every list that can be
sorted: the order and mandate collections (`name`, `created`, `amount`,
`status`), the statement collections (for example `ordering=-booking_date`),
the Partner company list, and the Mahnservice invoice (`created`, `due_date`,
`amount`, `invoice_number`) and debtor (`created`, `customer_number`) lists.
Terms are comma-separated, and a `-` prefix sorts descending. Without
`ordering`, the order, mandate, statement, and Mahnservice lists are newest
first; on the order, mandate, and Mahnservice lists ties are broken by `id`.
Every other list is ordered as its reference documents. An undeclared
parameter — including the retired `sort` and `direction` — is `400` with
`unknown_parameter`, so read the operation reference before adding one.
An empty `ordering=` value is treated as omitted and keeps the default.
Otherwise `ordering` is strict: an unknown field or an empty comma-separated
term (for example `created,,name`) is `400 validation_error` with
`invalid_parameter` on `ordering`.

Not every collection is paginated. In particular, the immutable Case and
Partner legal-form catalogs return an unpaginated JSON array and may be cached
privately for one day.

```http theme={null}
GET /v2/webhooks/?limit=1&offset=0 HTTP/1.1
Host: api.paywise.de
Authorization: Bearer <api-key>

HTTP/1.1 200 OK
Content-Type: application/json

{
  "count": 2,
  "next": "https://api.paywise.de/v2/webhooks/?limit=1&offset=1",
  "previous": null,
  "results": [
    {
      "id": "b0000000-0000-4000-8000-000000000001",
      "url": "https://hooks.example.test/paywise",
      "enabled": true,
      "events": ["order.submitted"],
      "description": "Production order events",
      "max_consecutive_failures": 50,
      "consecutive_failures": 0,
      "last_failure_at": null,
      "created_at": "2026-08-01T08:00:00Z",
      "updated_at": "2026-08-27T09:00:00Z"
    }
  ]
}
```

## Incremental synchronization

Mutable collections of the Case Management and Partner APIs accept a
timezone-aware RFC 3339 `updated_since`; a naive timestamp or another format
is `400 validation_error` with `invalid_parameter`. The filter is inclusive.
The Mahnservice API offers offset pagination only — its `updated` field on
invoices and documents is a change marker to compare, not a filter.

With `updated_since`, a list switches from offset to keyset paging:

* Results are ordered by `updated_at`, then `id`. A requested `ordering` is
  ignored, because the cursor needs that order.
* `next` carries an opaque `cursor` instead of `offset`. Follow it exactly;
  do not decode, build, or edit a cursor. `previous` is always `null`, and
  `count` is the size of the whole window, not of the remainder.
* `offset` together with `updated_since` is `400 validation_error` with
  `invalid_parameter` on `offset`. `cursor` without `updated_since` is
  `invalid_parameter` as well, and an altered cursor is `invalid_cursor`.
* Rows changed less than 60 seconds ago are held back until they are
  settled. A row whose change was still being committed therefore never
  lands behind your checkpoint.
* A row that changes while you page is returned again later in the same
  scan with its newer `updated_at`, instead of pushing another row out of
  the result. One complete scan sees every row changed since `updated_since`.

Keep one checkpoint per company and collection:

1. For the initial import, record the time you start (allow a few minutes
   for clock skew), omit `updated_since`, and follow every `next` URL. Use
   the recorded time as the first checkpoint.
2. On each later run, request `updated_since=<checkpoint>` and follow every
   returned `next` URL. Upsert each row by `id`, keeping the representation
   with the newest `updated_at`; the inclusive boundary returns rows at the
   checkpoint again, so accept duplicates.
3. After all pages are durably applied, move the checkpoint to the greatest
   `updated_at` you received. No overlap window is needed.
4. If a run is interrupted, keep the previous checkpoint. Either resume with
   the last unapplied `next` URL or start the run again from the checkpoint.

Use webhooks for prompt change signals and the checkpointed scan for
convergence.

### Synchronize claims by stable UUID

The claim collection accepts `order_id`, effective `status`, and
`updated_since` together. For example:

```http theme={null}
GET /v2/claims/?order_id=20000000-0000-4000-8000-000000000001&status=submitted&updated_since=2026-08-27T09%3A00%3A00Z&limit=100 HTTP/1.1
Host: api.paywise.de
Authorization: Bearer <api-key>
```

Treat filters as a view of current relationships, not a permanent partition.
If paywise review moves a claim, its `order_id` changes and its `updated_at`
advances. A change to the current order's debtor projection likewise advances
the claim cursor because `debtor_id` or `additional_debtor_ids` changed in the
claim read.

Follow the exact `next` URL returned by the server, including every filter. A
page can, for example, return
`https://api.paywise.de/v2/claims/?cursor=MjAyNi0wOC0yN1QwOToxNTowMCswMDowMHwzMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAxMDA&limit=100&order_id=20000000-0000-4000-8000-000000000001&status=submitted&updated_since=2026-08-27T09%3A00%3A00Z`.
Do not rebuild that URL or assume later pages still contain the same rows.
Deduplicate claim versions by `(id, updated_at)` and retain the newest
complete representation. A stored claim UUID can always be
confirmed directly at `/v2/claims/{id}/`, without scanning orders.

## Runnable reconciliation example

This Python example synchronizes the Case Management API's debtor collection. Use the
same approach for other mutable collections that support `updated_since`,
with a separate checkpoint for each company and collection. The key needs
read access to the collection being synchronized.

The example starts from a previously saved checkpoint and runs two
consecutive incremental syncs, each starting from the checkpoint the previous
run committed. It keeps its results in memory. In your application, load that
checkpoint and the existing local records, then persist each applied page
before advancing the checkpoint. For the initial import, omit `updated_since`
and follow all pages.

```python Python workflow theme={null}
from datetime import datetime, timezone
from urllib.parse import urlsplit
import requests

PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_API_KEY = "pw_sbx_your_api_key"
SYNC_CHECKPOINT = "2026-08-31T10:00:00Z"
SYNC_RUNS = "2"

base_url = PAYWISE_API_URL
base_parts = urlsplit(base_url)
if (
    base_parts.scheme != "https"
    or not base_parts.hostname
    or base_parts.username is not None
    or base_parts.password is not None
):
    raise ValueError("PAYWISE_API_URL must use HTTPS")
api_origin = (base_parts.hostname.lower(), base_parts.port or 443)
page_budget = 5
headers = {"Authorization": f"Bearer {PAYWISE_API_KEY}"}

def parse_aware_instant(value):
    try:
        instant = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except (AttributeError, TypeError, ValueError) as error:
        raise ValueError(f"Invalid RFC3339 timestamp: {value!r}") from error
    if instant.tzinfo is None or instant.utcoffset() is None:
        raise ValueError(f"Timestamp must include a timezone offset: {value!r}")
    return instant

committed_checkpoint = parse_aware_instant(SYNC_CHECKPOINT)
run_count = int(SYNC_RUNS)
if not 1 <= run_count <= 10:
    raise ValueError("Use one to ten bounded runs")

versions = set()
resources_by_id = {}
resource_instants_by_id = {}
for _ in range(run_count):
    lower_bound = committed_checkpoint.astimezone(timezone.utc)
    next_url = f"{base_url}/v2/debtors/"
    params = {
        "updated_since": lower_bound.isoformat().replace("+00:00", "Z"),
        "limit": 100,
    }
    greatest_applied = committed_checkpoint
    seen_urls = set()
    pages_read = 0
    while next_url:
        next_parts = urlsplit(next_url)
        next_origin = (
            next_parts.hostname.lower() if next_parts.hostname else "",
            next_parts.port or (443 if next_parts.scheme == "https" else None),
        )
        if (
            next_parts.scheme != "https"
            or next_origin != api_origin
            or next_parts.username is not None
            or next_parts.password is not None
        ):
            raise RuntimeError("Refusing a pagination URL outside the API HTTPS origin")
        if next_url in seen_urls:
            raise RuntimeError("Pagination cycle detected")
        if pages_read >= page_budget:
            raise RuntimeError("Pagination page budget exhausted")
        seen_urls.add(next_url)
        pages_read += 1
        page_response = requests.get(
            next_url, headers=headers, params=params, timeout=(5, 30)
        )
        if page_response.status_code != 200:
            page_response.raise_for_status()
            raise RuntimeError(f"Expected 200, received {page_response.status_code}")
        page = page_response.json()
        for row in page["results"]:
            resource_id = row["id"]
            updated_at = row["updated_at"]
            observed = parse_aware_instant(updated_at)
            version = (resource_id, updated_at)
            if version not in versions:
                versions.add(version)
                current_instant = resource_instants_by_id.get(resource_id)
                if current_instant is None or observed > current_instant:
                    resources_by_id[resource_id] = row
                    resource_instants_by_id[resource_id] = observed
            greatest_applied = max(greatest_applied, observed)
        # next carries the cursor and every filter; send it unchanged.
        next_url = page.get("next")
        params = None
    committed_checkpoint = greatest_applied

WORKFLOW_RESULT = {
    "runs": run_count,
    "resource_ids": sorted(resources_by_id),
    "versions": [list(version) for version in sorted(versions)],
    "selected_references": {
        resource_id: row.get("your_reference")
        for resource_id, row in sorted(resources_by_id.items())
    },
    "committed_checkpoint": committed_checkpoint.isoformat(),
}
```

The example keeps the newest version of each resource across both runs. It
limits each run to five pages and rejects pagination cycles and URLs outside
the configured API origin. Choose a finite page budget suitable for your
dataset. An interrupted run must retain its previous checkpoint.

Mandate history supports the same incremental reads through
`GET /v2/mandates/{id}/history/`. Its `updated_at` includes changes to
represented attachments and linked resources, so update existing entries
by status `id` when they reappear. See
[Follow a case](https://docs.paywise.de/api-docs/case-management-api/workflows/follow-a-case.md) for
main-case and subcase selection and the complete history representation.

## Recover interrupted syncs

After a timeout or transient `5xx`, retry the same `next` URL. If its result may
already have been applied, upsert by `id` so replay is harmless. After `429`,
wait for `Retry-After`, then resume from the last uncommitted page. If a stored
URL is no longer usable or answers `invalid_cursor`, restart from the last
committed checkpoint rather than guessing a cursor or offset.

See the current [orders collection](https://docs.paywise.de/api-docs/case-management-api/reference/orders/list-orders.md),
[mandates collection](https://docs.paywise.de/api-docs/case-management-api/reference/mandates/list-mandates.md), and
[Partner companies collection](https://docs.paywise.de/api-docs/partner-api/reference/companies/list-managed-companies.md)
for supported filters and response shapes. The
[Case legal-form catalog](https://docs.paywise.de/api-docs/case-management-api/reference/legal-forms/list-legal-forms.md)
shows the unpaginated exception.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.