/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 atPOST /v2/claims/. Finalize the entire order with one bodyless command. - Submission is an explicit command, not a field write.
PATCH submission_state: releasedbecomesPOST /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.acceptedpayload name the mandate directly. - Writes are idempotent and errors are typed. Every logical
POSTcarries a stableIdempotency-Key, and failures arrive in one envelope with nested field paths, stable codes, andX-Paywise-Request-Id.
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, andmandate_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.
Conventions that apply to every step
Pagination. v2 list responses usecount, 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 withGET /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. ReadGET /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
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.
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_datehas 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 tobirth_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, andmobile_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, omitcommunication_channelsfrom the PATCH. If you intentionally replace that collection, inventoryfax,skype,facebook_messenger,imessage,whatsapp,facebook,twitter,linkedin,xing,social_various,website_url, andweb_variousfirst and route them for review; do not silently relabel a fax, social handle, or website as a supported type.
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 existingmetadata: [{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
/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 aPATCH:
migration-example=case-v1-claim-request
migration-example=case-v1-release-request
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.
Complete receivable field crosswalk v1 → v2
Complete receivable field crosswalk v1 → v2
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
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
Order representation:
migration-example=case-current-finalize-response
Reading a v1 submission_state as a v2 status
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: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
PATCH /v2/claims/{id}/. Titled claim metadata and
title attachments stay below the stable claim UUID:
migration-example=case-current-claim-title-request
/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 anorder.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-levelPOST /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 isdraft,submitted, orawaiting_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
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 withPOST /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:
Webhook event crosswalk v1 → v2
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.
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.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.
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.
Statement crosswalk v1 → v2
Statement crosswalk v1 → v2
/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.
Field crosswalk v1 → v2
Field crosswalk v1 → v2
/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
- Inventory v1 credentials, endpoints, jobs, webhooks, IDs, in-flight claims, and downstream consumers. Freeze undocumented dependencies.
- 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). - 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.
- Create and verify v2 sandbox webhooks, including duplicate and out-of-order delivery drills. Keep v2 events as change signals only.
- 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.
- 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.
- Expand cohorts only after submission, mandate reconciliation, payments, requests, documents, and statements pass the verification gates below.
- Verify the default
/v2/statements/history byclearing_noand 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
submittedor lateraccepted. - 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, andmandate_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 matchingclearing_no, totals, and (once transferred) PDF; none is represented as a/v2/statements/row.
