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

# Follow a case

> Find an accepted mandate, read its current state, and consume the unified published history for its main or subcase track.

## Outcome

Your application has a current mandate read model and one durable, paginated
feed of the activity paywise published for the relevant main/subcase tracks.

## Prerequisites

* A Bearer key.
* The stable claim UUID retained at submission and the exact mandate UUID —
  from the claim's current `mandate_id`, the `order.accepted` payload's
  `mandate_id`, or `mandate.created`.

When `order.accepted` includes the stored claim UUID in `data.claim_ids`, read
`GET /v2/claims/{claim_id}/` once. Require claim status `accepted` and require
its `mandate_id` to match the event, then store the claim's current `order_id`.
The submitted order ID is informational after review: do not reconstruct
order-merge chains, and never follow `merged_into` before a claim command.

## 1. Read the known mandate

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_API_KEY = "pw_sbx_your_api_key"
  MANDATE_ID = "50000000-0000-4000-8000-000000000001"

  response = requests.get(
      f"{PAYWISE_API_URL}/v2/mandates/{MANDATE_ID}/",
      headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"}, timeout=(5, 30),
  )
  if response.status_code != 200:
      response.raise_for_status()
      raise RuntimeError(f"Expected 200, received {response.status_code}")
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/mandates/${process.env.MANDATE_ID}/`, {
    method: "GET", headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}` }, signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 200) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.time.Duration;

  class GetMandateRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(System.getenv("PAYWISE_API_URL") + "/v2/mandates/" + System.getenv("MANDATE_ID") + "/"))
          .timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY")).GET().build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 200) throw new IOException(response.body());
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Net.Http;
  using System.Threading;
  using System.Threading.Tasks;

  class GetMandateRequest
  {
      static async Task Main()
      {
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Get, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/mandates/{Environment.GetEnvironmentVariable("MANDATE_ID")}/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
      }
  }
  ```

  ```bash cURL theme={null}
  http_status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request GET "$PAYWISE_API_URL/v2/mandates/$MANDATE_ID/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" --output mandate.json --write-out '%{http_code}')"
  [ "$http_status" -eq 200 ] || exit 1
  ```
</CodeGroup>

Inspect `main_case` and `subcases` before deciding which tracks to render. The
mandate's `state` is the authoritative current legal, processing, and payment
state. For the overall case, read the main mandate; for one subcase, read that
subcase's mandate resource. Detail does not embed top-level document, payment,
statement, or request-to-client collections; claim documents remain below
their claims.

## 2. Walk unified history

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_API_KEY = "pw_sbx_your_api_key"
  MANDATE_ID = "50000000-0000-4000-8000-000000000001"

  response = requests.get(
      f"{PAYWISE_API_URL}/v2/mandates/{MANDATE_ID}/history/",
      headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"}, params={"limit": 100}, timeout=(5, 30),
  )
  if response.status_code != 200:
      response.raise_for_status()
      raise RuntimeError(f"Expected 200, received {response.status_code}")
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/mandates/${process.env.MANDATE_ID}/history/?limit=100`, {
    method: "GET", headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}` }, signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 200) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.time.Duration;

  class GetMandateHistoryRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(System.getenv("PAYWISE_API_URL") + "/v2/mandates/" + System.getenv("MANDATE_ID") + "/history/?limit=100"))
          .timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY")).GET().build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 200) throw new IOException(response.body());
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Net.Http;
  using System.Threading;
  using System.Threading.Tasks;

  class GetMandateHistoryRequest
  {
      static async Task Main()
      {
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Get, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/mandates/{Environment.GetEnvironmentVariable("MANDATE_ID")}/history/?limit=100");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
      }
  }
  ```

  ```bash cURL theme={null}
  http_status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request GET --get "$PAYWISE_API_URL/v2/mandates/$MANDATE_ID/history/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" --data-urlencode 'limit=100' \
    --output history-page.json --write-out '%{http_code}')"
  [ "$http_status" -eq 200 ] || exit 1
  ```
</CodeGroup>

