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

# Migrate from Case Management API v1

> Use existing v1 records and UUIDs through stable v2 claim resources, preserve lifecycle state, and switch traffic without duplicate submissions.

This is a step-by-step port of a working `/v1/` integration to the Case
Management API at `/v2/`. Work through Part A to change your code, then use
Part B to move production traffic across one cohort at a time.

It is a breaking migration, not a base-path substitution. Keep the
[frozen v1 introduction](/api-docs/legacy/case-management/introduction) open
for old behaviour, and build the new flows from the current
[submit-order workflow](/api-docs/case-management-api/workflows/submit-an-order).

Use the documented trailing slash on every `/v2/` root and route. Slashless
requests return JSON `404` for every method, including `GET`, `HEAD`,
`OPTIONS`, and writes, with no redirect or `Location` header. Update request
builders to send the canonical URL directly; see
[canonical URLs](/api-docs/essentials/requests-and-responses#canonical-urls).

## Outcome

Your integration creates v2 orders, finalizes them, follows them to acceptance
into a mandate, and reports payments, documents, and webhook events against v2
resources. Existing debtors, claims, mandates, and payments are the same stored
records in both versions, with the same UUIDs. Continue their lifecycle through
v2; switching API versions requires no reimport or replacement submission.

## What actually changes

The API changes how you address and submit records. Four things change:

* **The order resource is the submission aggregate.** In v2 you create an order
  with complete claims inline at `POST /v2/orders/`, or add a complete claim to
  an existing draft at `POST /v2/claims/`. Finalize the entire order with one
  bodyless command.
* **Submission is an explicit command, not a field write.** `PATCH
  submission_state: released` becomes `POST /v2/orders/{id}/finalize/`.
* **Acceptance produces a mandate you are told about.** You no longer poll a
  claim for a mandate link; the accepted order and the `order.accepted` payload
  name the mandate directly.
* **Writes are idempotent and errors are typed.** Every logical `POST` carries
  a stable `Idempotency-Key`, and failures arrive in one envelope with nested
  field paths, stable codes, and `X-Paywise-Request-Id`.

The business records are shared across versions, but their wire fields and
commands still need the mappings below. A claim created through v1 keeps the
same UUID in v2. Read it directly once to observe its current `order_id`,
`status`, and `mandate_id`; never reconstruct the relationship by scanning
orders. Metadata and events already stored on the supported resources are
readable through v2 immediately; do not resubmit them to migrate.

One thing genuinely disappears: the v1 mandate archive `PATCH` has no public
v2 command. Per-case Aktenabrechnungen do **not** disappear — they continue at
`/v2/single-mandate-statements/` with every v1 field and its meaning intact;
only the wire mechanics follow v2 conventions. Step 8 covers both statement
resources.

## Prerequisites

* Credentials verified for v2 in the intended sandbox company. Record identity
  continuity does not establish credential compatibility or access.
* A durable **migration ledger** you can write to before every v2 command.
  A useful row holds the source system, the existing debtor/claim/mandate UUIDs,
  the immutable business reference, the target company UUID, the current
  observed `order_id`, `status`, and `mandate_id`, the command type, the idempotency key, the
  canonical body hash, the last known outcome, and a verification timestamp.
* An inventory of your v1 call sites, background jobs, webhook receivers, and
  in-flight claims. Freeze any undocumented dependency before you start.

<Warning>
  Do not assume an unchanged invoice number, `your_reference`, email address, or
  amount tuple is unique. Resolve every write from its schema-defined response
  UUID, or from an exact, fully traversed reconciliation with a singular-result
  guard. Keep `X-Paywise-Request-Id` for support correlation only — it is fresh
  on an idempotent replay and is never a resource ID.
</Warning>

## Conventions that apply to every step

**Pagination.** v2 list responses use `count`, exact `next` and `previous`
URLs, and a `results` array. Follow every exact `next` URL. Mutable offset
pagination has no snapshot: use inclusive `updated_since` where declared,
overlap successive scans, deduplicate by `(id, updated_at)`, repeat bounded
fresh sweeps where a single new object must be found, and run periodic full
reconciliation. Never select the first result by position, and never claim a
sweep is complete under concurrent writes.

**Errors.** Branch on the envelope's `code` and on nested `errors[].field` and
`errors[].code`. Authentication and access failures are `401` and `403`;
a wrong tenant or a hidden resource ID may be `404`; validation is normally
`400`; lifecycle and idempotency conflicts are `409`; `429` requires waiting at
least the integer `Retry-After` seconds.

**Idempotency.** Every logical v2 `POST` uses one stable `Idempotency-Key`. On
a timeout, `500`, or `503`, retry the exact method, path, selected company, and
body with the same key. A validation failure releases the key: correct the
request and retry the same logical command with the same key. Use a new key
only for a different logical command. See
[errors and safe retries](/api-docs/essentials/errors-and-safe-retries).

## Part A — change your code

### 1. Get and verify v2 credentials

Both contracts describe Bearer authentication, but a matching scheme and header
shape does **not** prove that a v1 credential is accepted by a v2 endpoint.
Inventory your credentials by environment and owner, then obtain or confirm
separately authorized v2 sandbox and production credentials through the
authorized portal or staff process. Never copy a production key into a sandbox
test. The public contracts expose no Case Management API key create, list, rotate, or
revoke endpoint.

Verify each candidate credential with `GET /v2/info/` and a safe scoped read.
Require the authenticated response's case-insensitively parsed
`X-Paywise-Environment` value to equal the intended environment. A missing
header is a failed verification. Record only credential metadata and a secret
manager reference, never the key itself.

**Company context.** A direct Case key selects its company implicitly. A
Partner key selects a company on every `/v2/` call with
`X-On-Behalf-Of-Company: <company-uuid>`, and must not send that header to
Partner management routes. Follow
[authentication and environments](/api-docs/essentials/authentication-and-environments)
for the full cutover checklist.

### 2. Repoint your debtor code

**Reuse the existing debtor UUID.** Read `GET /v2/debtors/{id}/` with the UUID
you already stored from v1, under the same company and environment. Do not
create another debtor merely to switch API versions. The record remains
reusable for later orders, subject to the current editing rules.

For a genuinely new debtor, port the creation payload as follows. Here is what
v1 sent:

```http migration-example=case-v1-debtor-request theme={null}
POST /v1/debtors/ HTTP/1.1
Authorization: Bearer <legacy-key>
Content-Type: application/json

{
  "your_reference": "CUSTOMER-1001",
  "person": {
    "salutation": "Herr",
    "first_name": "Alex",
    "last_name": "Example",
    "birth_date": "1986-08-24"
  },
  "acting_as": "consumer",
  "addresses": [{
    "street": "Example Street 12",
    "zip": "10115",
    "city": "Berlin",
    "country": "DE"
  }],
  "communication_channels": [],
  "bank_accounts": [],
  "metadata": [],
  "events": []
}
```

The equivalent creation payload in v2:

`addresses` accepts the debtor's complete address collection in this one
request (up to the published limit). With multiple addresses, set `primary`
on every entry and mark exactly one as `true`.

```http theme={null}
POST /v2/debtors/ HTTP/1.1
Authorization: Bearer <v2-key>
Idempotency-Key: 81000000-0000-4000-8000-000000000010
Content-Type: application/json

{
  "your_reference": "CUSTOMER-1001",
  "acting_as": "consumer",
  "person": {
    "salutation": "mr",
    "first_name": "Alex",
    "last_name": "Example",
    "birth_date": "1986-08-24"
  },
  "addresses": [{
    "street": "Example Street 12",
    "postal_code": "10115",
    "city": "Berlin",
    "country": "DE",
    "primary": true
  }],
  "communication_channels": [],
  "bank_accounts": [],
  "metadata": [],
  "events": []
}
```

Change list:

| v1 | v2 |
| - | - |
| `zip` | `postal_code`, plus an explicit `primary` flag on the address |
| German salutation labels (`Herr`) | Enum codes (`mr`) — translate the meaning, never substitute a neutral value |
| `POST /v1/debtors/{id}/addresses/`, `/bankaccounts/`, `/communicationchannels/` | `PATCH /v2/debtors/{id}/` with the **complete** owned array; a partial array deletes the rest |
| `metadata`, `events` on the create body | Retained as arrays with the v1 fields and resource-specific types; historical values are readable without reimport |
| No idempotency requirement | `Idempotency-Key` required on create |

#### Map consumer and business identities

v2 makes the debtor's business identity explicit. Load the catalog once from
GET `/v2/legal-forms/`, store its stable codes, and refresh it periodically.
The catalog group determines which identity object the debtor must carry:

| Business meaning | v2 identity |
| - | - |
| Consumer | `acting_as: "consumer"` with `person`; omit `legal_form` and `organization` |
| A business debtor with a person identity, such as a sole proprietor | `acting_as: "business"` with `person` and a matching `legal_form` when known; omit `organization` |
| A business debtor with an organization identity | `acting_as: "business"` with `organization` and a matching `legal_form` when known; omit `person` |

Do not choose the identity shape from the presence of a company name alone.
When the source business type resolves to a catalog code, follow that code's
group. When the legal form is unknown, keep `acting_as: "business"`, omit
`legal_form` or send `null` or `""`, and supply exactly one person or organization
identity. Consumer salutations may also be omitted, null, or an empty string;
business persons and legal representatives still require a supported salutation.

Two v1 inputs are intentionally narrower or absent on v2 writes:

* v1 `person.death_date` has no v2 write field. No current client integration
  supplies this field, and no current submission behavior depends on it. Stop
  serializing it to v2; retain it in the source system if required for audit.
  Do not map it to `birth_date`, metadata, or an event. If a future inventory
  finds a populated value, review that record before cutover.
* v1 accepted fifteen communication-channel types. New v2 writes accept only
  `email`, `phone`, and `mobile_phone`. Historical values remain part of the
  stored debtor; switching versions does not require rewriting them. To
  preserve the complete stored collection while changing another field, omit
  `communication_channels` from the PATCH. If you intentionally replace that
  collection, inventory `fax`, `skype`, `facebook_messenger`, `imessage`,
  `whatsapp`, `facebook`, `twitter`, `linkedin`, `xing`, `social_various`,
  `website_url`, and `web_various` first and route them for review; do not
  silently relabel a fax, social handle, or website as a supported type.

On debtor `PATCH`, omit `addresses` to leave the collection unchanged. If you
send `addresses`, that array replaces the collection; child `id` is optional,
so a replacement does not require copying existing IDs. Send an existing ID
only when you want to retain that exact address resource, and send `[]` to
clear all addresses.

Persist UUIDs returned for newly created debtors. For existing debtors, retain
the stored UUID and refresh the v2 projection. Reconcile by UUID and
`updated_at`; a matching reference is not proof of identity. A failed or hidden
read is a reason to check company, environment, and access, not to create a
replacement.

Then reference the record from an order exactly the way v1 referenced it from a
claim:

```json theme={null}
{
  "debtor_id": "40000000-0000-4000-8000-000000000001",
  "additional_debtor_ids": [],
  "starting_approach": "extrajudicial",
  "claims": [ /* … */ ]
}
```

<Info>
  An order write accepts debtor references only. Create or reuse the debtor
  first, then send its UUID in `debtor_id`; send each other debtor UUID in
  `additional_debtor_ids`. The order resource does not create or update debtor
  content.
</Info>

#### Preserve metadata and events

The v2 create contract accepts the existing `metadata: [{type, value}]` format
on debtors, claims, additional charges, and payments. Debtors, claims, and
additional charges also accept `events` with `type`, `title`, `occurence`, and
optional nullable `your_reference`, `description`, and `location`. Keep each
resource's allowed types and repeated metadata entries; do not convert the
array to a dictionary or rename `occurence`.

Historical values appear on the corresponding v2 reads without reimport.
These fields support creation and reading, not editing: explicit context
PATCHes are rejected, while unrelated PATCHes preserve the stored context.
Complete `additional_charges` replacement keeps its replacement semantics and
requires the desired context on each replacement entry. See
[Metadata and events](/api-docs/case-management-api/concepts/claims#metadata-and-events).
Historical read values can include types outside today's input enums, so a
read payload is not automatically valid replacement input.

### 3. Read existing claims directly and port new submissions

#### Continue an existing claim

A v1 claim keeps its UUID. Under the same company and environment, perform
exactly one direct claim read before claim work:

```http migration-example=case-current-claim-read theme={null}
GET /v2/claims/50000000-0000-4000-8000-000000000001/ HTTP/1.1
Authorization: Bearer <v2-key>
```

Treat this as GET `/v2/claims/{claim_id}/` in your implementation. Require the
response `id` to equal the stored v1 claim UUID, then store its returned
`status`, current observed `order_id`, and `mandate_id` in the migration
ledger. A hidden or missing read is a company, environment, credential, or
access problem; it is never permission to scan orders, recreate the claim, or
replay its metadata and events.

Before cohort assignment, a v1 claim in `created` state remains v1-owned.
Finish and release every existing v1 draft through v1 before assigning its cohort to v2.
After release, read the same claim UUID through v2 and continue from its
already-submitted state. An already released v1 claim must not be finalized again.
An order already
under review continues in that state, and an accepted case continues through
its existing mandate UUID. Do not recreate any of these records to make them
visible in v2.

#### Create a new receivable

This is the real breaking change. In v1 you created a standalone claim against
a debtor, then released it with a `PATCH`:

```http migration-example=case-v1-claim-request theme={null}
POST /v1/claims/ HTTP/1.1
Authorization: Bearer <legacy-key>
Content-Type: application/json

{
  "debtor": "10000000-0000-4000-8000-000000000001",
  "your_reference": "INV-2026-0042",
  "document_reference": "2026-0042",
  "subject_matter": "Website accessibility consulting in June 2026",
  "occurence_date": "2026-06-01",
  "document_date": "2026-06-05",
  "due_date": "2026-06-19",
  "reminder_date": "2026-07-01",
  "delay_date": "2026-07-09",
  "total_claim_amount": {"value": "100.00", "currency": "EUR"},
  "main_claim_amount": {"value": "100.00", "currency": "EUR"},
  "items": [],
  "additional_charges_amount": {"value": "0.00", "currency": "EUR"},
  "additional_charges": [],
  "payments": [],
  "starting_approach": "extrajudicial",
  "claim_disputed": false,
  "obligation_fulfilled": true,
  "metadata": [],
  "events": []
}
```

```http migration-example=case-v1-release-request theme={null}
PATCH /v1/claims/50000000-0000-4000-8000-000000000001/ HTTP/1.1
Authorization: Bearer <legacy-key>
Content-Type: application/json

{
  "submission_state": "released",
  "send_order_confirmation": true
}
```

In the v2 request the new claim sits inside an order aggregate. The following is a complete
`OrderCreateRequest`; deterministic UUIDs make the storage and correlation
boundaries explicit. Note the renames: `main_claim_amount` becomes
`principal_amount`, `claim_disputed` becomes `is_disputed`,
`obligation_fulfilled` moves up to the order as
`creditor_obligation_fulfilled`, the claim `due_date` remains the claim
`due_date`, and `reminder_date` becomes `reminders[].date`. The public v1
claim input has no separate reminder deadline, so omit `reminders[].due_date`;
only populate it when a real legacy or internal reminder-deadline value exists.
Never derive it from the claim due date, delay date, or an arbitrary offset.
`legal_basis` replaces `occurence_date` as the carrier of contract context.

<Accordion title="Complete receivable field crosswalk v1 → v2">
  | v1 claim input | v2 order/claim input | Migration rule |
  | - | - | - |
  | `debtor` | Order `debtor_id` | Keep the debtor UUID; the order owns the debtor reference |
  | `starting_approach` | Order `starting_approach` | Move the unchanged value to the order |
  | `obligation_fulfilled` | Order `creditor_obligation_fulfilled` | Move the business confirmation to the order |
  | `your_reference`, `document_reference`, `subject_matter`, `document_date`, `due_date`, `delay_date` | Same claim fields | Preserve the source values; apply v2 validation rather than manufacturing missing dates |
  | `occurence_date` | `legal_basis.contract_date` | Preserve the date and add the applicable `claim_type_code`; note the legacy spelling |
  | `main_claim_amount` | `principal_amount` | Preserve the original principal amount including VAT |
  | `claim_disputed` | `is_disputed` | Rename it; when `true`, also supply the debtor's non-blank `dispute_reason` |
  | `reminder_date` | `reminders[].date` | Create the one known reminder; do not invent `reminders[].due_date` |
  | `items[].description`, `items[].quantity`, `items[].unit` | Same item fields | Serialize `quantity` as a decimal string in v2 |
  | `items[].amount` | `items[].unit_price` | Rename the stored per-unit money value; items support the claim but do not replace `principal_amount` |
  | `additional_charges[].type`, `your_reference`, `subject_matter`, `amount`, `metadata`, `events` | Same charge fields | Preserve supported values and context |
  | `additional_charges[].occurence_date` | `additional_charges[].occurrence_date` | Preserve the date and correct the spelling only for this v2 charge field |
  | `additional_charges[].id` | — | Do not send a client-selected child ID when creating the v2 claim |
  | `additional_charges[].document_date`, `additional_charges[].due_date` | — | These dates are not v2 inputs and drive no current v2 charge behavior; retain them in the source audit record if needed |
  | `payments[]` | Claim `payments[]` during creation | Preserve `your_reference`, `amount`, `value_date`, and `metadata`; report receipts arriving later through the scoped payment command in step 6 |
  | `metadata`, `events` | Same claim arrays | Preserve repeated entries and the legacy event field spelling `occurence` |
  | `total_claim_amount`, `additional_charges_amount` | Response `total_amount`, `additional_charges_amount` | Do not send either aggregate. v2 calculates them from principal, charges, and payments |

  v2 accepts new additional charges only with type `reminder_fee`, `bank_charge`,
  or `research_costs`. The following v1 input types have no v2 write value:
  `processing_fee`, `cancellation_fee`, `convenience_fee`, `advisory_fee`,
  `handling_fee`, `insurance_fee`, `legal_fee`, `delivery_charge`,
  `service_charge`, `data_preparation`, `communication_cost`, and `expenses`.
  Do not coerce one into a supported type and do not silently drop it. Keep the
  source record and route any populated occurrence to a manual migration
  exception before cutover. Existing stored charges remain readable through v2
  without reimport; this restriction applies when constructing a new v2 write.
</Accordion>

```http migration-example=case-current-order-request theme={null}
POST /v2/orders/ HTTP/1.1
Authorization: Bearer <separately-authorized-current-key>
Idempotency-Key: 81000000-0000-4000-8000-000000000001
Content-Type: application/json

{
  "debtor_id": "40000000-0000-4000-8000-000000000001",
  "additional_debtor_ids": [],
  "starting_approach": "extrajudicial",
  "creditor_obligation_fulfilled": true,
  "confirmation_email": "collections@example.test",
  "claims": [{
    "type": "receivable",
    "your_reference": "INV-2026-0042",
    "document_reference": "2026-0042",
    "subject_matter": "Website accessibility consulting in June 2026",
    "principal_amount": {"value": "100.00", "currency": "EUR"},
    "document_date": "2026-06-05",
    "due_date": "2026-06-19",
    "is_disputed": false,
    "items": [],
    "additional_charges": [],
    "reminders": [{"date": "2026-07-01"}],
    "delay_date": "2026-07-09",
    "documents": [],
    "legal_basis": {
      "claim_type_code": "H05",
      "contract_date": "2026-06-01",
      "description": "Consulting agreement"
    }
  }]
}
```

Persist the returned order, debtor, and claim UUIDs before enqueueing finalize.
The claim UUID is the durable command key. Keep the returned `order_id` as
observed lifecycle context, not as part of any claim-specific URL.

For a new v2 submission, v1 `submission_state: released` maps to the order
finalize command. Finalize accepts an empty body or exactly `{}`; any member is
`400 validation_error` with `unknown_field`. This mapping does not authorize
replay: an already released v1 claim must not be finalized again.

```http migration-example=case-current-finalize-request theme={null}
POST /v2/orders/20000000-0000-4000-8000-000000000001/finalize/ HTTP/1.1
Authorization: Bearer <separately-authorized-current-key>
Idempotency-Key: 82000000-0000-4000-8000-000000000001
```

<Warning>
  The v1 release option `send_order_confirmation` is **not a finalize input** in
  v2. An empty finalize body and `{}` mean the same thing; neither carries that
  preference. The confirmation preference is draft data instead:
  set `confirmation_email` on the order — at creation or via an order `PATCH`
  before finalize — and finalize sends the order entry confirmation
  (Eingangsbestätigung) to that address. When `send_order_confirmation` was
  `true`, v1 resolved from the order contact; v2 does not infer a recipient from
  the credential or company. Inventory the address that v1 would have used and
  store that address in integration configuration before cutover, then send it
  explicitly as `confirmation_email`. Omit the field (or set it `null`) when the
  v1 flag was false or absent. The boolean alone cannot reconstruct the
  recipient. Do not translate the release `PATCH`
  into a v2 claim `PATCH`; a claim `PATCH` edits draft data and never submits
  anything.
</Warning>

A successful response is the complete `Order` representation:

```json migration-example=case-current-finalize-response theme={null}
{
  "type": "invoice",
  "mandate": null,
  "merged_into": null,
  "confirmation_email": "collections@example.test",
  "expires_at": null,
  "rejection": null,
  "id": "20000000-0000-4000-8000-000000000001",
  "status": "submitted",
  "your_reference": "INV-2026-0042",
  "starting_approach": "extrajudicial",
  "creditor_obligation_fulfilled": true,
  "debtor": {
    "id": "40000000-0000-4000-8000-000000000001",
    "your_reference": "CUSTOMER-1001",
    "acting_as": "consumer",
    "person": {
      "salutation": "mr",
      "first_name": "Alex",
      "last_name": "Example",
      "birth_date": "1986-08-24"
    },
    "organization": null,
    "legal_form": null,
    "metadata": [],
    "events": []
  },
  "additional_debtors": [],
  "claims": [{
    "id": "30000000-0000-4000-8000-000000000001",
    "order_id": "20000000-0000-4000-8000-000000000001",
    "status": "submitted",
    "mandate_id": null,
    "debtor_id": "40000000-0000-4000-8000-000000000001",
    "additional_debtor_ids": [],
    "payments": [],
    "type": "receivable",
    "your_reference": "INV-2026-0042",
    "document_reference": "2026-0042",
    "subject_matter": "Website accessibility consulting in June 2026",
    "is_disputed": false,
    "dispute_reason": null,
    "principal_amount": {"value": "100.00", "currency": "EUR"},
    "items": [],
    "additional_charges": [],
    "additional_charges_amount": {"value": "0.00", "currency": "EUR"},
    "total_amount": {"value": "100.00", "currency": "EUR"},
    "document_date": "2026-06-05",
    "due_date": "2026-06-19",
    "reminders": [{
      "id": "60000000-0000-4000-8000-000000000001",
      "date": "2026-07-01",
      "due_date": null
    }],
    "delay_date": "2026-07-09",
    "legal_basis": {
      "claim_type_code": "H05",
      "contract_date": "2026-06-01",
      "description": "Consulting agreement"
    },
    "documents": [],
    "metadata": [],
    "events": [],
    "created_at": "2026-08-27T10:00:00Z",
    "updated_at": "2026-08-27T10:05:00Z"
  }],
  "totals": {
    "order_value": {"value": "100.00", "currency": "EUR"},
    "main_claims": {"value": "100.00", "currency": "EUR"},
    "charges": {"value": "0.00", "currency": "EUR"},
    "payments": {"value": "0.00", "currency": "EUR"}
  },
  "created_at": "2026-08-27T10:00:00Z",
  "updated_at": "2026-08-27T10:05:00Z"
}
```

<Accordion title="Reading a v1 submission_state as a v2 status">
  Treat state conversion as a business interpretation, never as an update sent
  across APIs. A v1 `created` claim corresponds operationally to an editable v2
  `draft`; v1 `released` and `under_review` correspond to `submitted`; v1
  `client_response_pending` corresponds to `awaiting_client_response`; terminal
  acceptance or rejection correspond to `accepted` or `rejected`.

  v1 reported the reason for a rejection as three fields on the claim; v2 groups
  them into one nullable `rejection` object on the order, present only while
  `status` is `rejected`:

  | v1 (claim) | v2 (order) |
  | - | - |
  | `rejection_reason` | `rejection.reason` |
  | `rejected_at` | `rejection.rejected_at` |
  | `rejection_notified_to` | `rejection.notified_to` |
  | — | `rejection.code`: stable key of the standard reason, `null` for an individually worded rejection |

  The `order.rejected` webhook stays thin (order and company UUIDs only), so
  read the order for the reason instead of expecting it in the payload.

  Read the existing order's v2 status and continue from it. Do not write a status
  translation or create a second order for already released work. Review the
  [core resources](/api-docs/case-management-api/concepts/resource-model) and
  [order lifecycle](/api-docs/case-management-api/concepts/orders#lifecycle)
  before changing storage or queue semantics.
</Accordion>

### 4. Move claim operations to stable top-level URLs

`POST /v1/claims/{id}/documents/` becomes
`POST /v2/claims/{claim_id}/documents/`. The claim UUID is the only resource
identifier in this command:

```http migration-example=case-current-claim-document-request theme={null}
POST /v2/claims/50000000-0000-4000-8000-000000000001/documents/ HTTP/1.1
Authorization: Bearer <v2-key>
Idempotency-Key: 83000000-0000-4000-8000-000000000001
Content-Type: application/json

{
  "type": "invoice",
  "filename": "invoice-2026-0042.pdf",
  "base64": "JVBERi0xLjQK"
}
```

Claim edits likewise use `PATCH /v2/claims/{id}/`. Titled claim metadata and
title attachments stay below the stable claim UUID:

```http migration-example=case-current-claim-title-request theme={null}
PUT /v2/claims/50000000-0000-4000-8000-000000000001/enforceable-title/ HTTP/1.1
Authorization: Bearer <v2-key>
If-Match: "claim-version"
Content-Type: application/json

{
  "title_type": "enforcement_order",
  "issuing_authority": "Amtsgericht Berlin",
  "file_number": "12-3456789-0-1",
  "issued_on": "2026-01-15",
  "legally_binding_since": "2026-02-01",
  "served_on": "2026-01-20",
  "amount": {"value": "2500.00", "currency": "EUR"}
}
```

Use `/v2/claims/{claim_id}/enforceable-title/documents/…` for title documents;
no claim operation accepts an order ID.

Ingestion and virus scanning are asynchronous. Do not treat the upload response
as a finished document: follow the document status until it is terminal before
any downstream handoff. Deletion of a claim is still limited to a claim inside
an editable `draft` order. Follow
[manage documents](/api-docs/case-management-api/workflows/manage-documents).

### 5. Follow the case to acceptance

Match the stored claim UUID in an `order.accepted` event's `claim_ids`, then
read that claim directly. Require its status to be `accepted` and its
`mandate_id` to equal the event's `mandate_id`; accept the read claim's current
`order_id` as authoritative. The submitted order ID is informational after
review. Do not reconstruct merge chains or follow `merged_into` for claim
commands, and never select a mandate by list position.

The follow-up surface retains existing mandate UUIDs, with these route changes:

| v1 | v2 |
| - | - |
| `GET /v1/mandates/`, `/v1/mandates/{id}/` | Same routes under `/v2/` with the existing mandate UUID. Mandate detail includes claims, while documents, payments, statements, and requests remain separate resources |
| `GET /v1/mandates/{id}/statusupdates/` | `GET /v2/mandates/{id}/history/`; each published status is one entry with its attachments and `related_resources` |
| `GET /v1/statusupdates/` (cross-mandate search, added to v1 on 2026-08-28) | No top-level v2 equivalent. Page through `GET /v2/mandates/`, then read `GET /v2/mandates/{id}/history/` for each followed case, or consume `GET /v2/events/` for incremental change detection |
| `GET /v1/mandates/{mandate_id}/requests-to-client/…` | Same parent-scoped routes under `/v2/` |
| `POST …/answer/`, `POST …/answer/documents/` | Same commands; validate the advertised answer type, issue one idempotent answer, and wait for terminal scanning status on uploads |
| `PATCH /v1/mandates/{id}/` (archive) | **No public v2 command.** Do not emulate one with local state changes |

The v2 contract intentionally has no company-wide history search. Preserve
each mandate UUID, page through the mandate collection when rebuilding a
projection, and read each case through
`GET /v2/mandates/{id}/history/?updated_since=`. For continuous synchronization,
consume `GET /v2/events/` and refresh the affected mandate rather than polling
all histories.

Refresh mandate projections from the paginated v2 collection and reconcile
them by their existing UUIDs. See
[follow a case](/api-docs/case-management-api/workflows/follow-a-case) and
[messages and client requests](/api-docs/case-management-api/workflows/messages-and-client-requests).

### 6. Repoint payments to scoped commands

v1 had one top-level `POST /v1/payments/` that named its target in the body.
v2 has two scoped commands, and the URL supplies the target:

* Before acceptance, report against the claim with
  `POST /v2/claims/{claim_id}/payments/`. While the order is `draft`,
  `submitted`, or `awaiting_client_response`, the receipt remains attributed
  to that claim and is visible to the review team.
* After acceptance, report against the exact mandate or subcase with
  `POST /v2/mandates/{mandate_id}/payments/`.

```http migration-example=case-current-claim-payment-request theme={null}
POST /v2/claims/50000000-0000-4000-8000-000000000001/payments/ HTTP/1.1
Authorization: Bearer <v2-key>
Idempotency-Key: 84000000-0000-4000-8000-000000000001
Content-Type: application/json

{
  "your_reference": "BANK-TRANSACTION-99812",
  "amount": {"value": "50.00", "currency": "EUR"},
  "value_date": "2026-07-17"
}
```

The accepted-claim route remains supported for compatibility: it delegates to
accepted-case reporting, preserves exact claim attribution, and returns `201`.
Do not use that technical compatibility as the migration routing rule. Choose
exactly one command per bank transaction with one stable idempotency key and
never issue both commands for it.
`GET /v2/payments/` remains as a read-only collection with target filters.
Retain the returned payment UUID; business references are correlation data,
not uniqueness keys.

Existing v1 payment reports are readable through v2 with their existing UUIDs,
including stored `metadata`. Do not report a receipt again because you changed
API versions. New payment creates accept the same resource-specific metadata
array; see [Payment metadata](/api-docs/case-management-api/concepts/payments#payment-metadata).

Claim payment routes need `case:payments:write` (`case:payments:read` to
list), even while the order is a draft; a key created with order permissions
only is `403 permission_denied` there. A `409 duplicate_payment_report` is not
a dead target: the same payment is already reported on that case, so reconcile
with `existing_payment_id`. A `409 conflict` on the claim command means the
target is dead — the claim was rejected or cancelled, or the mandate's
processing has ended. Do not retry against another target; move the payment to
a manual exception queue with its correlation data for finance and support
reconciliation, and never fall back to v1 or a mandate guessed from a list.

See [report payments](/api-docs/case-management-api/workflows/report-payments).

### 7. Recreate webhook subscriptions

Existing subscriptions remain visible, but their payload and signature
contract does not change when you read them through v2. **Do not point a v1
secret at the v2 verifier.** With a direct Case credential, create a new
company-owned v2 subscription with `POST /v2/webhooks/`, store its
new UUID and one-time secret separately, and run the old and new receivers in
parallel during observation.

Partner integrations create Partner-owned subscriptions through
`POST /partner/v2/webhooks/` with a `companies` selector; Partner credentials
cannot write company-owned subscriptions. See
[webhook ownership](/api-docs/partner-api/concepts/webhook-ownership).

Port the v1 subscription settings deliberately; do not replay its JSON:

| v1 setting | v2 setting |
| - | - |
| `url` | `url`; use a public HTTPS receiver and keep the v1 and v2 paths or verifier configuration separate during observation |
| `enabled` | `enabled`; create disabled first if your deployment process needs the verifier in place before delivery |
| `events` | `events`; translate every selected key with the crosswalk below. On PATCH, `events` is a complete replacement array, not an incremental add/remove operation |
| `access_mode` | `access_mode` has no v2 request field; the API host and credential select the environment. Create the sandbox subscription with the sandbox host and key, and the production subscription with the production host and key |
| `description` | `description`; preserve the operator-facing label |
| `max_consecutive_failures` | Same name, now an integer from 1 through 1000; review a legacy value outside that range instead of clipping it silently |

<Accordion title="Webhook event crosswalk v1 → v2">
  v2 events are narrower change signals. A “no direct equivalent” row means
  there is no safe catch-all rename: select the named business signals your
  consumer needs and refetch the referenced v2 resource.

  | v1 event | v2 subscription and business handling |
  | - | - |
  | `mandate.created` | `mandate.created`; refetch the mandate |
  | `mandate.updated` | No direct equivalent. Subscribe to the applicable precise mandate events — `mandate.state.changed`, `mandate.status_update.published`, `mandate.balance_updated`, and `mandate.message.created` — then refetch |
  | `mandate.status_updated` | `mandate.status_update.published`; read the mandate history entry and its attachments |
  | `mandate.closed` | `mandate.state.changed`; refetch and evaluate the current processing state |
  | `mandate.balance_updated` | Unchanged; refetch the mandate's legal balance |
  | `mandate.message_created` | `mandate.message.created`; refetch the referenced message or mandate |
  | `claim.created` | Use `order.submitted` for a new submission and `order.accepted` or `order.rejected` for its review result; preserve claim UUIDs from the order and refetch |
  | `claim.updated` | No generic v2 claim-updated event. Use the relevant order lifecycle signal and direct claim read; do not treat event arrival as the new state |
  | `payment.created` | `payment.reported` for a client-reported payment. Also use `mandate.balance_updated` when the consumer needs the resulting legal balance |
  | `payment.updated` | No generic v2 payment-updated event. Use `mandate.balance_updated` for booked-payment or cost effects and refetch the payment or mandate |
  | `statement.created` | `statement.published`; refetch the statement |
  | `statement.updated` | `statement.cancelled` for a reversal. There is no generic update event; refetch on published or cancelled signals |
  | `single_mandate_statement.created` | `single_mandate_statement.published`; refetch the Aktenabrechnung |
  | `single_mandate_statement.updated` | `single_mandate_statement.cancelled` for a reversal. There is no generic update event |
  | `request_to_client.created` | Unchanged; refetch the request below its mandate |
  | `request_to_client.answered` | Unchanged; refetch the request below its mandate |

  The six Mahnservice keys keep their names when they were selected on a v1
  subscription: `invoice.created`, `invoice.paid`, `invoice.cancelled`,
  `invoice.written_off`, `dunning.level_advanced`, and
  `dunning.handed_to_collection`. They describe the separate Mahnservice
  product; include them only when the receiver already handles those events.
</Accordion>

Every webhook read exposes a read-only `contract_version` field: subscriptions
created through this API are always `v2`, while `v1` marks a legacy
subscription that still receives v1 payloads and signatures. Use it to find
leftover v1 subscriptions after cutover — `GET /v2/webhooks/` lists both — and
remove them with `DELETE /v2/webhooks/{id}/`. Legacy subscriptions also appear
with a **Legacy v1** badge in the developer portal's webhook list, where they
can be deleted as well.

Create and bodyless rotate expose `secret_key` once through their documented
one-time-secret response schema; reads never reveal it. `PUT /v1/webhooks/{id}/`
becomes `PATCH /v2/webhooks/{id}/`, and partial update still requires complete
owned event arrays. Test and manual redelivery return `202 Accepted` with
required `detail`, `delivery_id`, and `event_id`; redelivery creates a new
delivery ID while preserving the original event ID. Never use
`X-Paywise-Request-Id` as a delivery correlation ID.

Deliveries are at least once and unordered. Verify the exact raw bytes,
deduplicate by event UUID, acknowledge only after durable receipt, and refetch
authoritative state instead of applying arrival order.

<Warning>
  `GET /v1/webhooks/{id}/deliveries/` has no nested replacement. Read the v2
  top-level delivery list instead and narrow it to one subscription with the
  declared `webhook` query parameter (your stored v2 subscription UUID),
  alongside `event_type`, `status`, `updated_since`, `limit`, and `offset`.
  Follow every exact `next` URL for the complete collection and deduplicate by
  delivery UUID. Never declare the inventory complete after one page.
</Warning>

Follow the [Case webhook workflow](/api-docs/case-management-api/workflows/consume-webhooks).

### 8. Move statements and Aktenabrechnungen

`/v1/statements/` Sammelabrechnungen map to `/v2/statements/` with every v1
capability available: the case rows are embedded in the detail as `mandates`
and remain pageable and filterable at `GET /v2/statements/{id}/mandates/`,
each row keeps `related_statements[]`, the four downloads are listed in
`files[]`, and `comment` and `mandate_count` are on list and detail.
Reconcile their case rows and totals explicitly.

<Accordion title="Statement crosswalk v1 → v2">
  | v1 | v2 |
  | - | - |
  | `href` / `mandate_details_href` | Dropped; address the statement by `id`, page its rows at `GET /v2/statements/{id}/mandates/` |
  | `canceled` boolean | `status` (`published` / `cancelled`) plus `cancelled_at` |
  | `overview_vat_specific`, top-level balances | Grouped under `financials` (`vat_entries`, `total_balance`, `balance_before_outstanding_items_offsetting`, `outstanding_items_offset`) |
  | `downloads[]` with `full_pdf`, `third_party_money_xlsx`, `cost_burden_xlsx`, `closing_xlsx` and per-type download paths | `files[]` with `type` `full-pdf`, `third-party-money-xlsx`, `cost-burden-xlsx`, `closing-xlsx`; one proxy `GET /v2/statements/{id}/download/?type=…` (`full-pdf` default). Each descriptor has `type`, `filename`, `media_type`, and `download_url`; there is no singular `file`, byte `size`, or deterministic per-file `id` — key on `type` |
  | `GET /v1/statements/{id}/mandate-details/` with `reference_number`, `your_reference`, `document_reference` | `GET /v2/statements/{id}/mandates/` with the same three partial-match filters; rows are also embedded as `mandates` in the detail |
  | The row's embedded `mandate` | Each case row maps `mandate.legal_stage`, `mandate.processing_state`, and `mandate.payment_state` to `state.legal_stage`, `state.processing`, and `state.payment`; its `debtor` is an idless `MandateDebtor` case snapshot, not a reusable debtor resource |
  | `mandate_details[].related_statements[]` with `href`s | `mandates[].related_statements[]` as `{id, clearing_no, booking_date, period_start, period_end}`; only published statements are listed |
  | `comment`, `mandate_count` | Unchanged names and meaning on list and detail |
  | Exact `clearing_no`, `invoice_no`, `mandate_reference_number`, `booking_date`, `period_start`, `period_end` | Exact `clearing_no`, `invoice_no`, `mandate_reference_number`; dates through `booking_date_from/to`, `period_start_from/to`, `period_end_from/to`; `q` for partial searches |
  | `created` | `created_at` / `updated_at` |
  | Unknown query parameters ignored | Undeclared parameters are rejected with `400` |
</Accordion>

`/v1/single-mandate-statements/` Aktenabrechnungen map to
`/v2/single-mandate-statements/`. The resource keeps every v1 field and its
meaning — `statement_type`, the case references (`reference_number`,
`your_reference`), per-case principal claims and VAT allocation
(`vat_entries[].vat_rate` stays a decimal fraction, `0.19`), `cost_burden`,
`payout_method`, `pre_tax_deductible`, and the per-case PDF — and it serves
the complete history of your published Aktenabrechnungen, so there is no
archive-before-retirement obligation. What changes is wire mechanics only:

The standard multi-case statement is the default. New Aktenabrechnungen are
produced only for customers configured for single-mandate statements.
Historical published and cancelled single-mandate statements remain readable
even when that configuration is no longer active, so preserve their UUIDs and
continue to reconcile that existing history.

<Accordion title="Field crosswalk v1 → v2">
  | v1 | v2 |
  | - | - |
  | `href` hyperlink identity | Dropped; address the resource by `id` |
  | `canceled` boolean | `status` (`published` / `cancelled`) plus `cancelled_at` |
  | Embedded `mandate` summary | Mandate reference `{id, reference_number}`; still `null` when the case link was removed — `reference_number` on the statement is the durable snapshot |
  | Flat top-level money fields | Grouped under `financials` (`principal_claims_total`, `open_principal_claim`, `total_balance`, `principal_claims`, `vat_entries`, `cost_burden`) with unchanged field names and semantics inside |
  | `downloads[]` descriptor array | One nullable `file` object (`filename`, `media_type`, `download_url`); no byte `size` is reported. The PDF stays an authenticated proxy at `GET /v2/single-mandate-statements/{id}/download/`. A `null` file is a pending PDF, not a missing statement |
  | `created` | `created_at` / `updated_at` |
  | Unknown query parameters ignored | Undeclared parameters are rejected with `400` |
</Accordion>

An Aktenabrechnung still materializes exactly one case, while a v2 statement
settles a whole clearing run; the two collections remain distinct resources
with distinct shapes — never fold Aktenabrechnungen into `/v2/statements/`.

For change signals, subscribe to `single_mandate_statement.published` — the v2
name for the release event v1 delivered as `single_mandate_statement.created`
— and `single_mandate_statement.cancelled` for a reversal (Storno). v2
deliveries carry the thin reference envelope
(`single_mandate_statement_id`, `single_mandate_statement_url`); refetch the
resource for content.

## Part B — cut over

### Run v1 and v2 without double submission

Create a routing ledger keyed by your immutable business command ID, not by an
API-side uniqueness assumption. Select one dispatch path for each logical
command while both versions operate on the same records:

| Existing state at cutover | Write owner |
| - | - |
| v1 claim `created`, not released | Keep the cohort on v1, finish the draft, and release it through v1 before cohort assignment. Do not cut over this state |
| v1 claim released, under review, pending response, accepted, or rejected | Read the same claim UUID directly and continue supported follow-up in v2 from its current state; do not finalize it again |
| New receivable after its cohort switch | v2 order flow only |
| Existing v1 mandate, payment, or statement | Refresh its v2 representation by the existing UUID; use the appropriate current follow-up command |
| New payment while its v2 order is a draft | Choose the draft claim target once and store the returned UUID |
| New payment while its order is submitted or awaiting a response | Report once through the existing claim-scoped payment route; acceptance is not a prerequisite |
| New payment for an accepted v2 mandate | Choose that exact mandate once and store the returned UUID |

Before dispatch, atomically claim the business command for one surface. Store
the request body and key before sending. After any ambiguous outcome, pause
that record, query the known UUID or perform exact reconciliation, and only
then decide whether the original same-key retry is needed. **Never fall through
to v1 because a v2 request timed out.**

A cohort is v2-only once assigned. New drafts created after assignment are v2-owned and are finalized through v2.

### Staged rollout

1. Inventory v1 credentials, endpoints, jobs, webhooks, IDs, in-flight
   claims, and downstream consumers. Freeze undocumented dependencies.
2. Obtain or confirm v2 sandbox credentials and verify them. Transform golden
   debtor/claim/payment/statement fixtures and run the
   [quickstart](/api-docs/case-management-api/quickstart) against the sandbox
   host (`https://api-sandbox.paywise.de`).
3. Add dual-read comparison and the migration ledger. Do not dual-write.
   Compare lifecycle meaning, amounts, optional resource projections, and
   pagination convergence rather than raw JSON equality.
4. Create and verify v2 sandbox webhooks, including duplicate and out-of-order
   delivery drills. Keep v2 events as change signals only.
5. Drain the proposed cohort to zero pending v1 commands and zero unsubmitted v1 drafts.
   Finish and release every existing v1 draft through v1 before assigning its cohort to v2.
   Verify each released claim through its stable v2 UUID without finalizing it again.
6. Assign one production canary cohort. From that assignment onward, route
   every new draft and command for the cohort only to v2; do not recreate
   existing records or reset their lifecycle.
7. Expand cohorts only after submission, mandate reconciliation, payments,
   requests, documents, and statements pass the verification gates below.
8. Verify the default `/v2/statements/` history by `clearing_no` and totals.
   Verify `/v2/single-mandate-statements/` as well only when the customer is
   configured for it or has historical records there. After every cohort is
   assigned, retire v1 credentials and subscriptions through the authorized
   process.

### Verification gates

* Each canary business command has exactly one write owner, one canonical body
  hash, and at most one server-side order/claim/payment UUID set.
* Created v2 orders retain the expected company, debtor, claim, amounts, and
  reference; finalized orders are `submitted` or later `accepted`.
* Existing debtor, claim, mandate, and payment UUIDs resolve to the same
  records. Historical metadata/events are readable, and each direct claim
  read records its current `order_id`, `status`, and `mandate_id`.
* Acceptance reconciliation proves the exact claim UUID and reference on one
  mandate detail; it never selects a list position.
* Pagination interruption and overlap drills converge without treating a sweep
  as a snapshot.
* Typed error paths reach the correct field-level remediation and the request
  ID is retained without becoming a business ID.
* Duplicate and reordered webhook drills apply each business effect once and
  finish by refetching v2 state.
* Claim-before-acceptance and mandate-after-acceptance payments reconcile to
  exactly one target. The standard statement collection reproduces the
  accounting totals required by every consumer; single-mandate parity is a
  gate only for configured customers or customers with historical records.
* Each payment received during review is reported once to its exact claim,
  reconciled after an ambiguous outcome, or visible in a manual exception queue.
* Every v1 Aktenabrechnung UUID resolves on `/v2/single-mandate-statements/{id}/`
  with matching `clearing_no`, totals, and (once transferred) PDF; none is
  represented as a `/v2/statements/` row.


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