> ## 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 reconcile incremental updates across inclusive, non-snapshot scans.

Paginated mutable-resource collections use limit/offset pagination. `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&updated_since=2026-08-27T09%3A00%3A00Z 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&updated_since=2026-08-27T09%3A00%3A00Z",
  "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
and results are stably ordered by `updated_at`, then `id`. The Mahnservice
API offers offset pagination only — its `updated` field on invoices and
documents is a change marker to compare, not a filter.
There is no snapshot or cursor guarantee: writes during a scan can move rows
relative to an offset and cause a page-only consumer to miss them.

Use an overlapping reconciliation window instead of treating one offset scan
as complete:

1. For the initial sync, omit `updated_since`. Thereafter, store a high-water
   timestamp and choose an overlap longer than the longest expected full page
   sweep. Query from `high_water - overlap`.
2. Follow every returned `next` URL. Reconcile each row by `(id, updated_at)`,
   retaining the newest representation and safely accepting duplicates.
3. After all pages are durably applied, move the high-water timestamp to the
   greatest observed `updated_at`. Keep applying the overlap on the next run.
4. If a scan takes longer than the overlap, is interrupted, or runs during
   unusually heavy writes, do not advance the high-water timestamp. Repeat
   from the same lower bound with a larger overlap.
5. Run a periodic full reconciliation (omit `updated_since`, or use a retained
   older floor) to repair any row movement outside the operational overlap.

An inclusive boundary avoids timestamp-tie loss but does not make offset
pagination snapshot-safe. No finite overlap alone can prove that every row was
seen under arbitrary concurrent writes; use webhooks for prompt change signals
and periodic full reconciliation 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/?limit=100&offset=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.
Across overlapping sweeps, 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 timestamp and 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, timedelta, 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_HIGH_WATER = "2026-08-31T10:00:00Z"
SYNC_OVERLAP_SECONDS = "300"
SYNC_SWEEPS = "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_high_water = parse_aware_instant(SYNC_HIGH_WATER)
overlap = timedelta(seconds=int(SYNC_OVERLAP_SECONDS))
sweep_count = int(SYNC_SWEEPS)
if overlap <= timedelta(0) or not 1 <= sweep_count <= 10:
    raise ValueError("Use a positive overlap and one to ten bounded sweeps")

versions = set()
resources_by_id = {}
resource_instants_by_id = {}
for _ in range(sweep_count):
    lower_bound = (committed_high_water - overlap).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_high_water
    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_url = page.get("next")
        params = None
    committed_high_water = greatest_applied

WORKFLOW_RESULT = {
    "sweeps": sweep_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_high_water": committed_high_water.isoformat(),
}
```

The example makes two overlapping sweeps and keeps the newest version of
each resource. It limits each sweep to five pages and rejects pagination
cycles and URLs outside the configured API origin. Choose a finite page
budget and overlap suitable for your dataset. An interrupted sweep must
retain its previous checkpoint; also retain it if the sweep takes longer
than the overlap.

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](/api-docs/case-management-api/workflows/follow-a-case) 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, restart from the last committed `updated_since`
lower bound rather than guessing a larger offset.

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


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