Follow the response's exact `next` URL until it is `null`. Each row's
`source_mandate` identifies the main or subcase track. A main-mandate request
includes published entries for the main case and all valid subcases. A direct
subcase request includes that subcase only.

Each row is one published status, even when it has several attachments or
related resources. `related_resources` can contain references to a
`request_to_client`, `message`, `email`, or `single_mandate_statement`. Use
`expand=history.related_resources` when the documented expanded bodies are
needed and the credential can read them. Email references have `url: null`;
their optional expanded body supplies client-visible subject, date, and
direction metadata.

## Representative response

```http theme={null}
HTTP/1.1 200 OK
Content-Type: application/json

{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": "f0000000-0000-4000-8000-000000000001",
      "type": "status_update",
      "occurred_at": "2026-08-27T11:00:00Z",
      "updated_at": "2026-08-27T11:05:00Z",
      "title": "Collection started",
      "description": "The extrajudicial collection process has started.",
      "text": "The extrajudicial collection process has started.",
      "claim_type_code": "H01",
      "is_advance_request": false,
      "advance_type": null,
      "state": {
        "legal_stage": {
          "code": "extrajudicial",
          "label_key": "mandate.state.legal_stage.extrajudicial",
          "label": "Außergerichtlich"
        },
        "processing": {
          "code": "in_progress",
          "label_key": "mandate.state.processing.in_progress",
          "label": "Laufend"
        },
        "payment": {
          "code": "unpaid",
          "label_key": "mandate.state.payment.unpaid",
          "label": "Unbezahlt"
        }
      },
      "source_mandate": {
        "id": "50000000-0000-4000-8000-000000000001",
        "reference_number": "PW-2026-000042",
        "relation": "main_case"
      },
      "attachments": [
        {
          "id": "f1000000-0000-4000-8000-000000000001",
          "filename": "collection-started.pdf",
          "mime_type": "application/pdf",
          "download_url": "https://api-sandbox.paywise.de/v2/mandates/50000000-0000-4000-8000-000000000001/history/f0000000-0000-4000-8000-000000000001/attachments/f1000000-0000-4000-8000-000000000001/download/"
        }
      ],
      "related_resources": [
        {
          "type": "request_to_client",
          "id": "f2000000-0000-4000-8000-000000000001",
          "url": "https://api-sandbox.paywise.de/v2/mandates/50000000-0000-4000-8000-000000000001/requests-to-client/f2000000-0000-4000-8000-000000000001/"
        },
        {
          "type": "email",
          "id": "f3000000-0000-4000-8000-000000000001",
          "url": null
        }
      ]
    }
  ]
}
```

This complete response matches `MandateHistoryPage`.

Use `updated_since` for incremental reads. The cutoff is inclusive and rows
expose `updated_at`, so persist the greatest applied timestamp, overlap the
next sweep, and upsert entries by status `id`, keeping their newest version.
Only advance the saved timestamp after every page has been applied. An entry
can reappear when an attachment or linked resource changes. With a cutoff,
rows are ordered by `updated_at` and then `id`; without one, they are ordered
newest first by event time and then `id`. Follow the shared
[pagination and synchronization guidance](/api-docs/essentials/pagination-and-synchronization)
for overlap windows and recovery after an interrupted read.

## Runnable Python workflow

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

PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_API_KEY = "pw_sbx_your_api_key"
MANDATE_ID = "50000000-0000-4000-8000-000000000001"

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}"}
mandate_id = MANDATE_ID

mandate_response = requests.get(
    f"{base_url}/v2/mandates/{mandate_id}/", headers=headers, timeout=(5, 30)
)
if mandate_response.status_code != 200:
    mandate_response.raise_for_status()
    raise RuntimeError(f"Expected 200, received {mandate_response.status_code}")
mandate = mandate_response.json()
if mandate.get("id") != mandate_id:
    raise RuntimeError("Mandate response did not preserve the requested ID")

def read_all(initial_url):
    rows_by_id = {}
    next_url = initial_url
    params = {"limit": 100}
    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"]:
            rows_by_id[row["id"]] = row
        next_url = page.get("next")
        params = None
    return rows_by_id

