Skip to main content
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 only to inventory old behaviour; build the new flows from the current Partner overview and resource model. 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.

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

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:
Delegated Case calls use that same key plus the exact company UUID on every /v2/ request:
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.

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:
migration-example=partner-v1-info-response
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:
migration-example=partner-current-info-response
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 and calling the Case Management 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:
migration-example=partner-v1-company-request
The v2 command commits the company, up to ten memberships, provisional authorization, events, and its idempotent response together. The same business data, renamed:
migration-example=partner-current-company-request
Change list: Legal representative types: Companies onboarded through v1 read back in the English codes. Persist the response’s company and membership UUIDs directly:
migration-example=partner-current-company-response
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.
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.

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:
migration-example=partner-v1-user-request
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.
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.
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.

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 and 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.
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.
Use the access-change workflow, consume Partner webhooks, and 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. See also 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: 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, 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.