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

# Read debtor correspondence

> List the emails exchanged with the debtor on a case, read their text, download the PDF and the attachments.

## Outcome

Your application lists the debtor correspondence of an accepted mandate, reads
the text of each email, and stores its PDF rendering and attachments.

## Prerequisites

* A Bearer key with the `case:correspondence:read` scope. `*` covers it;
  `case:mandates:read` alone still lists the history but answers `403` on the
  correspondence routes.
* The exact mandate UUID — from the claim's current `mandate_id`, the
  `order.accepted` payload's `mandate_id`, or `mandate.created`.

The collection contains the correspondence the client may read: the same set
the paywise portal shows. A main-mandate request lists the emails of the main
case and all its subcases; a direct subcase request lists that subcase only.
Each row's `source_mandate` names the track it belongs to.

## 1. List correspondence

<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}/correspondence/",
      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}/correspondence/?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 ListMandateCorrespondenceRequest {
    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") + "/correspondence/?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 ListMandateCorrespondenceRequest
  {
      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")}/correspondence/?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/correspondence/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" --data-urlencode 'limit=100' \
    --output correspondence-page.json --write-out '%{http_code}')"
  [ "$http_status" -eq 200 ] || exit 1
  ```
</CodeGroup>

Follow the response's exact `next` URL until it is `null`. Narrow the list with
`channel` and `direction` (`inbound` or `outbound`). List rows carry the
envelope — `subject`, `occurred_at`, `direction`, the `email` addresses
(`from`, `to`, `cc`), `history_entry_ids`, and `pdf_url` — but no text and no
attachments; read the item for both.

## 2. Read one item

<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"
  CORRESPONDENCE_ID = "f3000000-0000-4000-8000-000000000001"

  response = requests.get(
      f"{PAYWISE_API_URL}/v2/mandates/{MANDATE_ID}/correspondence/{CORRESPONDENCE_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}/correspondence/${process.env.CORRESPONDENCE_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 GetMandateCorrespondenceItemRequest {
    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") + "/correspondence/" + System.getenv("CORRESPONDENCE_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 GetMandateCorrespondenceItemRequest
  {
      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")}/correspondence/{Environment.GetEnvironmentVariable("CORRESPONDENCE_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/correspondence/$CORRESPONDENCE_ID/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" --output correspondence-item.json --write-out '%{http_code}')"
  [ "$http_status" -eq 200 ] || exit 1
  ```
</CodeGroup>

* `channel` is an open enumeration. It is `email` today; treat it as a plain
  string and skip or store items with values your application does not know
  yet. `email` carries the addresses of an email item.
* `body` holds the visible text of the new message part in `text` and quoted
  earlier messages in `quoted_text`. The text is projected from the stored
  mail and is never HTML. `complete: false` means the text was cut at the size
  limit or the mail could only be projected partially; the PDF and the
  attachments remain the full record. `body` is `null` when no body is stored.
* `attachments[]` lists the files of the email. `id` is an opaque string, not a
  UUID. `status` is `available` or `unavailable` (open enumeration):
  `available` comes with a `download_url`; `unavailable` means the mail
  structure does not allow a download, so `download_url` is `null`. Keep the
  metadata of an unavailable attachment, and of any status your application
  does not know yet, without attempting a download. `size_bytes` is `null` when the mail does not declare it.
* `history_entry_ids` names the published history entries that surface this
  email. It is empty for an email reachable only through a client request.

## 3. Download the PDF and the attachments

<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"
  CORRESPONDENCE_ID = "f3000000-0000-4000-8000-000000000001"

  response = requests.get(
      f"{PAYWISE_API_URL}/v2/mandates/{MANDATE_ID}/correspondence/{CORRESPONDENCE_ID}/pdf/",
      headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"}, stream=True, timeout=(5, 30),
  )
  if response.status_code != 200:
      response.raise_for_status()
      raise RuntimeError(f"Expected 200, received {response.status_code}")
  with open("correspondence.pdf", "wb") as output:
      for chunk in response.iter_content(chunk_size=64 * 1024):
          if chunk:
              output.write(chunk)
  ```

  ```javascript JavaScript theme={null}
  import { createWriteStream } from "node:fs";
  import { Readable } from "node:stream";
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/mandates/${process.env.MANDATE_ID}/correspondence/${process.env.CORRESPONDENCE_ID}/pdf/`, {
    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()}`);
  await new Promise((resolve, reject) => Readable.fromWeb(response.body).pipe(createWriteStream("correspondence.pdf")).on("finish", resolve).on("error", reject));
  ```

  ```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.nio.file.AtomicMoveNotSupportedException;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.nio.file.StandardCopyOption;
  import java.time.Duration;

  class DownloadMandateCorrespondencePdfRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      String url = System.getenv("PAYWISE_API_URL") + "/v2/mandates/" + System.getenv("MANDATE_ID") + "/correspondence/" + System.getenv("CORRESPONDENCE_ID") + "/pdf/";
      Path destination = Path.of("correspondence.pdf").toAbsolutePath();
      Path temporary = Files.createTempFile(destination.getParent(), ".paywise-download-", ".tmp");
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY")).GET().build();
      try {
        HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(temporary));
        if (response.statusCode() != 200) throw new IOException("HTTP " + response.statusCode());
        try {
          Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
        } catch (AtomicMoveNotSupportedException unsupported) {
          Files.move(temporary, destination, StandardCopyOption.REPLACE_EXISTING);
        }
      } finally {
        Files.deleteIfExists(temporary);
      }
    }
  }
  ```

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

  class DownloadMandateCorrespondencePdfRequest
  {
      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")}/correspondence/{Environment.GetEnvironmentVariable("CORRESPONDENCE_ID")}/pdf/");
          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, HttpCompletionOption.ResponseHeadersRead, cancellation.Token);
          if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
          await using var output = File.Create("correspondence.pdf");
          await response.Content.CopyToAsync(output, cancellation.Token);
      }
  }
  ```

  ```bash cURL theme={null}
  temporary_file="$(mktemp ./correspondence.pdf.XXXXXX)"
  cleanup_download() {
    rm -f -- "$temporary_file"
  }
  trap cleanup_download EXIT HUP INT TERM
  http_status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request GET "$PAYWISE_API_URL/v2/mandates/$MANDATE_ID/correspondence/$CORRESPONDENCE_ID/pdf/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" --output "$temporary_file" --write-out '%{http_code}')"
  [ "$http_status" -eq 200 ] || exit 1
  mv -- "$temporary_file" correspondence.pdf
  trap - EXIT HUP INT TERM
  ```
