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

# Mahnservice quickstart

> Create a debtor, submit an invoice, release it into dunning, and read the dunning state.

This quickstart is designed for the sandbox and uses a company-bound Bearer
key. The Python examples use `https://api-sandbox.paywise.de` directly; replace
the inline `PAYWISE_API_KEY` placeholder with a sandbox key (`pw_sbx_…`)
created in the developer portal. Other language tabs read the matching
environment variables.

The request tabs below are complete, copyable examples for individual
operations. The final section runs the whole flow end to end once in Python.

There is no `Idempotency-Key` header on this API: submitting an invoice is
idempotent on its `invoice_number` (per company and environment), so a repeat
submission returns `200` with the existing invoice instead of creating a
duplicate.

## Before you start: save a sandbox Mahnlauf

Mahnservice configuration is sandbox-native and is not copied from production.
Company profile, the entering user's membership, company-level notification
recipients, and that user's own notification recipients are mirrored; a
synthetic sandbox entitlement is granted independently of a production purchase.
Enter the sandbox from the production portal under *Für Entwickler*
→ *Zur Sandbox*, then open *Mahnservice* → *Erste Schritte* → *Mahnlauf einrichten*.
Configure the flow, proceed to step 4 (*Speichern*), and choose
**Mahnlauf speichern**. The sandbox saves a default flow without checkout,
a paid subscription or a SEPA mandate. Bookkeeping integration is unnecessary;
this API supplies the invoices.

Saving creates the default configuration; activation is a separate step.
Choose *Weiter* to return to *Erste Schritte*, then **Mahnlauf prüfen und
aktivieren** when the page shows *Alles bereit!*. This starts the sandbox
ladder; outbound notices are captured in the *E-Mail-Log* instead of delivered.
Until you activate the flow in the portal, API release creates a `pending`
dunning process. `GET /mahnservice/v1/dunning-flows/` lets you inspect the
configuration. If release reports `409 no_default_dunning_flow`, complete the
wizard in the same sandbox company as your key.

In production, the wizard also saves the configuration before opening
*Weiter zur Zahlung*. Abandoning checkout keeps the flow so checkout can be
resumed through *Abo abschließen* on *Erste Schritte*, but a payment method and an active subscription are still required
for production use. Sandbox setup does not change production billing.

## 1. Find or create the debtor

Creating a debtor is not idempotent — a repeat call creates a second debtor
row. Keep the returned `id` in your own customer record. If it is missing,
look up your customer before creating another:

```bash cURL lookup theme={null}
curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
  --get "$PAYWISE_API_URL/mahnservice/v1/debtors/" \
  --header "Authorization: Bearer $PAYWISE_API_KEY" \
  --data-urlencode "customer_number=C-1042"
```

`customer_number` is an exact, case-sensitive match; `email` is an exact,
case-insensitive match against the debtor's displayed email. If you supply
both, they are combined with **AND**. Results stay scoped to your company and
credential's data lane and use normal `count`, `next` and `results` pagination.
Omit an unused filter; blank or whitespace-only values, customer numbers over
50 characters and malformed email values return `400` with the field name.
Surrounding whitespace is trimmed from `customer_number` on lookup exactly as
it is on create, so a padded lookup still finds the row you created.

Reuse a matching debtor only after checking its identity. These fields are
not unique: multiple matches need reconciliation, and a lookup followed by a
create cannot prevent two concurrent workers creating duplicates. Serialize
creation per customer in your integration and store the resulting id. The
create examples below apply when there is no existing match.

```python Python theme={null}
import requests

PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_API_KEY = "pw_sbx_your_api_key"

debtor_payload = {
    "name": "Alex Example",
    "customer_number": "C-1042",
    "type": "person",
    "email": "alex@example.test",
    "address": {
        "street": "Example Street 12", "zip_code": "10115",
        "city": "Berlin", "country": "DE",
    },
}
response = requests.post(
    f"{PAYWISE_API_URL}/mahnservice/v1/debtors/",
    headers={
        "Authorization": f"Bearer {PAYWISE_API_KEY}",
        "Content-Type": "application/json",
    },
    json=debtor_payload,
    timeout=(5, 30),
)
response.raise_for_status()
```

