Skip to main content

Migration guides

Upgrade @zkp2p/pay-sdk and @zkp2p/pay-shared together to the same version, then typecheck. TypeScript points at each site that needs attention. The SDK re-exports shared types and depends on the matching shared major: @zkp2p/pay-sdk@7.1.0 requires @zkp2p/pay-shared@^7.1.0.

npm install @zkp2p/pay-sdk@^7.1.0 @zkp2p/pay-shared@^7.1.0

Coming from an older major, apply the sections below in order, oldest first, and move both packages to that major before starting the next one. Each section lists only the steps specific to its version.

v7.0 to v7.1​

Upgrade @zkp2p/pay-sdk and @zkp2p/pay-shared together to 7.1.0. WebhookEventType gains DISPUTE_OPENED, DISPUTE_PAID, and DISPUTE_ESCALATED for merchant-paid seller disputes. These are separate from chargebacks paid from buyer stake.

Existing LIVE webhook endpoints are auto-subscribed to the three DISPUTE_* events at the 7.1.0 rollout. Webhooks created afterward and all sandbox webhooks must explicitly list the three dispute events.

The SDK exports DisputeWebhookPayload, DisputeWebhookData, DisputeStatus, isDisputeWebhook, and DISPUTE_WEBHOOK_VERSION (1). Dispute amountUsdc is a raw 6-decimal integer string; caseAmount has 2 fiat decimals. See dispute events.

Existing order and payout payload shapes are unchanged, but automatic subscription is a breaking integration change for handlers that assume every delivery contains data.order. Dispute deliveries use the new DisputeWebhookData payload family and have no data.order. Branch on the event type or isDisputeWebhook, and handle payouts with isPayoutWebhook, before reading order data. A switch over WebhookPayload['type'] must handle the three new members.

Upgrade @zkp2p/peer-pay-cli to 0.7.0 for disputes list, disputes get DISPUTE_ID, and simulate ORDER_ID --event DISPUTE_* (choose DISPUTE_OPENED, DISPUTE_ESCALATED, or DISPUTE_PAID). Simulation needs a settled local fiat payment; payment attempts remain dashboard-only. See the CLI command reference.

v6 to v7​

Upgrade @zkp2p/pay-sdk and @zkp2p/pay-shared to 7.0.0, and @zkp2p/peer-pay-cli to 0.6.0. Version 6.1.0 was never published; its multi-currency changes ship here. This is a coordinated hard cutover of merchant contracts: there are no old SDK exports, API aliases or CLI command aliases. Update callers, webhook subscriptions, validators and iframe listeners together.

SDK functions, types and constants​

The complete step-1 export mapping is below. It covers SDK exports and shared-only exports; each remains available from the same package as before. Currency helpers introduced on the unreleased 6.1.0 branch appear here for users testing that branch. CashoutPayout* merchant types become Payout*, without a doubled Payout prefix. Player-only types retain their names.

OldNew
CASHOUT_CURRENCY_INFOPAYOUT_CURRENCY_INFO
CashoutCurrencyTypePayoutCurrencyType
CashoutOracleCurrencyTypePayoutOracleCurrencyType
CashoutCurrencyPayoutCurrency
CASHOUT_ORACLE_CURRENCIESPAYOUT_ORACLE_CURRENCIES
CASHOUT_RAIL_CURRENCIESPAYOUT_RAIL_CURRENCIES
CASHOUT_CURRENCY_NAMESPAYOUT_CURRENCY_NAMES
CASHOUT_CURRENCY_SYMBOLSPAYOUT_CURRENCY_SYMBOLS
CashoutFiatValuePayoutFiatValue
isCashoutCurrencyisPayoutCurrency
cashoutRailPaysCurrencypayoutRailPaysCurrency
isCashoutMultiCurrencyRailisPayoutMultiCurrencyRail
cashoutFiatCentspayoutFiatCents
formatCashoutFiatCentsformatPayoutFiatCents
formatCashoutRateformatPayoutRate
formatCashoutFiatValueformatPayoutFiatValue
isCashoutSarRailisPayoutSarRail
CASHOUT_CRYPTO_RAILSPAYOUT_CRYPTO_RAILS
isCashoutCryptoRailisPayoutCryptoRail
cashoutCryptoRailChainIdpayoutCryptoRailChainId
hasCashoutCryptoRailhasPayoutCryptoRail
cashoutAccountLabelpayoutAccountLabel
CASHOUT_FUNDING_CHAIN_IDSPAYOUT_FUNDING_CHAIN_IDS
getCashoutFundingTokenConfigsgetPayoutFundingTokenConfigs
CASHOUT_WEBHOOK_EVENT_TYPESPAYOUT_WEBHOOK_EVENT_TYPES
CashoutWebhookEventTypeValuePayoutWebhookEventTypeValue
isCashoutWebhookEventTypeisPayoutWebhookEventType
CASHOUT_WEBHOOK_VERSIONPAYOUT_WEBHOOK_VERSION
CashoutPartialPaymentPayoutPartialPayment
CashoutWebhookEventDataPayoutWebhookEventData
CashoutWebhookPayloadPayoutWebhookPayload
isCashoutWebhookisPayoutWebhook
CashoutStatusPayoutStatus
CashoutStatusTypePayoutStatusType
CashoutFundingStatusPayoutFundingStatus
CashoutFundingStatusTypePayoutFundingStatusType
CashoutCancelSourcePayoutCancelSource
CashoutCancelSourceTypePayoutCancelSourceType
CashoutPayoutKindPayoutKind
CashoutPayoutKindTypePayoutKindType
CashoutAttemptStatusPayoutAttemptStatus
CashoutAttemptStatusTypePayoutAttemptStatusType
CashoutFiatRailPayoutFiatRail
CashoutFiatRailTypePayoutFiatRailType
CashoutCryptoRailPayoutCryptoRail
CashoutCryptoRailTypePayoutCryptoRailType
CashoutRailPayoutRail
CashoutRailTypePayoutRailType
CashoutSarRailTypePayoutSarRailType
CashoutPayoutProviderPayoutProvider
CashoutPayoutProviderTypePayoutProviderType
CashoutCryptoDestinationPayoutCryptoDestination
CashoutCryptoMethodPayoutCryptoMethod
CashoutPartialFillViewPayoutPartialFillView
CashoutSettlementBreakdownPayoutSettlementBreakdown
CashoutViewPayoutView
CreateCashoutRequestCreatePayoutRequest
CreateCashoutResponseCreatePayoutResponse
ListCashoutsResponseListPayoutsResponse
CashoutTimelineEventTypePayoutTimelineEventType
CashoutTimelineEventTypeValuePayoutTimelineEventTypeValue
CASHOUT_PAYOUT_STEP_MAX_USDCPAYOUT_STEP_MAX_USDC
CashoutPayoutStepRangePayoutStepRange
CashoutSettingsPayoutSettings
CashoutFundingIssueKindPayoutFundingIssueKind
CashoutFundingIssueKindTypePayoutFundingIssueKindType
CashoutFundingIssueResolutionPayoutFundingIssueResolution
CashoutFundingIssueResolutionTypePayoutFundingIssueResolutionType
CashoutFundingTokenOptionPayoutFundingTokenOption
CashoutSettingsViewPayoutSettingsView
MerchantCashoutFundingIssueMerchantPayoutFundingIssue
CashoutPayoutMethodViewPayoutMethodView
MerchantCashoutViewMerchantPayoutView
CashoutTimelineEntryPayoutTimelineEntry
CashoutWebhookDeliveryViewPayoutWebhookDeliveryView
MerchantCashoutDetailMerchantPayoutDetail
CreateMerchantCashoutResponseCreateMerchantPayoutResponse
ListMerchantCashoutsResponseListMerchantPayoutsResponse
CashoutClientOptionsPayoutClientOptions
CreateCashoutOptionsCreatePayoutOptions
ListCashoutsParamsListPayoutsParams
cancelCashoutcancelPayout
createCashoutcreatePayout
getCashoutgetPayout
listCashoutslistPayouts

