Skip to main content
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 open for old behaviour, and build the new flows from the current submit-order workflow. 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.

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

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.

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 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:
migration-example=case-v1-debtor-request
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.
Change list:

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

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. 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:
migration-example=case-current-claim-read
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:
migration-example=case-v1-claim-request
migration-example=case-v1-release-request
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.
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.
migration-example=case-current-order-request
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.
migration-example=case-current-finalize-request
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.
A successful response is the complete Order representation:
migration-example=case-current-finalize-response
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: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 and order lifecycle before changing storage or queue semantics.

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:
migration-example=case-current-claim-document-request
Claim edits likewise use PATCH /v2/claims/{id}/. Titled claim metadata and title attachments stay below the stable claim UUID:
migration-example=case-current-claim-title-request
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.

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: 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 and 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/.
migration-example=case-current-claim-payment-request
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. 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.

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. Port the v1 subscription settings deliberately; do not replay its JSON:
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.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.
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.
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.
Follow the Case webhook workflow.

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.
/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.
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: 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 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.