```javascript JavaScript theme={null}
const debtorPayload = {
  name: "Alex Example",
  customer_number: "C-1042",
  type: "person",
  email: "alex@example.test",
  address: { street: "Example Street 12", zip_code: "10115", city: "Berlin", country: "DE" },
};
const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/mahnservice/v1/debtors/`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PAYWISE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(debtorPayload),
  signal: AbortSignal.timeout(30000),
});
if (!response.ok) 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 QuickstartRequest {
  public static void main(String[] args) throws IOException, InterruptedException {
    String json = "{\"name\":\"Alex Example\",\"customer_number\":\"C-1042\",\"type\":\"person\","
        + "\"email\":\"alex@example.test\",\"address\":{"
        + "\"street\":\"Example Street 12\",\"zip_code\":\"10115\","
        + "\"city\":\"Berlin\",\"country\":\"DE\"}}";
    HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(System.getenv("PAYWISE_API_URL") + "/mahnservice/v1/debtors/"))
        .timeout(Duration.ofSeconds(30))
        .header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json)).build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() >= 400) throw new IOException(response.body());
  }
}
```

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

class QuickstartRequest
{
    static async Task Main()
    {
        using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
        var payload = new {
            name = "Alex Example",
            customer_number = "C-1042",
            type = "person",
            email = "alex@example.test",
            address = new { street = "Example Street 12", zip_code = "10115", city = "Berlin", country = "DE" }
        };
        using var request = new HttpRequestMessage(HttpMethod.Post,
            $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/mahnservice/v1/debtors/");
        request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
        request.Content = JsonContent.Create(payload);
        using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        using var response = await client.SendAsync(request, cancellation.Token);
        response.EnsureSuccessStatusCode();
    }
}
```

```bash cURL theme={null}
curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
  --request POST "$PAYWISE_API_URL/mahnservice/v1/debtors/" \
  --header "Authorization: Bearer $PAYWISE_API_KEY" \
  --header "Content-Type: application/json" \
  --data @- <<JSON
{
  "name": "Alex Example",
  "customer_number": "C-1042",
  "type": "person",
  "email": "alex@example.test",
  "address": {
    "street": "Example Street 12",
    "zip_code": "10115",
    "city": "Berlin",
    "country": "DE"
  }
}
JSON
```

The create response returns the debtor's `id` directly — read it from the
`201` body.

## 2. Submit the invoice

The invoice is created **held**: recorded and visible, but not dunned, and no
dunning process exists yet. Use a fresh `invoice_number` for a new claim; a
repeat submission of the same number returns `200` with the existing invoice
and ignores every changed field. To fix wrong data while the invoice is held,
use `PATCH /mahnservice/v1/invoices/{uuid}/` instead of inventing a second
number.

```python Python theme={null}
import requests

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

invoice_payload = {
    "invoice_number": "RE-2026-0001",
    "amount": "125.50",
    "currency": "EUR",
    "due_date": "2026-07-14",
    "document_date": "2026-06-30",
    "your_reference": "quickstart-invoice-1",
    "debtor": DEBTOR_ID,
}
response = requests.post(
    f"{PAYWISE_API_URL}/mahnservice/v1/invoices/",
    headers={
        "Authorization": f"Bearer {PAYWISE_API_KEY}",
        "Content-Type": "application/json",
    },
    json=invoice_payload,
    timeout=(5, 30),
)
response.raise_for_status()
```