Routes and JSON fields​

SurfaceOldNew
API key: POST, GET list/api/v1/cashouts/api/v1/payouts
API key: GET detail/api/v1/cashouts/:id/api/v1/payouts/:id
API key: POST cancel/api/v1/cashouts/:id/cancel/api/v1/payouts/:id/cancel
Dashboard: POST, GET list/api/v1/merchants/me/cashouts/api/v1/merchants/me/payouts
Dashboard: GET detail/api/v1/merchants/me/cashouts/:id/api/v1/merchants/me/payouts/:id
Dashboard: POST cancel/api/v1/merchants/me/cashouts/:id/cancel/api/v1/merchants/me/payouts/:id/cancel
Dashboard: GET, PUT settings/api/v1/merchants/me/cashout-settings/api/v1/merchants/me/payout-settings
Dashboard page/cashouts/payouts
Dashboard detail page/cashouts/:cashoutId/payouts/:payoutId
Dashboard settings page/settings/cashout/settings/payout

Merchant response and webhook data identifiers change from cashoutId to payoutId. The dashboard detail envelope changes from cashout to payout; read detail.payout.payoutId. This does not rename the existing nested PayoutView.payout amount/chain/token object. Request money fields, API-key/Privy authentication and idempotency headers are unchanged.

For iframe checkout.success and checkout.failed events, the id moved from payload.cashoutId to payload.payoutId; an iframe host reads event.data.payload.payoutId. The event names are unchanged. checkout.success with status: "LISTING" means listed, not paid; reconcile settlement from the API or a signed webhook.

Webhook version 2​

Update subscription names and exhaustive event switches:

v1v2
CASHOUT_ORDER_CREATEDPAYOUT_ORDER_CREATED
CASHOUT_ORDER_CANCELLEDPAYOUT_ORDER_CANCELLED
CASHOUT_ORDER_PARTIALLY_FUNDEDPAYOUT_ORDER_PARTIALLY_FUNDED
CASHOUT_ORDER_FUNDEDPAYOUT_ORDER_FUNDED
CASHOUT_ORDER_MATCHEDPAYOUT_ORDER_MATCHED
CASHOUT_ORDER_PARTIALLY_PAIDPAYOUT_ORDER_PARTIALLY_PAID
CASHOUT_ORDER_SETTLEDPAYOUT_ORDER_SETTLED
CASHOUT_ORDER_EXPIREDPAYOUT_ORDER_EXPIRED

New payout deliveries have version: 2 and data.payoutId. Use PAYOUT_WEBHOOK_VERSION and isPayoutWebhook; order webhooks keep their existing unversioned envelope. Rename partialPayment member types as listed above.

Deliveries queued before the deploy keep their stored v1 payloads, including old CASHOUT_ORDER_* types and data.cashoutId, even on retry. Drain those deliveries before cutover or keep a v1 receiver until they finish; the server does not convert historical payloads. The v7 types and isPayoutWebhook describe the new v2 contract, so route a legacy delivery by its old event type before passing it to v7 handling. See events and deduplication.

Merchant error codes​

Only merchant API and dashboard errors change. Player endpoints still return CASHOUT_*; generic codes such as INVALID_IDEMPOTENCY_KEY are unchanged.