</CodeGroup>

The PDF is rendered by paywise from the header block and the plain text; the
original mail HTML is never rendered. An item without a stored body has no PDF
and answers `404`.

<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"
  CORRESPONDENCE_ID = "f3000000-0000-4000-8000-000000000001"
  ATTACHMENT_ID = "logical_0123456789abcdef01234567"

  response = requests.get(
      f"{PAYWISE_API_URL}/v2/mandates/{MANDATE_ID}/correspondence/{CORRESPONDENCE_ID}/attachments/{ATTACHMENT_ID}/download/",
      headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"}, stream=True, timeout=(5, 30),
  )
  if response.status_code != 200:
      response.raise_for_status()
      raise RuntimeError(f"Expected 200, received {response.status_code}")
  with open("correspondence-attachment.bin", "wb") as output:
      for chunk in response.iter_content(chunk_size=64 * 1024):
          if chunk:
              output.write(chunk)
  ```

  ```javascript JavaScript theme={null}
  import { createWriteStream } from "node:fs";
  import { Readable } from "node:stream";
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/mandates/${process.env.MANDATE_ID}/correspondence/${process.env.CORRESPONDENCE_ID}/attachments/${process.env.ATTACHMENT_ID}/download/`, {
    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()}`);
  await new Promise((resolve, reject) => Readable.fromWeb(response.body).pipe(createWriteStream("correspondence-attachment.bin")).on("finish", resolve).on("error", reject));
  ```

  ```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.nio.file.AtomicMoveNotSupportedException;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.nio.file.StandardCopyOption;
  import java.time.Duration;

  class DownloadMandateCorrespondenceAttachmentRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      String url = System.getenv("PAYWISE_API_URL") + "/v2/mandates/" + System.getenv("MANDATE_ID") + "/correspondence/" + System.getenv("CORRESPONDENCE_ID") + "/attachments/" + System.getenv("ATTACHMENT_ID") + "/download/";
      Path destination = Path.of("correspondence-attachment.bin").toAbsolutePath();
      Path temporary = Files.createTempFile(destination.getParent(), ".paywise-download-", ".tmp");
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY")).GET().build();
      try {
        HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(temporary));
        if (response.statusCode() != 200) throw new IOException("HTTP " + response.statusCode());
        try {
          Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
        } catch (AtomicMoveNotSupportedException unsupported) {
          Files.move(temporary, destination, StandardCopyOption.REPLACE_EXISTING);
        }
      } finally {
        Files.deleteIfExists(temporary);
      }
    }
  }
  ```

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

  class DownloadMandateCorrespondenceAttachmentRequest
  {
      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")}/correspondence/{Environment.GetEnvironmentVariable("CORRESPONDENCE_ID")}/attachments/{Environment.GetEnvironmentVariable("ATTACHMENT_ID")}/download/");
          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, HttpCompletionOption.ResponseHeadersRead, cancellation.Token);
          if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
          await using var output = File.Create("correspondence-attachment.bin");
          await response.Content.CopyToAsync(output, cancellation.Token);
      }
  }
  ```

  ```bash cURL theme={null}
  temporary_file="$(mktemp ./correspondence-attachment.bin.XXXXXX)"
  cleanup_download() {
    rm -f -- "$temporary_file"
  }
  trap cleanup_download EXIT HUP INT TERM
  http_status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request GET "$PAYWISE_API_URL/v2/mandates/$MANDATE_ID/correspondence/$CORRESPONDENCE_ID/attachments/$ATTACHMENT_ID/download/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" --output "$temporary_file" --write-out '%{http_code}')"
  [ "$http_status" -eq 200 ] || exit 1
  mv -- "$temporary_file" correspondence-attachment.bin
  trap - EXIT HUP INT TERM
  ```
