> ## 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 Partner API v1

> Reuse existing Partner company UUIDs, resolve company-scoped memberships, and move delegated Case traffic to v2 without recreating records.

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

Use the [frozen Partner introduction](/api-docs/legacy/partner/introduction)
only to inventory old behaviour; build the new flows from the current
[Partner overview](/api-docs/partner-api/introduction) and
[resource model](/api-docs/partner-api/concepts/resources-and-memberships).

Use the documented trailing slash on every `/partner/v2/` root and route,
and on delegated `/v2/` requests. 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 canonical URLs directly;
see [canonical URLs](/api-docs/essentials/requests-and-responses#canonical-urls).

## Outcome

Your integration onboards companies through `/partner/v2/`, manages people as
memberships below their company, reads readiness and delegated access as two
separate answers, and submits Case work with a company header instead of a user
header. No legal company exists twice, and no person-company intent is
processed twice during cutover. Companies already managed through v1 are the
same records in v2 and retain their company UUIDs; they need no import or
recreation.

## What actually changes

The company record and its UUID are shared across versions. Its legal identity,
address, tax treatment, VAT number, default claim type, payout bank account, notification
channels, and legal representatives are the same concepts you already send —
mostly renamed or moved to enum codes. Four things change:

* **A user is no longer a top-level resource.** Global `/partner/v1/users/` and
  `/partner/v1/userinvites/` collapse into one nested membership below a
  company: `POST /partner/v2/companies/{company_id}/users/`. There is no global
  user listing and no separate onboarded-user lookup.
* **The tenant selector changed.** `X-User-Id` is removed. Every delegated
  `/v2/` Case call selects the company with
  `X-On-Behalf-Of-Company: <company-uuid>`.
* **Onboarding readiness split into two questions.** v1 treated
  `data_submission_completed` as the single gate. v2 answers
  `case_submission_readiness` (is the company's data valid?) and `case_access`
  (may the Partner act right now?) independently.
* **Writes are idempotent and errors are typed.** Every logical `POST` carries
  a stable `Idempotency-Key` with exact replay semantics.

One convenience is gone: `POST /partner/v1/userinvites/` returned a
redirect URL for the v1 web flow. The v2 membership command sends setup mail
and returns the fresh invitation once as `setup_url`; this is not a v1-style
hosted-onboarding redirect. Hosted onboarding is a separately arranged
paywise flow, not an invite-API replacement.

## Prerequisites

* Partner credentials verified for v2 in the intended sandbox environment.
  Shared company identity does not establish credential compatibility or access.
* A **migration ledger**. For each legal company, retain its existing company
  UUID, the authoritative external business ID, environment,
  migration state, readiness and access snapshots, and a verification time. For
  each person-company relationship, retain the v1 user or invite ID separately
  from the verified v2 membership UUID, where a corresponding membership exists.
* An inventory of v1 companies, users, invites, `X-User-Id` call sites,
  credentials, hosted redirects, background jobs, and Case write
  routing, including every in-flight onboarding state.

<Warning>
  **Reuse the company UUID you already hold.** Read the same company through
  `GET /partner/v2/companies/{id}/` before dispatching a mutation. If it is hidden
  or inaccessible, check environment and Partner access; a failed read is not a
  reason to create another company. Never use an email,
  company name, customer number, or list position as an API identity or a
  uniqueness proof.
</Warning>

## Conventions that apply to every step

**Pagination.** Company and membership lists are paginated. Follow each exact
`next` URL. Mutable offset pagination has no snapshot: use inclusive
`updated_since` where declared, overlap scans, reconcile by `(id, updated_at)`,
and schedule full scans. Never choose the first result by position, infer
uniqueness from a search term, or advance a high-water mark before the whole
sweep is durable.

**Errors.** v2 uses a typed envelope and `X-Paywise-Request-Id`. Branch on
stable codes and field paths. Treat `400` as input or context validation, `401`
as a credential/host/environment mismatch, `403` as an access or
entitlement failure, `404` as a wrong or intentionally hidden company or
resource context, and `409` as a lifecycle, identity-lock, last-admin, access,
or idempotency conflict. After `429`, wait at least the integer `Retry-After`
value.

**Idempotency.** Use a fresh stable `Idempotency-Key` for each logical v2
`POST`: company create, membership add/resend/cancel, webhook management, and
delegated Case commands. Store the key, canonical body hash, company UUID,
operation, and outcome before dispatch. Retry an ambiguous timeout or transient
`5xx` with the exact same path, body, context, and key. A replay has a fresh
request ID but the same stored business response; a changed body or operation
needs a new logical key.

## Part A — change your code

### 1. Get v2 credentials and delete `X-User-Id`

Both contracts describe HTTP Bearer schemes. That proves only the header shape;
it does **not** prove that a v1 credential is accepted by v2 Partner or Case
endpoints. Obtain or confirm separately authorized v2 sandbox and production
Partner credentials through the authorized portal or staff process. Never copy
or test a production secret in sandbox. The public contracts expose no operation
to create, list, mint, rotate, or revoke Partner or per-company API keys, and
there is no public per-company Partner secret or token lifecycle endpoint.

Verify a candidate key with `GET /partner/v2/info/` and a safe scoped company
read. Require the authenticated response's case-insensitively parsed
`X-Paywise-Environment` header to equal the intended environment; a missing
header fails verification. Store only secret-manager references and credential
metadata.

Partner management calls use the Partner key directly and must **not** send a
company-selection header:

```http theme={null}
GET /partner/v2/companies/10000000-0000-4000-8000-000000000001/ HTTP/1.1
Authorization: Bearer <separately-authorized-current-partner-key>
```

Delegated Case calls use that same key plus the exact company UUID on **every**
`/v2/` request:

```http theme={null}
GET /v2/orders/?limit=10 HTTP/1.1
Authorization: Bearer <separately-authorized-current-partner-key>
X-On-Behalf-Of-Company: 10000000-0000-4000-8000-000000000001
```

<Warning>
  Delete all `X-User-Id` generation and validation from the new call path. Do not
  replace its value with a membership UUID — the new header takes the **company**
  UUID. Store the company UUID and the membership UUID independently; only the
  company header selects a tenant.
</Warning>

#### Migrate the info-response parser

Frozen `GET /partner/v1/info/` returns a JSON array. Its schema sets no minimum
or maximum cardinality, so a parser must handle zero, one, or multiple entries
without selecting position zero. Each entry may expose the token owner's email,
first name, and last name as `user`, `user_first_name`, and `user_last_name`:

```json migration-example=partner-v1-info-response theme={null}
[
  {
    "id": "legacy-partner-token-17",
    "token_name": "Onboarding integration",
    "user": "owner@example.test",
    "user_first_name": "Taylor",
    "user_last_name": "Example"
  }
]
```

`GET /partner/v2/info/` returns one non-PII object, not an array or a paginated
envelope. It removes the token-owner fields and adds the authenticated
`environment` and owning `partner` projection. The response also retains the
credential's server-issued access metadata:

```json migration-example=partner-current-info-response theme={null}
{
  "id": "14000000-0000-4000-8000-000000000001",
  "token_name": "Onboarding integration",
  "scopes": ["*"],
  "capabilities": [],
  "environment": "sandbox",
  "partner": {
    "id": "11000000-0000-4000-8000-000000000001",
    "name": "Example Platform"
  }
}
```

Replace the collection parser with a direct-object parser, validate the required
keys, and compare body `environment` with the case-insensitively parsed
`X-Paywise-Environment` response header. Remove owner email and name columns
from the new token-info projection, and purge or restrict the legacy PII
according to retention policy. A missing or null token or partner identifier
must fail any verification step that requires identity, even though the schema
permits nullable projections.

Follow [authentication and environments](/api-docs/essentials/authentication-and-environments)
and [calling the Case Management API](/api-docs/partner-api/concepts/calling-the-case-api).

### 2. Reuse existing companies and port new company creation

An existing v1 company is immediately addressable through v2 with its existing
UUID, subject to current access. Refresh its company details, readiness,
access, and memberships through v2. Keep its onboarding progress and existing
cases; do not call company create or send onboarding invitations merely to
switch API versions.

For a new company, port the creation flow below. v1 created a company, then
attached people with separate top-level commands:

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

{
  "name": "Example Manufacturing GmbH",
  "phone": "+49301234567",
  "vat_number": "DE129273398",
  "address": {
    "street": "Example Avenue 5",
    "zip": "10115",
    "city": "Berlin",
    "country": "DE"
  },
  "users": [],
  "default_claim_type": "H11",
  "legal_form": "GmbH",
  "legal_representatives": [],
  "tax_deduction_eligibility": "J",
  "notification_channels": [],
  "iban": "DE89370400440532013000"
}
```

The v2 command commits the company, up to ten memberships, provisional
authorization, events, and its idempotent response together. The same business
data, renamed:

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

{
  "name": "Example Manufacturing GmbH",
  "onboarding_mode": "api_only",
  "legal_form": "gmbh",
  "address": {
    "street": "Example Avenue 5",
    "postal_code": "10115",
    "city": "Berlin",
    "country": "DE"
  },
  "phone": "+49301234567",
  "tax_treatment": "input_tax_deductible",
  "vat_number": "DE129273398",
  "default_claim_type": "H11",
  "payout_bank_account": {
    "account_holder": "Example Manufacturing GmbH",
    "iban": "DE89370400440532013000",
    "bic": null
  },
  "notification_channels": [{
    "type": "email",
    "value": "operations@example.test",
    "notifications": ["requests_to_client", "status_updates", "statements"]
  }],
  "legal_representatives": [{
    "type": "managing_director",
    "name": "Taylor Example"
  }],
  "data_sharing_basis": "self_authorized",
  "users": [{
    "email": "admin@example.test",
    "first_name": "Taylor",
    "last_name": "Example",
    "role": "admin",
    "skip_email_verification": false
  }]
}
```

Change list:

| v1 | v2 |
| - | - |
| `zip` | `postal_code` |
| `legal_form: "GmbH"` | Enum code `"gmbh"` |
| `tax_deduction_eligibility: "J"` | `tax_treatment: "input_tax_deductible"` |
| Bare `iban` | `payout_bank_account` object with `account_holder`, `iban`, `bic` |
| German `legal_representatives[].type` codes | English codes; see the mapping below. A German code is `400 invalid_choice` |
| Separate `POST /partner/v1/users/` | Inline `users[]` with a `role`, committed atomically |
| — | Required `onboarding_mode`, `data_sharing_basis`, and a required `Idempotency-Key` |

Legal representative types:

| v1 `type` | v2 `type` |
| - | - |
| `geschaeftsführer` | `managing_director` |
| `direktor`, `director` | `director` |
| `vorstand` | `board_member` |
| `vorsitzender` | `chairperson` |
| `komplementaer` | `general_partner` |
| `gesellschafter` | `shareholder` |
| `inhaber` | `owner` |
| `partner` | `partner` |
| `aufsichtsrat` | `supervisory_board` |
| `sonstiges` | `other` |

Companies onboarded through v1 read back in the English codes.

Persist the response's company and membership UUIDs directly:

```json migration-example=partner-current-company-response theme={null}
{
  "id": "10000000-0000-4000-8000-000000000001",
  "customer_number": "5G0123",
  "name": "Example Manufacturing GmbH",
  "onboarding_mode": "api_only",
  "onboarding_status": "confirmation_not_required",
  "legal_form": "gmbh",
  "address": {
    "street": "Example Avenue 5",
    "postal_code": "10115",
    "city": "Berlin",
    "country": "DE"
  },
  "phone": "+49301234567",
  "tax_treatment": "input_tax_deductible",
  "vat_number": "DE129273398",
  "default_claim_type": "H11",
  "payout_bank_account": {
    "account_holder": "Example Manufacturing GmbH",
    "iban": "DE89370400440532013000",
    "bic": null
  },
  "notification_channels": [{
    "type": "email",
    "value": "operations@example.test",
    "notifications": ["requests_to_client", "status_updates", "statements"]
  }],
  "legal_representatives": [{
    "type": "managing_director",
    "name": "Taylor Example"
  }],
  "case_access": "available",
  "case_submission_readiness": {"ready": true, "issues": []},
  "users": [{
    "id": "11000000-0000-4000-8000-000000000001",
    "email": "admin@example.test",
    "first_name": "Taylor",
    "last_name": "Example",
    "role": "admin",
    "status": "pending_setup",
    "invite_expires_at": "2026-09-06T10:00:00Z",
    "revoked_at": null,
    "revoked_by_token_id": null,
    "revocation_reason": "",
    "sandbox_origin": "unclassified",
    "created_at": "2026-08-27T10:00:00Z",
    "updated_at": "2026-08-27T10:00:00Z"
  }],
  "sandbox_origin": "unclassified",
  "created_at": "2026-08-27T10:00:00Z",
  "updated_at": "2026-08-27T10:00:00Z"
}
```

<Warning>
  **Company editing follows the paywise UI's protection rules.** Identity and
  tax fields lock as onboarding and case submission progress; legal
  representatives can lock earlier. Payout-account changes after submission
  require the secured paywise UI process. Resolve and verify these fields
  before the canary. Use `PATCH /partner/v2/companies/{id}/` for
  supplied company fields only; memberships have their own nested operations.
</Warning>

Provisional authorization committed by this command is **not** confirmed
consent. The invited admin later confirms the named Partner and the current
authorization version during setup or login. See
[API-only onboarding](/api-docs/partner-api/workflows/api-only-onboarding).

### 3. Replace top-level users and invites with nested memberships

New v2 invitations use a company-scoped membership. Before creating one for an
existing company, fully paginate `GET /partner/v2/companies/{company_id}/users/`
and resolve the intended relationship from the returned membership records.
Store the verified membership UUID with its company UUID. Do not substitute an
old global user or invite UUID, and do not assume every old invitation has a
direct v2 membership equivalent. Reconcile pending onboarding before sending
a new invitation.

For new onboarding, this is what v1 sent:

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

{
  "email": "admin@example.test",
  "first_name": "Taylor",
  "last_name": "Example",
  "company": "10000000-0000-4000-8000-000000000001",
  "notification_channels": [],
  "skip_email_verification": false
}
```

| v1 | v2 |
| - | - |
| `POST /partner/v1/users/` | `POST /partner/v2/companies/{company_id}/users/` — the company moves from the body into the path |
| `POST /partner/v1/userinvites/` | The same membership command; it sends setup mail and returns the invite link once as `setup_url` (absent from reads and replays), never a v1-style redirect URL |
| `GET /partner/v1/userinvites/{id}/get-onboarded-user/` | Nothing. The membership itself becomes active — read it |
| `GET /partner/v1/users/` (global) | `GET /partner/v2/companies/{company_id}/users/`; traverse the exact company |
| `GET /partner/v1/users/{id}/` | `GET /partner/v2/companies/{company_id}/users/{membership_id}/` — both UUIDs are required |

Retain the returned membership UUID; **an old global user ID is not a
membership ID.** Every add returns `pending_setup`, whether the address is new
or already belongs to a paywise account; the public response deliberately
does not reveal which global user path occurred. Only pending memberships can
change role, resend, or cancel. The invite link is returned once as
`setup_url` on the add and resend responses and omitted from reads and
idempotent replays; it leads to password setup only for an account the add
created and to sign-in for every account that existed before. Follow
[manage memberships](/api-docs/partner-api/workflows/manage-memberships).

<Info>
  Membership add is also resource-idempotent: the same company, case-insensitive
  email, and role may return the existing membership UUID without sending another
  email or event. That does not make email a safe resource ID, and it does not
  justify racing two adds.
</Info>

<Accordion title="skip_email_verification: what it does and does not do">
  Both the frozen v1 and the v2 wire schemas call the field
  `skip_email_verification`. One inherited frozen prose page calls it
  `skip_email_validation`; that is a documentation typo, not a wire alias, so
  never send it.

  The v2 flag requires a staff-controlled Partner entitlement and only asserts
  out-of-band email ownership for a **newly created** global user. It does not
  skip password setup, terms, named-Partner confirmation, or setup mail; it does
  not skip the initial `pending_setup` membership state; and it reveals nothing
  about an already existing global user.
</Accordion>

### 4. Read readiness and access as two separate answers

v1 treated `data_submission_completed` as the onboarding gate. v2 splits it,
and you must check both.

`case_submission_readiness.ready` answers whether the company's data can pass
Case finalization. Its issue objects contain actionable `field`, `code`, and
`message` values — render remediation from those pairs rather than
reconstructing your own. Draft creation can occur before readiness is complete;
finalize revalidates it atomically.

`case_access` answers whether the Partner may currently act, and exposes only
`available` or `unavailable`. Provisional API-onboarding access can be
available before confirmed consent; conversely, complete company data can be
ready while delegation is unavailable.

Require both projections separately before every finalize. Follow
[readiness and access](/api-docs/partner-api/concepts/readiness-and-access) and
[resolve readiness](/api-docs/partner-api/workflows/resolve-readiness).

### 5. Provision Partner-owned webhooks

Creating a company does not create a webhook subscription. Choose the owner
and event coverage explicitly:

* `/partner/v2/webhooks/` subscriptions belong to the **Partner** and receive
  company, membership, readiness, and access events. Add Case events with a
  `companies` selector (`"*"` or a list of company UUIDs). Manage them with the
  Partner key and **no** company header.
* `/v2/webhooks/` subscriptions belong to **one company** and receive Case
  events. A direct Case credential manages them. A Partner key with
  `X-On-Behalf-Of-Company` may read them but cannot write them
  (`403 use_partner_webhooks`).

Provision and store their UUIDs and secrets separately. Deliveries are at least
once and unordered: verify exact raw bytes, deduplicate by event UUID, accept
durably before `2xx`, then refetch authoritative state. Readiness events are
signals, not complete issue reports.

Create and bodyless rotate expose `secret_key` once through their documented
one-time-secret response schema. Test and manual-redelivery commands return
`202 Accepted` with required `detail`, `delivery_id`, and `event_id`;
redelivery creates a new delivery ID and preserves the event ID. Subscriptions
support `events: ["*"]`, while test-event selection remains limited to concrete
events. Never substitute `X-Paywise-Request-Id` for either identifier.

<Warning>
  On **every** `company.access.confirmed`, `.revoked`, or `.restored` event,
  temporarily gate delegated writes and fetch the exact current Company. Apply
  unavailable cleanup only if `case_access` is `unavailable`; restore or preserve
  traffic if it is `available`, even when a delayed revocation caused the fetch.
  `authorization_version` identifies consent text, not event order. Restoration
  does not replay missed events or restore deleted Partner-created drafts, so run
  scheduled current-state reconciliation.
</Warning>

Use the [access-change workflow](/api-docs/partner-api/workflows/handle-access-changes),
[consume Partner webhooks](/api-docs/partner-api/workflows/consume-webhooks),
and [webhook ownership](/api-docs/partner-api/concepts/webhook-ownership).

### 6. Route delegated Case calls by company

Every Case command your platform issues on a company's behalf now carries
`X-On-Behalf-Of-Company` and no user header. Route each business command by the
exact company UUID and command ID to exactly one contract — v1 **or** v2, never
both. When `case_access` is `unavailable`, gate and quarantine unsent work; do
not fall back to `X-User-Id` or another credential.

The Case-side migration itself — orders, finalize, documents, payments,
mandates — is covered by the
[Case Management migration guide](/api-docs/case-management-api/migrate-from-v1).
See also [submit cases for a company](/api-docs/partner-api/workflows/submit-cases-for-a-company).

## Part B — cut over

### Coexistence without duplicate companies or commands

Select one dispatch path for each logical command in the migration ledger.
Both versions operate on the same company and Case records:

| Record or command | Coexistence rule |
| - | - |
| Existing legal company | Reuse its existing UUID and refresh the v2 projection; no import or new company is needed |
| v1 user or invite | Resolve the company membership where one exists; old user/invite UUIDs are not membership UUIDs. Reconcile pending invitations before starting another flow |
| v2 company mutation | Stop v1 mutation for that company first; owned arrays and company-editing locks make dual writes unsafe |
| Delegated Case write | Route by exact company and business-command ID to v1 **or** v2, never both |
| Access unavailable | Gate and quarantine unsent work; do not fall back to `X-User-Id` or another credential |

Company read comparison can run in parallel, but raw payload equality is not a
goal. Compare legal identity, addresses, tax treatment, notification coverage,
membership intent, and business readiness while respecting the new models. Keep
the existing company UUID for current paths and audit. Keep old user and invite
IDs separately from company-scoped membership UUIDs.

### Staged rollout

1. Inventory v1 companies, users, invites, `X-User-Id` call sites, credentials,
   hosted redirects, background jobs, and Case write routing. Record
   every in-flight onboarding state.
2. Obtain or confirm v2 sandbox credentials and verify them. Read each existing
   company by its stored UUID and resolve its membership UUIDs without blind
   creates.
3. Transform golden company and user fixtures and exercise
   [API-only onboarding](/api-docs/partner-api/workflows/api-only-onboarding),
   membership recovery, readiness remediation, and delegated draft/finalize in
   sandbox with authoritative environment proof.
4. Create Partner-owned sandbox subscriptions with the required company
   coverage. Drill duplicates, out-of-order access events, and secret rotation.
5. Canary one explicit company cohort. Stop its v1 mutations, switch new
   onboarding and Case commands atomically, and keep other companies on v1.
6. Expand only after the verification gates pass. Then stop new v1 Partner
   writes, verify historical records through v2, and retire v1 credentials and
   subscriptions through the authorized process.

### Verification gates

* Every existing legal company retains its UUID and no duplicate is created
  by the migration; every resolved membership has one company parent and its
  own verified UUID.
* Company detail matches the intended legal, tax, bank, and notification data
  and has a non-cancelled admin; readiness issues are rendered from their
  field/code pairs rather than reconstructed.
* `case_access` and readiness are evaluated independently before every canary
  finalize; no v2 request sends `X-User-Id`.
* Partner management calls never send `X-On-Behalf-Of-Company`; every delegated
  Case call sends the exact canary company UUID.
* Exact same-key retry drills return the same company, membership, order, or
  delivery resource IDs while request IDs remain support-only.
* Paginated interruption and overlap drills converge and never claim a snapshot.
* Partner and Case webhook stores keep owners and secrets separate. Duplicate
  and reordered access events always end in a Company refetch, and a
  missed-restoration drill is repaired by scheduled reconciliation.

### Roll back by stopping writes

Rollback is a write stop, not a reverse migration. Disable v2 Partner and
delegated Case dispatch for the affected cohort, preserve all keys, bodies,
company context, and response IDs, then reconcile every unknown outcome. Do not
recreate a company, mint or seek an undocumented per-company key, replay
commands with `X-User-Id`, or automatically route a timed-out v2 command to v1.

Previously committed changes remain on the shared company and Case records;
membership relationships also remain intact. Rollback changes only future
routing. Resume a v1 operation only after
an operator proves that the same business command did not commit in v2 and that
v1 is still its assigned authorized owner. For access loss, keep writes gated
until exact Company state is available; never use rollback to bypass delegation.


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