OldNew
CASHOUTS_DISABLEDPAYOUTS_DISABLED
CASHOUT_FUNDING_AMOUNT_TOO_LOWPAYOUT_FUNDING_AMOUNT_TOO_LOW
CASHOUT_FUNDING_DETECTEDPAYOUT_FUNDING_DETECTED
CASHOUT_FUNDING_TOKEN_UNSUPPORTEDPAYOUT_FUNDING_TOKEN_UNSUPPORTED
CASHOUT_MERCHANT_WALLET_MISSINGPAYOUT_MERCHANT_WALLET_MISSING
CASHOUT_NOT_CANCELLABLEPAYOUT_NOT_CANCELLABLE
CASHOUT_NOT_FOUNDPAYOUT_NOT_FOUND
CASHOUT_NO_RAILS_AVAILABLEPAYOUT_NO_RAILS_AVAILABLE
CASHOUT_PAYOUT_ABOVE_MAXPAYOUT_ABOVE_MAX
CASHOUT_PAYOUT_NOT_STEP_MULTIPLEPAYOUT_NOT_STEP_MULTIPLE
CASHOUT_PAYOUT_TOKEN_UNSUPPORTEDPAYOUT_TOKEN_UNSUPPORTED
CASHOUT_PRIVY_WALLET_FAILEDPAYOUT_PRIVY_WALLET_FAILED
CASHOUT_RELAY_QUOTE_FAILEDPAYOUT_RELAY_QUOTE_FAILED
CASHOUT_RELAY_STATUS_FAILEDPAYOUT_RELAY_STATUS_FAILED
CASHOUT_REQUESTED_RAILS_UNAVAILABLEPAYOUT_REQUESTED_RAILS_UNAVAILABLE
CASHOUT_ROUTE_UNQUOTABLEPAYOUT_ROUTE_UNQUOTABLE
CASHOUT_SANDBOX_UNSUPPORTEDPAYOUT_SANDBOX_UNSUPPORTED

CLI 0.6.0​

Every command that takes a payout id now shows the positional PAYOUT_ID. Replace scripts and saved command lines; the old command names are rejected. The new command reference anchors are linked below.

Old commandNew command
cashoutspayouts
cashouts createpayouts create
cashouts getpayouts get
cashouts listpayouts list
cashouts cancelpayouts cancel
cashouts configure-localpayouts configure-local
cashouts simulate-fundingpayouts simulate-funding
cashouts sweep-localpayouts sweep-local
cashouts simulate-credentialpayouts simulate-credential
cashouts resolve-sendpayouts resolve-send
cashouts simulate-payout-quotepayouts simulate-payout-quote
cashouts simulate-payout-bridgepayouts simulate-payout-bridge
cashouts simulate-paymentpayouts simulate-payment
cashouts simulate-peer-withdrawpayouts simulate-peer-withdraw
cashouts simulate-wallet-balancepayouts simulate-wallet-balance
cashouts emailspayouts emails
cashouts funding-issuespayouts funding-issues
cashouts resolve-funding-issuepayouts resolve-funding-issue
cashouts timelinepayouts timeline
local admin cashoutlocal admin payout
local admin cashout-settingslocal admin payout-settings

Old local state files with legacy subscription/event names or v1 deliveries are rejected. There is no load-time conversion. To reset an old simulator state file, use peer-pay-cli local reset --state PATH (see local reset). Stop the server first. The command requires --state and an existing file, renames it to PATH.backup-<uuid> without parsing it, and prints the backup path. Restart peer-pay-cli serve with the same --state path for fresh state. Hosted data is unchanged.

The admin emulator command labels change, but the Peer admin paths, types and fields do not. For example, payouts funding-issues still returns the admin cashoutId field. Local-control errors use PAYOUT_* (including PAYOUT_LOCAL_MERCHANT_ONLY); player-route errors keep CASHOUT_*.

Intentional retained names​

  • Treat webhook id and stored ids as opaque deduplication keys. An id such as cashout_<storedId>_CASHOUT_ORDER_SETTLED keeps that spelling; do not infer the public event type from it, rewrite it, or reset deduplication records.
  • The player API remains /api/v1/cashout-checkout with x-cashout-token, and the hosted player URL remains /c/:id. Types such as CashoutCheckoutState, CashoutPayoutQuote and SetCashoutPayoutMethodRequest keep their names.
  • Peer admin /api/v1/admin/cashouts routes and AdminCashout* types remain. Internal modules, database columns, Prisma enum labels and stored subscriptions also retain their names; the API maps stored webhook labels at its boundaries.
  • Historical migration sections below describe the exports of their own major version. Their old names are migration evidence, not v7 aliases.

Multi-currency changes included in v7​

Players can receive every catalog currency on PayPal and Revolut by default. Currency options come from the rail catalog. Merchant create requests, funding, fees, limits, refunds and accounting remain USD/USDC.

Payout responses add payoutCurrency, partialFills[].fiat, settlement.buyerPaidFiat and webhook partialPayment.fill.fiat. Fiat is information only, at the buyer's bound rate. A missing currency or rate gives null; grouped totals are null if any payment is unknown, and [] if no buyer paid. Update fixtures and strict response validators for these fields. The SDK exports PayoutCurrency, PayoutCurrencyType and PayoutFiatValue; the shared package also exports selector/pricing types and adds payoutCurrencies, fiatPricing, buyerPayment, partialPayment.paidFiat, and fiat method currency. These fields ship with payout webhook version 2; order webhooks are unchanged.

The internal Revolut player request is a hard cutover: it now needs { rail: 'revolut', payeeHandle: 'alice', currency: 'USD' } (or any Revolut catalog currency). An old request without currency returns 400; reload old checkout tabs. PayPal now also requires currency: { rail: 'paypal', payeeHandle: 'alice', paypalEmail: 'alice@example.com', currency: 'EUR' }. Other rails keep their existing request bodies. A currency outside its rail's catalog returns 400. Oracle estimate failures never block saving or confirming a payout. See payout currencies and local CLI simulation.