history_by_id = read_all(f"{base_url}/v2/mandates/{mandate_id}/history/")
related_resource_ids = {
    resource["id"]
    for entry in history_by_id.values()
    for resource in entry.get("related_resources", [])
}
attachments = [
    attachment
    for entry in history_by_id.values()
    for attachment in entry.get("attachments", [])
]
attachment_ids = {attachment["id"] for attachment in attachments}
attachment_download_urls = sorted(
    attachment["download_url"] for attachment in attachments
    if attachment.get("download_url") is not None
)
WORKFLOW_RESULT = {
    "mandate_id": mandate["id"],
    "history_ids": sorted(history_by_id),
    "related_resource_ids": sorted(related_resource_ids),
    "attachment_ids": sorted(attachment_ids),
    "attachment_download_urls": attachment_download_urls,
}
```

The runnable iterator deliberately caps history at five pages. Set an
operational budget appropriate to your dataset, but always keep a finite cap,
cycle detection, and exact HTTPS-origin validation before sending credentials.
Attachment downloads are authenticated: retain the Bearer header when calling
one of the returned `download_url` values. A null `download_url` means no file
is available; keep the attachment metadata without attempting a download.

## Failure and recovery

* `403`: verify the selected company and contact paywise support if access
  is unexpectedly denied.
* `404`: verify the selected company and exact mandate UUID.
* `400 invalid_parameter` on a list: use the documented filters and values.
  Unknown `ordering` terms on the mandate, order, and statement lists are
  refused the same way; the retired `sort` and `direction` parameters are
  `400 unknown_parameter`.
* `429`: wait at least `Retry-After`, then resume from the exact last
  uncommitted `next` URL.
* Duplicate or out-of-order webhook signals: refetch the mandate and upsert
  rows by stable server `id`; never select the first list row or infer state
  from event order.

## Verify

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_API_KEY = "pw_sbx_your_api_key"
  MANDATE_ID = "50000000-0000-4000-8000-000000000001"

  response = requests.get(
      f"{PAYWISE_API_URL}/v2/mandates/{MANDATE_ID}/",
      headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"},
      timeout=(5, 30),
  )
  if response.status_code != 200:
      response.raise_for_status()
      raise RuntimeError(f"Expected 200, received {response.status_code}")
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/mandates/${process.env.MANDATE_ID}/`, {
    method: "GET", headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}` }, signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 200) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.time.Duration;

  class ReadMandateRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(System.getenv("PAYWISE_API_URL") + "/v2/mandates/" + System.getenv("MANDATE_ID") + "/"))
          .timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY")).GET().build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 200) throw new IOException(response.body());
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Net.Http;
  using System.Threading;
  using System.Threading.Tasks;

  class ReadMandateRequest
  {
      static async Task Main()
      {
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Get, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/mandates/{Environment.GetEnvironmentVariable("MANDATE_ID")}/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
      }
  }
  ```

  ```bash cURL theme={null}
  http_status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request GET "$PAYWISE_API_URL/v2/mandates/$MANDATE_ID/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" \
    --output mandate.json --write-out '%{http_code}')"
  [ "$http_status" -eq 200 ] || exit 1
  ```
</CodeGroup>

Require `.id` to equal the mandate UUID you follow and `.state` to carry
`legal_stage`, `processing`, and `payment` dimensions, each with a `code`.
Require every history row to have `updated_at`, `source_mandate`,
`related_resources`, and `attachments`. Treat blank state values in a history
entry as event context; use the mandate's current `state` for decisions.

## Related reference

* [GET `/v2/mandates/{id}/`](/api-docs/case-management-api/reference/mandates/get-mandate)
* [GET `/v2/mandates/{id}/history/`](/api-docs/case-management-api/reference/mandates/get-mandate-history)
* [GET `/v2/mandates/{id}/history/{entry_id}/attachments/{file_id}/download/`](/api-docs/case-management-api/reference/mandates/download-mandate-history-attachment)
* [GET `/v2/mandates/{id}/documents/`](/api-docs/case-management-api/reference/mandates/get-mandate-documents)
* [GET `/v2/statements/`](/api-docs/case-management-api/reference/statements/list-statements)


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