</CodeGroup>

Use the `download_url` of an `available` attachment, or build the route from
its opaque `id`. The response's `Content-Type` is detected from the bytes and
is never taken from the mail, so it can differ from the declared `mime_type`;
store the file by the detected type. Every download is sent with
`Content-Disposition: attachment` and a sanitised filename — never render an
attachment inline in a browser context. Unknown and `unavailable` attachments
answer `404`.

## Representative response

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

{
  "id": "f3000000-0000-4000-8000-000000000001",
  "url": "https://api-sandbox.paywise.de/v2/mandates/50000000-0000-4000-8000-000000000001/correspondence/f3000000-0000-4000-8000-000000000001/",
  "channel": "email",
  "direction": "inbound",
  "occurred_at": "2026-08-27T10:58:00Z",
  "updated_at": "2026-08-27T11:05:00Z",
  "subject": "Ratenzahlung",
  "source_mandate": {
    "id": "50000000-0000-4000-8000-000000000001",
    "reference_number": "PW-2026-000042",
    "relation": "main_case"
  },
  "history_entry_ids": [
    "f0000000-0000-4000-8000-000000000001"
  ],
  "pdf_url": "https://api-sandbox.paywise.de/v2/mandates/50000000-0000-4000-8000-000000000001/correspondence/f3000000-0000-4000-8000-000000000001/pdf/",
  "email": {
    "from": "debtor@example.test",
    "to": [
      "inkasso@paywise.de"
    ],
    "cc": []
  },
  "body": {
    "text": "Guten Tag, ich möchte in Raten zahlen.",
    "quoted_text": null,
    "complete": true
  },
  "attachments": [
    {
      "id": "logical_0123456789abcdef01234567",
      "filename": "kontoauszug.pdf",
      "mime_type": "application/pdf",
      "size_bytes": 48213,
      "status": "available",
      "download_url": "https://api-sandbox.paywise.de/v2/mandates/50000000-0000-4000-8000-000000000001/correspondence/f3000000-0000-4000-8000-000000000001/attachments/logical_0123456789abcdef01234567/download/"
    }
  ]
}
```

This complete response matches `CorrespondenceDetail`.

## Incremental reads

Use `updated_since` for incremental reads, exactly as for the
[case history](/api-docs/case-management-api/workflows/follow-a-case). The
cutoff is inclusive and rows expose `updated_at`, so persist the greatest
applied timestamp, overlap the next sweep, and upsert items by `id`, keeping
their newest version. An item becomes readable through a status or
client-request publication; that moment counts as an update, so
`updated_since` also returns newly readable items. With a cutoff, rows are ordered by `updated_at` and then
`id` and the `next` URL carries an opaque `cursor`; follow it exactly. Items
changed less than a minute ago are held back until they settle. Only advance
the saved timestamp after every page has been applied. Follow the shared
[pagination and synchronization guidance](/api-docs/essentials/pagination-and-synchronization)
for overlap windows and recovery after an interrupted read.

## History links and webhooks

A history entry's `related_resources[]` reference of type `email` links to the
correspondence item in `url`; the reference's `url` is `null` when paywise
keeps no stored copy of that email. With
`expand=history.related_resources`, the expanded email body also carries
`correspondence_url`.

Subscribe to `mandate.correspondence.published` to learn when an item first
becomes readable — through a published status or a client request. The event
is sent once per item; its `data` carries `company_id`, `mandate_id`,
`mandate_url`, `correspondence_id`, `correspondence_url`, and `channel`. Fetch
`correspondence_url` and upsert by `correspondence_id`. See
[Webhook events](/api-docs/case-management-api/concepts/webhook-events).

## 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)
headers = {"Authorization": f"Bearer {PAYWISE_API_KEY}"}
mandate_id = MANDATE_ID


def require_api_url(url):
    parts = urlsplit(url)
    origin = (
        parts.hostname.lower() if parts.hostname else "",
        parts.port or (443 if parts.scheme == "https" else None),
    )
    if (
        parts.scheme != "https"
        or origin != api_origin
        or parts.username is not None
        or parts.password is not None
    ):
        raise RuntimeError("Refusing a URL outside the API HTTPS origin")
    return url


list_response = requests.get(
    f"{base_url}/v2/mandates/{mandate_id}/correspondence/",
    headers=headers,
    params={"limit": 100},
    timeout=(5, 30),
)
if list_response.status_code != 200:
    list_response.raise_for_status()
    raise RuntimeError(f"Expected 200, received {list_response.status_code}")
rows = list_response.json()["results"]
correspondence_ids = [row["id"] for row in rows]
if not rows:
    raise RuntimeError("The mandate has no readable correspondence yet")

item_id = rows[0]["id"]
item_response = requests.get(
    f"{base_url}/v2/mandates/{mandate_id}/correspondence/{item_id}/",
    headers=headers,
    timeout=(5, 30),
)
if item_response.status_code != 200:
    item_response.raise_for_status()
    raise RuntimeError(f"Expected 200, received {item_response.status_code}")
item = item_response.json()
if item.get("id") != item_id:
    raise RuntimeError("Correspondence response did not preserve the requested ID")

attachment_ids = [attachment["id"] for attachment in item["attachments"]]
available = [
    attachment
    for attachment in item["attachments"]
    if attachment["status"] == "available" and attachment.get("download_url")
]
downloaded_bytes = 0
if available:
    download_response = requests.get(
        require_api_url(available[0]["download_url"]),
        headers=headers,
        stream=True,
        timeout=(5, 30),
    )
    if download_response.status_code != 200:
        download_response.raise_for_status()
        raise RuntimeError(f"Expected 200, received {download_response.status_code}")
    content = bytearray()
    for chunk in download_response.iter_content(chunk_size=64 * 1024):
        if chunk:
            content.extend(chunk)
    downloaded_bytes = len(content)

WORKFLOW_RESULT = {
    "mandate_id": mandate_id,
    "correspondence_ids": correspondence_ids,
    "attachment_ids": attachment_ids,
    "downloaded_bytes": downloaded_bytes,
}
```