Customer checkout cancellation has been removed from the API and local CLI simulator. Remove CANCELLING, cancel source CHECKOUT, player actions CANCEL / FINISH_CANCEL, and timeline event CANCEL_STARTED from integrations. The player /cashout-checkout/:id/cancel route no longer exists. Funded payouts are managed in the Peer app; the merchant cancelPayout operation still cancels an unfunded payout.

USD stays fixed at 1:1 without an oracle. Non-USD listings use zero spread and exactly the SDK's getSpreadOracleConfig parameters with the per-environment adapter. Pay adds no staleness gate. Currency selection uses the saved currency, then the last payout's currency, then USD; there is no geolocation.

v5 to v6​

From v5 (@zkp2p/pay-sdk@5.0.0 / @zkp2p/pay-shared@5.0.0) to v6 (@zkp2p/pay-sdk@6.0.0 / @zkp2p/pay-shared@6.0.0).

Summary​

v6 adds the cashout client and versioned cashout webhooks. WebhookPayload now covers both order and cashout events; order payloads keep their existing data.order shape.

Breaking changes​

  • WebhookPayload is OrderWebhookPayload | CashoutWebhookPayload. Narrow before reading data.order. isCashoutWebhook is exported from both the SDK and shared package:

    import { isCashoutWebhook, type WebhookPayload } from '@zkp2p/pay-sdk';

    function handleWebhook(payload: WebhookPayload) {
    // Call only after verifying the signature.
    if (isCashoutWebhook(payload)) {
    // payload.data is a CashoutView; payload.version is the cashout schema version.
    // CASHOUT_ORDER_PARTIALLY_PAID also carries payload.partialPayment.
    } else {
    // payload.data.order is the CheckoutOrder | null, as before.
    }
    }
  • WebhookEventTypeValue includes eight new events: CASHOUT_ORDER_CREATED, CASHOUT_ORDER_CANCELLED, CASHOUT_ORDER_PARTIALLY_FUNDED, CASHOUT_ORDER_FUNDED, CASHOUT_ORDER_MATCHED, CASHOUT_ORDER_PARTIALLY_PAID, CASHOUT_ORDER_SETTLED and CASHOUT_ORDER_EXPIRED. Update exhaustive switches, or use OrderWebhookEventTypeValue from @zkp2p/pay-shared for order-only handlers.

New​

  • createCashout, getCashout, listCashouts and cancelCashout, with CashoutClientOptions, CreateCashoutOptions and ListCashoutsParams: see SDK cashouts.
  • Typed cashout views, lifecycle enums and webhooks, including CASHOUT_WEBHOOK_VERSION and CashoutPartialPayment: see the Cashouts API.
  • See Cashouts in the dashboard for merchant setup, funding and tracking.

v4 to v5​

From v4 (@zkp2p/pay-sdk@4.0.1 / @zkp2p/pay-shared@4.0.1) to v5 (@zkp2p/pay-sdk@5.0.0 / @zkp2p/pay-shared@5.0.0).

Summary​

v5 adds open-amount orders, where the buyer chooses the amount at checkout. Every order now carries amountMode, and an open-amount order has no amount until its buyer starts a payment. v5 also publishes the type changes made since 4.0.1, listed below.

Breaking changes​

  • CheckoutOrder is a union on amountMode. Narrow before reading amounts:

    if (order.amountMode === 'OPEN' && order.requestedUsdcAmount === null) {
    // The buyer has not chosen an amount yet.
    }
  • createCheckout results with idempotentReplay: true have checkoutUrl: null; keep the URL from the first call.

  • feePayer can be 'SPLIT' (with buyerFeeShareBps); handle it wherever you switch on FeePayerType.

  • CheckoutAggregate.order is a HostedCheckoutOrder. Narrow on amountMode the same way.

  • PaymentDeeplinkResponse.intentHash and proofSubmissionUrl can be null.

  • CreatePaymentRequest takes amount, amountCurrency and expectedAmountVersion all together or not at all.

  • New required fields matter only if you build these objects yourself, for example in test fixtures: CheckoutPayment.penalties; buyerFeeShareBps on CheckoutOrder and MerchantConfig; MerchantConfig.applePayAvailable, masterMerchantEnabled and masterMerchantMaxFeeBps; MerchantProfile.apiKeyIpAllowlist and masterMerchantId; and MerchantUser.removalLocked.

  • createCheckout throws when fields from more than one amount mode are set, even if one of them is an empty string. If you matched on the old message, match Provide exactly one of requestedUsdcAmount, requestedFiatAmount + requestedFiatCurrency, or openAmount.

New​

  • createCheckout({ openAmount: { currency: 'USD', minAmount: '10', maxAmount: '2000', presets: ['25', '50', '100'] } }).
  • Webhook ORDER_AMOUNT_SET with data.amountChange; subscribe to it explicitly. Credit the buyer on ORDER_FULFILLED, as before.
  • amountChange.fiat.currency and openAmount.savedInput.currency are the buyer's selected checkout currency, which can differ from openAmount.currency; account in USDC.
  • See Open amounts.

v3 to v4​

From v3 (@zkp2p/pay-sdk@3.1.0 / @zkp2p/pay-shared@3.1.0) to v4 (@zkp2p/pay-sdk@4.0.0 / @zkp2p/pay-shared@4.0.0).

Summary​

v4 is driven by one wire-level change: merchant plans. The API no longer reports FREE / PRO tiers. It reports BASE / PRO / CONCIERGE, or null when a merchant has not chosen a plan yet. Because the API already serves the new values, the v3 type definitions are stale; upgrade so getMerchant() is typed correctly.

Everything else in v4 is additive (chargeback webhook events, structured API errors, self-serve tier definitions, Tron USDT payouts).

Breaking changes in @zkp2p/pay-sdk​

These affect you even if you never import @zkp2p/pay-shared directly.

MerchantProfile.tier values changed​

getMerchant() returns MerchantProfile, which the SDK re-exports from @zkp2p/pay-shared.