```javascript JavaScript theme={null}
const invoicePayload = {
  invoice_number: "RE-2026-0001",
  amount: "125.50",
  currency: "EUR",
  due_date: "2026-07-14",
  document_date: "2026-06-30",
  your_reference: "quickstart-invoice-1",
  debtor: process.env.DEBTOR_ID,
};
const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/mahnservice/v1/invoices/`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PAYWISE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(invoicePayload),
  signal: AbortSignal.timeout(30000),
});
if (!response.ok) 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 QuickstartRequest {
  static String jsonString(String value) {
    if (value == null) throw new IllegalArgumentException("Missing environment value");
    StringBuilder escaped = new StringBuilder("\"");
    for (int index = 0; index < value.length(); index++) {
      char character = value.charAt(index);
      switch (character) {
        case '\"': escaped.append("\\\""); break;
        case '\\': escaped.append("\\\\"); break;
        case '\b': escaped.append("\\b"); break;
        case '\f': escaped.append("\\f"); break;
        case '\n': escaped.append("\\n"); break;
        case '\r': escaped.append("\\r"); break;
        case '\t': escaped.append("\\t"); break;
        default:
          if (character < 0x20) escaped.append(String.format("\\u%04x", (int) character));
          else escaped.append(character);
      }
    }
    return escaped.append('\"').toString();
  }

  public static void main(String[] args) throws IOException, InterruptedException {
    String json = "{\"invoice_number\":\"RE-2026-0001\",\"amount\":\"125.50\","
        + "\"currency\":\"EUR\",\"due_date\":\"2026-07-14\","
        + "\"document_date\":\"2026-06-30\",\"your_reference\":\"quickstart-invoice-1\","
        + "\"debtor\":" + jsonString(System.getenv("DEBTOR_ID")) + "}";
    HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(System.getenv("PAYWISE_API_URL") + "/mahnservice/v1/invoices/"))
        .timeout(Duration.ofSeconds(30))
        .header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json)).build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() >= 400) throw new IOException(response.body());
  }
}
```

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

class QuickstartRequest
{
    static async Task Main()
    {
        using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
        var payload = new {
            invoice_number = "RE-2026-0001",
            amount = "125.50",
            currency = "EUR",
            due_date = "2026-07-14",
            document_date = "2026-06-30",
            your_reference = "quickstart-invoice-1",
            debtor = Environment.GetEnvironmentVariable("DEBTOR_ID")
        };
        using var request = new HttpRequestMessage(HttpMethod.Post,
            $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/mahnservice/v1/invoices/");
        request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
        request.Content = JsonContent.Create(payload);
        using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        using var response = await client.SendAsync(request, cancellation.Token);
        response.EnsureSuccessStatusCode();
    }
}
```

```bash cURL theme={null}
curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
  --request POST "$PAYWISE_API_URL/mahnservice/v1/invoices/" \
  --header "Authorization: Bearer $PAYWISE_API_KEY" \
  --header "Content-Type: application/json" \
  --data @- <<JSON
{
  "invoice_number": "RE-2026-0001",
  "amount": "125.50",
  "currency": "EUR",
  "due_date": "2026-07-14",
  "document_date": "2026-06-30",
  "your_reference": "quickstart-invoice-1",
  "debtor": "$DEBTOR_ID"
}
JSON
```

`201` means a new invoice was created; `200` means this `invoice_number` was
already submitted and the existing invoice was returned unchanged. Read the
invoice's `id` from the body — `dunning_state` is `held`.

## 3. Attach the invoice PDF and wait for scanning

If you want the original PDF enclosed with the notices, upload it while the
invoice is held. The [invoice documents guide](https://docs.paywise.de/api-docs/mahnservice-api/concepts/documents.md)
shows the multipart request and the response shape. `POST .../documents/`
returns `201` with a document id and `status: "pending"`; it does not return
an invoice. Poll `GET .../documents/{document_uuid}/` until `ready` before
releasing. Stop on `rejected` or `failed`, and upload a corrected replacement
while held. Release returns `409 document_not_ready` while an attached
document is not ready. An invoice without an attachment can also be released.

The complete Python example below includes the upload and a bounded poll.

## 4. Release the invoice for dunning

Release declares no request body. It creates the dunning process and is the
moment paywise starts writing to the debtor. The process starts `active` when
the company's Mahnlauf is activated, `pending` until then. Release is
idempotent — a second call returns the current state.

```python Python theme={null}
import requests

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

response = requests.post(
    f"{PAYWISE_API_URL}/mahnservice/v1/invoices/{INVOICE_ID}/release/",
    headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"},
    timeout=(5, 30),
)
response.raise_for_status()
```

```javascript JavaScript theme={null}
const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/mahnservice/v1/invoices/${process.env.INVOICE_ID}/release/`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}` },
  signal: AbortSignal.timeout(30000),
});
if (!response.ok) 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 QuickstartRequest {
  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") + "/mahnservice/v1/invoices/" + System.getenv("INVOICE_ID") + "/release/"))
        .timeout(Duration.ofSeconds(30))
        .header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY"))
        .POST(HttpRequest.BodyPublishers.noBody()).build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() >= 400) throw new IOException(response.body());
  }
}
```

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