The workflow reads only the first page; use the history workflow's bounded
`next` iterator for a full sweep. Attachment downloads are authenticated:
retain the Bearer header, and validate that a returned `download_url` stays on
the API HTTPS origin before sending it.

## Failure and recovery

* `403`: the key lacks `case:correspondence:read`. Grant the scope in the
  developer portal; `case:mandates:read` alone is not enough.
* `404`: verify the selected company, the exact mandate UUID, and the item ID.
  Unknown items and items of another company answer `404` alike. On the PDF
  route, `404` also means no body is stored; on the attachment route, the
  attachment is unknown or `unavailable`.
* `400 invalid_parameter` on the list: use the documented filters and values.
* `429`: wait at least `Retry-After`, then resume from the exact last
  uncommitted `next` URL.
* `503` on a download: the document storage was briefly unavailable. Retry
  after `Retry-After` seconds.

## Related reference

* [GET `/v2/mandates/{mandate_id}/correspondence/`](/api-docs/case-management-api/reference/mandates/list-mandate-correspondence)
* [GET `/v2/mandates/{mandate_id}/correspondence/{id}/`](/api-docs/case-management-api/reference/mandates/get-mandate-correspondence-item)
* [GET `/v2/mandates/{mandate_id}/correspondence/{id}/pdf/`](/api-docs/case-management-api/reference/mandates/download-mandate-correspondence-pdf)
* [GET `/v2/mandates/{mandate_id}/correspondence/{id}/attachments/{attachment_id}/download/`](/api-docs/case-management-api/reference/mandates/download-mandate-correspondence-attachment)
* [GET `/v2/mandates/{id}/history/`](/api-docs/case-management-api/reference/mandates/get-mandate-history)


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