v3v4
Type'FREE' | 'PRO'MerchantTierName | null where MerchantTierName = 'BASE' | 'PRO' | 'CONCIERGE'
Meaning of nullThe merchant has not selected a plan. Live order creation is rejected with 403 MERCHANT_TIER_FORBIDDEN until one is chosen in the dashboard.
Legacy mappingFREEnull
PROCONCIERGE (custom-terms plan)
// v3
if (merchant.tier === 'FREE') {
showUpgradePrompt();
}

// v4
if (merchant.tier === null) {
showChoosePlanPrompt();
}

BASE and PRO are the self-serve plans with monthly fiat volume and fiat order caps (crypto-only orders never count); their definitions are exported from @zkp2p/pay-shared as TIER_DEFINITIONS.

API failures throw PayApiError​

createCheckout, getMerchant, and checkQuoteAvailability now throw PayApiError (a subclass of Error) instead of a bare Error when the HTTP response is not OK or the API returns { success: false }.

import { createCheckout, PayApiError } from '@zkp2p/pay-sdk';

try {
await createCheckout(order, options);
} catch (error) {
if (error instanceof PayApiError) {
error.statusCode; // e.g. 400
error.errorCode; // e.g. 'VALIDATION_ERROR' (when the API sent one)
error.fieldErrors; // e.g. { amount: ['Required'] }
error.formErrors; // top-level validation messages
error.responseObject; // raw responseObject from the envelope
}
}

error.message now includes field-level detail, for example Invalid request: amount: Required; destinationChainId: Unsupported chain instead of just Invalid request. If you matched on the exact message text, switch to errorCode / fieldErrors. instanceof Error remains true. Malformed response envelopes (no success property, or no responseObject) still throw a plain Error.

Breaking changes in @zkp2p/pay-shared​

MerchantProfile.tier​

See above. The new MerchantTierName type is exported alongside it.

ChainConfig.usdcAddress is nullable​

Tron is now a supported payout chain, and it settles in USDT rather than USDC, so usdcAddress is string | null. getUsdcAddress(chainId) is unchanged and still returns string | undefined. Use resolveDestinationTokenAddress when you need the actual payout token for a chain.

Narrowed and renamed unions​

  • OrderBridgeInfo.status is now PaymentBridgeStatusType (PENDING, SUBMITTED, COMPLETED, FAILED), exported with the PaymentBridgeStatus constant. BridgeStatusResponse.status is unchanged and keeps its own lowercase union: 'pending' | 'completed' | 'failed' | 'not_applicable'.
  • OnboardingStepState.id is now OnboardingStepIdValue instead of string, and OnboardingStepId gained PLAN. Code that assigned arbitrary strings to id no longer compiles.

New in v4​

Chargeback webhook events​

WebhookEventType gained three members. Live webhooks that existed at the v4 rollout were subscribed to them automatically. Webhooks created since, and all sandbox webhooks, must list them in events explicitly.

  • PAYMENT_CHARGEBACKED
  • ORDER_PARTIALLY_CHARGEBACKED
  • ORDER_CHARGEBACKED

Supporting types: PaymentChargebackStatus, OrderChargebackStatus, ChargebackRefundOverlap, and PaymentChargebackFact.

Self-serve tiers​

SELF_SERVE_TIERS, TIER_DEFINITIONS, TierDefinition, TierMonthlyLimits, formatTierMonthlyLimitsCopy, MaterializedFeeConfig, and TierChangeSource.

Other additions​

  • CheckoutPaymentCreationResponse.recipientRecovery ('matched' | 'missed' | 'not_applicable') reports whether a re-quote after expiry landed on the same recipient.
  • TRON_CHAIN_ID, Tron USDT destination support, and the base58 helpers encodeBase58 / doubleSha256 next to the existing isValidTronAddress.
  • Integration types (IntegrationStatusResponse, SandboxTestOrderRequest, ...) for the /api/v1/integration endpoints.

v4 checklist​

  1. Replace tier === 'FREE' checks with tier === null; treat CONCIERGE as the former PRO.
  2. Replace message-string matching in catch blocks with PayApiError fields.
  3. If you subscribe to webhooks with an explicit event list, add the chargeback events you want to receive.

v2 to v3​

From v2 (@zkp2p/pay-sdk@2.0.0 / @zkp2p/pay-shared@2.0.0) to v3 (@zkp2p/pay-sdk@3.0.0 / @zkp2p/pay-shared@3.0.0).

Summary​

Both packages have breaking changes. Most are in @zkp2p/pay-shared, but @zkp2p/pay-sdk consumers are affected too: the SDK re-exports several shared types, and its embedded event union gained a member.

Every break in v3 is compile-time.

Breaking changes in @zkp2p/pay-sdk​

These affect you even if you never import @zkp2p/pay-shared directly.

EmbedCheckoutEventType gained checkout.closed​

If you exhaustively narrow the embedded event union (a switch with a default that assigns to never, or an exhaustiveness helper), it no longer compiles until you add the new arm:

function handle(type: EmbedCheckoutEventType) {
switch (type) {
case 'checkout.success': return onSuccess();
case 'checkout.failed': return onFailed();
case 'checkout.closed': return onClosed(); // v3: add this arm
default: {
const _exhaustive: never = type; // v2 compiled; v3 errors without the arm above
return _exhaustive;
}
}
}

Non-exhaustive if chains keep compiling, but they silently ignore checkout.closed and leave the customer stuck in an iframe that never closes. See Dismissing the iframe.

getMerchant(merchantId, options) overload removed​

getMerchant now takes a single argument:

// v2: the two-argument form silently ignored merchantId
const m = await getMerchant(merchantId, { apiBaseUrl, apiKey });

// v3
const m = await getMerchant({ apiBaseUrl, apiKey });

The merchant has always been determined by the API key, and the extra merchantId argument was ignored.