class QuickstartRequest
{
    static async Task Main()
    {
        using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
        using var request = new HttpRequestMessage(HttpMethod.Post,
            $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/mahnservice/v1/invoices/{Environment.GetEnvironmentVariable("INVOICE_ID")}/release/");
        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);
        response.EnsureSuccessStatusCode();
    }
}
```

```bash cURL theme={null}
curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
  --request POST "$PAYWISE_API_URL/mahnservice/v1/invoices/$INVOICE_ID/release/" \
  --header "Authorization: Bearer $PAYWISE_API_KEY"
```

## 5. Read the dunning state

The dunning projection shows the state, the current level, the next action
date, the configured flow, and the history of sent notices with their fees.

```python Python theme={null}
import requests

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

response = requests.get(
    f"{PAYWISE_API_URL}/mahnservice/v1/invoices/{INVOICE_ID}/dunning/",
    headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"},
    timeout=(5, 30),
)
response.raise_for_status()
```

```javascript JavaScript theme={null}
const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/mahnservice/v1/invoices/${process.env.INVOICE_ID}/dunning/`, {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}` },
  signal: AbortSignal.timeout(30000),
});
if (!response.ok) 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 QuickstartRequest {
  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") + "/mahnservice/v1/invoices/" + System.getenv("INVOICE_ID") + "/dunning/"))
        .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() >= 400) throw new IOException(response.body());
  }
}
```

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

class QuickstartRequest
{
    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")}/mahnservice/v1/invoices/{Environment.GetEnvironmentVariable("INVOICE_ID")}/dunning/");
        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);
        response.EnsureSuccessStatusCode();
    }
}
```

```bash cURL theme={null}
curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
  --request GET "$PAYWISE_API_URL/mahnservice/v1/invoices/$INVOICE_ID/dunning/" \
  --header "Authorization: Bearer $PAYWISE_API_KEY"
```

From here, report payments as they arrive
(`POST /mahnservice/v1/invoices/{uuid}/payments/`), or end the claim with
`cancel` or `write-off`. When the debtor never pays, the dunning ladder ends
in the handover to collection (`dunning_state: "inkasso"`,
`dunning.handed_to_collection`). That handover needs no bookkeeping
integration: paywise builds the collection case from the invoice, debtor,
reported payments, and PDF you submitted here.

## 6. Runnable end-to-end workflow

Install `requests`, replace the inline `PAYWISE_API_KEY` placeholder, and
set `INVOICE_PDF_PATH` to a real local PDF. Configure the sandbox Mahnlauf
first as described above. The program reuses a customer by `customer_number`,
uploads the PDF, and stops on a failed scan or after at most 60 polls. Each
run uses a fresh invoice number.