Response aliases removed​

Three aliases are gone because the same name resolved to different shapes in the two packages.

RemovedFromReplace with
CheckoutCreateResponse@zkp2p/pay-sdkCreateCheckoutResult
CheckoutCreateResponse@zkp2p/pay-sharedCreateOrderResponse
CreateCheckoutResponse@zkp2p/pay-sharedCreateOrderResponse

Non-enveloped API responses are rejected​

The SDK now requires a well-formed { success, message, responseObject, statusCode } envelope and throws Malformed API response envelope otherwise. The API always sends that envelope, so this only matters if you point the SDK at a mock, proxy, or fixture that returns bare JSON.

MerchantProfile fields changed​

MerchantProfile (also re-exported as MerchantInfo, and returned by getMerchant) dropped migrationStatus and added three required fields: integrationPath, onboardingCompletedAt, onboardingSkippedAt.

Reading profile.migrationStatus no longer compiles. Any code that constructs a MerchantProfile (test fixtures, mocks, fakes) must supply the three new fields.

Breaking changes in @zkp2p/pay-shared​

CreateWebhookResponse is now nested​

The webhook creation response changed from a flat object to a nested one. In v2 it was Webhook & { secret }, so the webhook fields sat at the top level:

// v2
const res: CreateWebhookResponse = await createWebhook(...);
res.id; // webhook id
res.url; // webhook url
res.events; // subscribed events
res.secret; // signing secret

In v3 the webhook is a nested property:

// v3
const res: CreateWebhookResponse = await createWebhook(...);
res.webhook.id;
res.webhook.url;
res.webhook.events;
res.secret; // unchanged, still top level

Only secret kept its position. Every other field moved under webhook.

WebhookWithSecret removed​

WebhookWithSecret was the alias backing the old flat CreateWebhookResponse. It no longer exists. Use CreateWebhookResponse in its new nested shape, or compose it yourself:

// v2
import type { WebhookWithSecret } from '@zkp2p/pay-shared';

// v3
import type { Webhook, CreateWebhookResponse } from '@zkp2p/pay-shared';
type WebhookWithSecret = { webhook: Webhook; secret: string };

Dashboard-only exports removed​

These were internal to the merchant dashboard and have no v3 replacement. Delete any references:

  • DashboardPrivyApp
  • DashboardPrivyAppType
  • MerchantMigrationStatus
  • MerchantMigrationStatusType

New in v3​

The checkout.closed event​

Covered as a breaking change above for its effect on exhaustive unions, but the behavior behind it is new. When an embedded checkout has no payable option, for example because no payment rail has liquidity for the amount, it shows a Go Back button that posts:

{
"channel": "zkp2p_checkout_embed_v1",
"type": "checkout.closed",
"timestamp": 1723456789012,
"payload": { "order_id": "...", "reason": "no_payment_methods" }
}

Handle it by closing the iframe. It is a dismissal rather than a failure, and the order can be reopened with the same checkout URL. If you do not handle it, the Go Back button appears to do nothing and the customer stays stuck in the iframe. See Integration Options for the full listener.

Not proof that nothing was charged

checkout.closed does not guarantee nothing was charged. A partially paid order returns to method selection to pay its remaining balance, and if no rail has liquidity for that remainder it can emit checkout.closed too. Check the authoritative order state by order_id or from your webhook stream before telling a customer nothing was taken.

Additive changes​

Nothing in this section requires a change to existing code.

  • CheckoutNearbySuggestion and CheckoutNearbySuggestions are re-exported from @zkp2p/pay-sdk, describing the nearby-amount suggestions offered to dynamic-orders merchants when an exact amount has no liquidity.
  • @zkp2p/pay-shared gained address validators (isValidSolanaAddress, isValidTronAddress, base58 helpers), merchant onboarding step types, and referral fee-cap helpers.

v3 checklist​

  1. Add a checkout.closed arm to any exhaustive switch over EmbedCheckoutEventType, and a handler that closes the iframe if you embed checkout. A non-exhaustive if chain still compiles but silently strands the customer.
  2. Drop the first argument from any getMerchant(merchantId, options) call.
  3. Replace CheckoutCreateResponse / CreateCheckoutResponse with CreateCheckoutResult (SDK) or CreateOrderResponse (shared).
  4. If you point the SDK at a mock or proxy, make sure it returns the full { success, message, responseObject, statusCode } envelope.
  5. Replace reads of MerchantProfile.migrationStatus, and add integrationPath, onboardingCompletedAt and onboardingSkippedAt to any MerchantProfile fixtures or mocks you construct.
  6. Search your codebase for WebhookWithSecret and replace it.
  7. Update webhook-creation handling to read response.webhook.* instead of response.*, keeping response.secret as is.
  8. Delete any references to DashboardPrivyApp or MerchantMigrationStatus.

v1 to v2​

From v1 (@zkp2p/pay-sdk@1.x) to v2 (@zkp2p/pay-sdk@2.0.0 / @zkp2p/pay-shared@2.0.0).

Summary​

v2 unifies Zelle on a single rail. Previously a Zelle payment could carry a bank-specific rail value (zelle-chase, zelle-bofa, zelle-citi); as of v2 the rail is always the base platform:

  • payment.rail is always zelle for Zelle payments, in API responses, checkout aggregates, and webhook payloads.
  • The customer's bank pick is exposed separately as the new payment.paymentMethodId field, typed string | null (currently zelle-chase | zelle-bofa | zelle-citi, null for non-Zelle rails). It is display metadata, not a rail.
  • enabledRails and preselectedMethod continue to use base platform values (zelle). Nothing changes there.

Breaking changes​

payment.rail no longer carries Zelle bank variants​

If your integration matched on zelle-chase, zelle-bofa, or zelle-citi (for example in webhook handlers or reconciliation), match on zelle instead. Read the bank from payment.paymentMethodId if you need it for display.