```python Python workflow theme={null}
from pathlib import Path
import time
import uuid

import requests

PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_API_KEY = "pw_sbx_your_api_key"
INVOICE_PDF_PATH = "invoice.pdf"
CUSTOMER_NUMBER = "quickstart-alex-example"

TIMEOUT = (5, 30)


def main():
    base_url = PAYWISE_API_URL
    session = requests.Session()
    session.headers.update({
        "Authorization": f"Bearer {PAYWISE_API_KEY}",
        "Accept": "application/json",
    })

    response = session.get(f"{base_url}/mahnservice/v1/info/", timeout=TIMEOUT)
    response.raise_for_status()
    if response.json()["environment"] != "sandbox":
        raise RuntimeError("This rehearsal requires the sandbox environment")

    run_id = str(uuid.uuid4())

    debtor_payload = {
        "name": "Alex Example",
        "customer_number": CUSTOMER_NUMBER,
        "type": "person",
        "email": "alex@example.test",
        "address": {
            "street": "Example Street 12", "zip_code": "10115",
            "city": "Berlin", "country": "DE",
        },
    }
    response = session.get(
        f"{base_url}/mahnservice/v1/debtors/",
        params={"customer_number": CUSTOMER_NUMBER},
        timeout=TIMEOUT,
    )
    response.raise_for_status()
    matches = response.json()
    if matches["count"] > 1:
        raise RuntimeError("Multiple debtors match; reconcile before continuing")
    if matches["count"] == 1:
        debtor_id = matches["results"][0]["id"]
    else:
        response = session.post(
            f"{base_url}/mahnservice/v1/debtors/",
            json=debtor_payload,
            timeout=TIMEOUT,
        )
        response.raise_for_status()
        debtor_id = response.json()["id"]

    invoice_payload = {
        "invoice_number": f"RE-{run_id}",
        "amount": "125.50",
        "currency": "EUR",
        "due_date": "2026-07-14",
        "document_date": "2026-06-30",
        "your_reference": f"quickstart-invoice-{run_id}",
        "debtor": debtor_id,
    }
    response = session.post(
        f"{base_url}/mahnservice/v1/invoices/",
        headers={"Content-Type": "application/json"},
        json=invoice_payload,
        timeout=TIMEOUT,
    )
    response.raise_for_status()
    invoice = response.json()
    invoice_id = invoice["id"]
    if invoice["dunning_state"] != "held":
        raise RuntimeError(f"Expected held, invoice is {invoice['dunning_state']}")

    pdf_path = Path(INVOICE_PDF_PATH)
    with pdf_path.open("rb") as pdf:
        response = session.post(
            f"{base_url}/mahnservice/v1/invoices/{invoice_id}/documents/",
            files={"file": (pdf_path.name, pdf, "application/pdf")},
            timeout=TIMEOUT,
        )
    response.raise_for_status()
    document = response.json()
    document_id = document["id"]
    for _ in range(60):
        if document["status"] == "ready":
            break
        if document["status"] in {"rejected", "failed"}:
            raise RuntimeError(f"Document scan ended: {document['status']}")
        if document["status"] != "pending":
            raise RuntimeError(f"Unknown document status: {document['status']}")
        time.sleep(2)
        response = session.get(
            f"{base_url}/mahnservice/v1/invoices/{invoice_id}/documents/{document_id}/",
            timeout=TIMEOUT,
        )
        response.raise_for_status()
        document = response.json()
    if document["status"] != "ready":
        raise RuntimeError(f"Document is {document['status']}; invoice remains held")

    # Release creates the dunning process; this is the moment paywise starts
    # writing to the debtor. Idempotent — a repeat returns the current state.
    response = session.post(
        f"{base_url}/mahnservice/v1/invoices/{invoice_id}/release/",
        timeout=TIMEOUT,
    )
    response.raise_for_status()

    response = session.get(
        f"{base_url}/mahnservice/v1/invoices/{invoice_id}/dunning/",
        timeout=TIMEOUT,
    )
    response.raise_for_status()
    dunning = response.json()

    return {
        "debtor_id": debtor_id,
        "invoice_id": invoice_id,
        "dunning_state": dunning.get("state"),
    }


WORKFLOW_RESULT = main()
```

## Related reference

* [GET `/mahnservice/v1/debtors/`](https://docs.paywise.de/api-docs/mahnservice-api/reference/debtors/list-debtors)
* [POST `/mahnservice/v1/debtors/`](https://docs.paywise.de/api-docs/mahnservice-api/reference/debtors/create-a-debtor)
* [POST `/mahnservice/v1/invoices/`](https://docs.paywise.de/api-docs/mahnservice-api/reference/invoices/submit-an-invoice)
* [PATCH `/mahnservice/v1/invoices/{uuid}/`](https://docs.paywise.de/api-docs/mahnservice-api/reference/invoices/correct-a-held-invoice)
* [POST `/mahnservice/v1/invoices/{uuid}/release/`](https://docs.paywise.de/api-docs/mahnservice-api/reference/invoices/release-a-held-invoice-for-dunning)
* [GET `/mahnservice/v1/invoices/{uuid}/dunning/`](https://docs.paywise.de/api-docs/mahnservice-api/reference/invoices/get-the-dunning-state)
* [POST `/mahnservice/v1/invoices/{uuid}/payments/`](https://docs.paywise.de/api-docs/mahnservice-api/reference/invoices/report-a-payment)


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