// v1
if (payment.rail.startsWith('zelle')) { /* ... */ }

// v2
if (payment.rail === 'zelle') {
const bank = payment.paymentMethodId; // 'zelle-chase' | 'zelle-bofa' | 'zelle-citi' | null
}

Historic payments created before the cutover keep their original stored rail values in your records; only new payments are affected.

Removed @zkp2p/pay-shared exports​

The following Zelle-variant helpers were removed. They existed to support the retired per-bank rails and have no v2 equivalent:

  • ZELLE_QUOTE_PLATFORMS
  • expandZelleQuotePlatforms
  • zelleCompatibleMethods
  • SUPPORTED_ZELLE_RAIL_VARIANTS
  • isSupportedZelleRailVariant

If you validated bank picks with isSupportedZelleRailVariant, use the new ZELLE_PAYMENT_METHOD_IDS set instead.

Renamed export​

  • ZELLE_VARIANT_ACTION_SUFFIX -> ZELLE_METHOD_ACTION_SUFFIX (same keys and values; now keyed by paymentMethodId).

New in v2​

  • payment.paymentMethodId on payment objects (see TypeScript Types).
  • Create-payment accepts an optional paymentMethodId when rail is zelle (zelle-chase | zelle-bofa | zelle-citi). Hosted checkout sends it automatically; direct API integrations may pass it to preserve the customer's bank pick across sessions.
  • New @zkp2p/pay-shared helper: ZELLE_PAYMENT_METHOD_IDS.

No action needed if...​

  • You only use createCheckout / hosted checkout URLs and handle webhooks by order status: nothing in your integration observes Zelle bank variants.
  • You already treat rail values as opaque base-platform strings.

v0 to v1​

From the previous checkout integration (v0) to v1 (@zkp2p/pay-sdk@1.0.0 / @zkp2p/pay-shared@1.0.0). Use this section if your existing merchant integration still calls v0 SDK methods or handles v0 webhook events.

Summary​

  • The integration model is order-first in v1.
  • The canonical SDK creation method is createCheckout.
  • Fiat payments support multi-currency selection in checkout.
  • Webhook events moved from dotted lowercase names to uppercase lifecycle events.
  • Order state handling should use PAYMENT_SETTLED, PAYMENT_FAILED, PAYMENT_EXPIRED, and ORDER_FULFILLED; PAYMENT_EXPIRED is a customer payment-window event, not automatic order cancellation.
  • Crypto payout lifecycle is represented with PAYMENT_BRIDGE_* events.

New features in v1​

1. Referral split support​

Referral splits can be configured on the checkout flow and are applied automatically at settlement.

2. Partial order fulfillment​

  • Orders can now be partially settled and remain open with status PARTIALLY_FULFILLED.
  • Additional payment attempts can continue until the order reaches FULFILLED.
  • The checkout experience supports completing the remaining amount in follow-up attempts.

3. Fee modes (MERCHANT vs PAYEE)​

feePayer controls quote behavior and settlement accounting:

  • MERCHANT:
    • Buyer pays the displayed fiat amount.
    • Merchant absorbs fees.
  • PAYEE:
    • Buyer pays enough to cover both requested net and fees.
    • Merchant receives net settlement amount.

4. Custom webhook headers​

Webhook endpoints support up to 10 merchant-defined custom headers.

Feature changes​

Previous Integration (v0)Current Integration (v1)Migration Action
Session-first flow (CheckoutSession, then order)Order-first flow (CheckoutOrder, payments attached to order)Store and reconcile by order.id
Exact-token and exact-fiat checkout modesXOR create request (requestedUsdcAmount OR requestedFiatAmount + requestedFiatCurrency)Migrate request payload builders to XOR amount mode
Session-level fiat currency defaultsMerchant config defaultPaymentCurrency + checkout geolocation fallbackMove default currency logic to merchant config + checkout
enabledPaymentPlatforms request fieldenabledRails request fieldRename the request field
metadata request fieldnotes request fieldRename and update payload shape (Record<string, unknown>)
URL shape /checkout?session=...&token=...URL shape /?order=...&token=...Update any URL parsing or validation logic

SDK integration changes​

Canonical SDK methods​

Use these as the only public integration methods:

  • createCheckout
  • getMerchant
  • getCheckoutUrl
  • redirectToCheckout
  • createCheckoutAndRedirect

If your integration still uses createCheckoutSession or createSessionAndRedirect, migrate to the v1 methods above.

Method mapping​

Previous SDK (v0)Current SDK (v1)
createCheckoutSession(params, opts)createCheckout(params, opts)
createSessionAndRedirect(params, opts)createCheckoutAndRedirect(params, opts)
getMerchant(merchantId, opts)getMerchant(opts)
getCheckoutUrl(sessionId, opts, sessionToken)getCheckoutUrl(orderId, orderToken, opts)
redirectToCheckout(sessionId, sessionToken, opts)redirectToCheckout(orderId, orderToken, opts)

Request parameter mapping (createCheckoutSession to createCheckout)​

Previous field (v0)Current field (v1)Notes
merchantIdRemovedMerchant context comes from apiKey
amountUsdcrequestedUsdcAmountRequired in v1
recipientAddressdestinationAddressOptional in v1 (merchant defaults can apply)
destinationChainIddestinationChainIdSame meaning
destinationTokendestinationTokenSame meaning (alias/address support)
enabledPaymentPlatformsenabledRailsRename
metadatanotesRename + broader value type
successUrlsuccessUrlSame field; nullable in API shape
cancelUrlcancelUrlSame field; nullable in API shape
checkoutModeRemovedNo exact-fiat request mode in v1 API
fiatAmount + fiatCurrencyrequestedFiatAmount + requestedFiatCurrencyFiat create mode in v1
maxFeePercentageRemovedConfigure fee limits from the Merchant Dashboard settings

Response mapping​

Previous response (v0)Current response (v1)
session.idorder.id
sessionTokenorderToken
checkoutUrlcheckoutUrl (still returned)
bridgeBridge lifecycle represented in webhook events (PAYMENT_BRIDGE_*)

API endpoint mapping​

Previous endpoint (v0)Current endpoint (v1)
POST /api/checkout/sessionsPOST /api/v1/orders
GET /api/merchants/{merchantId}GET /api/v1/merchants/me

Before and after SDK example​

// v0
import { createCheckoutSession } from '@zkp2p/pay-sdk';

const session = await createCheckoutSession(
{
merchantId: 'merchant_123',
amountUsdc: '50.00',
destinationChainId: 8453,
destinationToken: 'USDC',
recipientAddress: '0xYourWalletAddress',
metadata: { orderId: 'order_123' },
},
{ apiBaseUrl: 'https://api.pay.peer.xyz', apiKey: process.env.ZKPAY_API_KEY! }
);
// v1
import { createCheckout } from '@zkp2p/pay-sdk';

const checkout = await createCheckout(
{
requestedFiatAmount: '50.00',
requestedFiatCurrency: 'EUR',
destinationChainId: 8453,
destinationToken: 'USDC',
destinationAddress: '0xYourWalletAddress',
notes: { orderId: 'order_123' },
},
{ apiBaseUrl: 'https://api.pay.peer.xyz', apiKey: process.env.ZKPAY_API_KEY! }
);

Webhook changes​

Event name migration​

Previous event (v0)Current event(s) (v1)Notes
order.createdORDER_CREATEDOrder lifecycle creation
order.payment_sentNo direct equivalent (PAYMENT_CREATED is the closest lifecycle signal)v1 does not expose a dedicated payment_sent event
order.fulfilledPAYMENT_SETTLED and ORDER_FULFILLEDSubscribe to both for final success handling
order.failedPAYMENT_FAILEDPayment failure signal; usually terminal, though a stalled crypto attempt can still settle late
order.expiredPAYMENT_EXPIREDCustomer payment-window expiration signal; cancellation flows can also emit cancel events
session.completedORDER_FULFILLEDUse ORDER_FULFILLED as the canonical checkout-complete event
session.startedORDER_CREATEDUse ORDER_CREATED as the canonical checkout-start event
session.abandonedNo equivalentOrders no longer expire
payout.submittedPAYMENT_BRIDGE_SUBMITTEDCrypto payout/bridge lifecycle
payout.completedPAYMENT_BRIDGE_COMPLETEDCrypto payout complete
payout.failedPAYMENT_BRIDGE_FAILEDCrypto payout failed

Payload shape migration​

Previous payload path (v0)Current payload path (v1)
data.session.iddata.order.id (and data.payment.orderId when payment is present)
data.session.statusdata.order.status and data.payment.status
data.order.paymentPlatformdata.payment.rail
data.order.fiatCurrencydata.payment.currency
data.order.fulfillTxdata.payment.fulfillTransaction
data.order.errorCode / data.order.errorMessagedata.payment.errorMessage (nullable; populated on PAYMENT_FAILED). data.payment.errorCode is present in the payload but always null. Use PAYMENT_EXPIRED for payment-window expiry and data.payment.status for reconciliation
Payout info on order/session objectsdata.paymentBridge on PAYMENT_BRIDGE_* events
[
"PAYMENT_SETTLED",
"PAYMENT_FAILED",
"PAYMENT_EXPIRED",
"ORDER_CREATED",
"ORDER_FULFILLED",
"ORDER_CANCELLED",
"PAYMENT_CREATED",
"PAYMENT_CANCELLED",
"PAYMENT_BRIDGE_PENDING",
"PAYMENT_BRIDGE_SUBMITTED",
"PAYMENT_BRIDGE_COMPLETED",
"PAYMENT_BRIDGE_FAILED"
]

For merchant order state transitions, treat PAYMENT_SETTLED, PAYMENT_FAILED, PAYMENT_EXPIRED, and ORDER_FULFILLED as the primary event set. Fiat attempts emit PAYMENT_EXPIRED after their one-hour window. An abandoned live Relay attempt ends in PAYMENT_FAILED; unfunded Zcash and sandbox crypto can emit PAYMENT_EXPIRED. These events do not auto-cancel the order, and eligible late settlements can still produce PAYMENT_SETTLED and ORDER_FULFILLED. See the provider-specific payment windows.

Additional v1 integrator features​

  • For iframe-based checkout, use SDK embedded helpers from @zkp2p/pay-sdk/embedded plus parent-window checkout.success / checkout.failed events.
  • For crypto payout tracking, subscribe to bridge lifecycle webhooks: PAYMENT_BRIDGE_PENDING, PAYMENT_BRIDGE_SUBMITTED, PAYMENT_BRIDGE_COMPLETED, PAYMENT_BRIDGE_FAILED.

v1 checklist​

  1. Replace createCheckoutSession with createCheckout.
  2. Replace createSessionAndRedirect with createCheckoutAndRedirect.
  3. Update getMerchant calls to getMerchant(opts).
  4. Rename request fields: amountUsdc -> requestedUsdcAmount, recipientAddress -> destinationAddress, metadata -> notes, enabledPaymentPlatforms -> enabledRails.
  5. Update create-order payload builders to use XOR amount mode (requestedUsdcAmount OR requestedFiatAmount + requestedFiatCurrency).
  6. Update webhook subscriptions from dotted v0 names to uppercase v1 event names.
  7. Update webhook handlers to use data.order, data.payment, and data.paymentBridge.
  8. Verify one happy-path payment (PAYMENT_SETTLED + ORDER_FULFILLED), one failed path, one payment-window expiry path, and one bridge payout path (PAYMENT_BRIDGE_*).