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.
| Old | New |
|---|---|
CASHOUT_CURRENCY_INFO | PAYOUT_CURRENCY_INFO |
CashoutCurrencyType | PayoutCurrencyType |
CashoutOracleCurrencyType | PayoutOracleCurrencyType |
CashoutCurrency | PayoutCurrency |
CASHOUT_ORACLE_CURRENCIES | PAYOUT_ORACLE_CURRENCIES |
CASHOUT_RAIL_CURRENCIES | PAYOUT_RAIL_CURRENCIES |
CASHOUT_CURRENCY_NAMES | PAYOUT_CURRENCY_NAMES |
CASHOUT_CURRENCY_SYMBOLS | PAYOUT_CURRENCY_SYMBOLS |
CashoutFiatValue | PayoutFiatValue |
isCashoutCurrency | isPayoutCurrency |
cashoutRailPaysCurrency | payoutRailPaysCurrency |
isCashoutMultiCurrencyRail | isPayoutMultiCurrencyRail |
cashoutFiatCents | payoutFiatCents |
formatCashoutFiatCents | formatPayoutFiatCents |
formatCashoutRate | formatPayoutRate |
formatCashoutFiatValue | formatPayoutFiatValue |
isCashoutSarRail | isPayoutSarRail |
CASHOUT_CRYPTO_RAILS | PAYOUT_CRYPTO_RAILS |
isCashoutCryptoRail | isPayoutCryptoRail |
cashoutCryptoRailChainId | payoutCryptoRailChainId |
hasCashoutCryptoRail | hasPayoutCryptoRail |
cashoutAccountLabel | payoutAccountLabel |
CASHOUT_FUNDING_CHAIN_IDS | PAYOUT_FUNDING_CHAIN_IDS |
getCashoutFundingTokenConfigs | getPayoutFundingTokenConfigs |
CASHOUT_WEBHOOK_EVENT_TYPES | PAYOUT_WEBHOOK_EVENT_TYPES |
CashoutWebhookEventTypeValue | PayoutWebhookEventTypeValue |
isCashoutWebhookEventType | isPayoutWebhookEventType |
CASHOUT_WEBHOOK_VERSION | PAYOUT_WEBHOOK_VERSION |
CashoutPartialPayment | PayoutPartialPayment |
CashoutWebhookEventData | PayoutWebhookEventData |
CashoutWebhookPayload | PayoutWebhookPayload |
isCashoutWebhook | isPayoutWebhook |
CashoutStatus | PayoutStatus |
CashoutStatusType | PayoutStatusType |
CashoutFundingStatus | PayoutFundingStatus |
CashoutFundingStatusType | PayoutFundingStatusType |
CashoutCancelSource | PayoutCancelSource |
CashoutCancelSourceType | PayoutCancelSourceType |
CashoutPayoutKind | PayoutKind |
CashoutPayoutKindType | PayoutKindType |
CashoutAttemptStatus | PayoutAttemptStatus |
CashoutAttemptStatusType | PayoutAttemptStatusType |
CashoutFiatRail | PayoutFiatRail |
CashoutFiatRailType | PayoutFiatRailType |
CashoutCryptoRail | PayoutCryptoRail |
CashoutCryptoRailType | PayoutCryptoRailType |
CashoutRail | PayoutRail |
CashoutRailType | PayoutRailType |
CashoutSarRailType | PayoutSarRailType |
CashoutPayoutProvider | PayoutProvider |
CashoutPayoutProviderType | PayoutProviderType |
CashoutCryptoDestination | PayoutCryptoDestination |
CashoutCryptoMethod | PayoutCryptoMethod |
CashoutPartialFillView | PayoutPartialFillView |
CashoutSettlementBreakdown | PayoutSettlementBreakdown |
CashoutView | PayoutView |
CreateCashoutRequest | CreatePayoutRequest |
CreateCashoutResponse | CreatePayoutResponse |
ListCashoutsResponse | ListPayoutsResponse |
CashoutTimelineEventType | PayoutTimelineEventType |
CashoutTimelineEventTypeValue | PayoutTimelineEventTypeValue |
CASHOUT_PAYOUT_STEP_MAX_USDC | PAYOUT_STEP_MAX_USDC |
CashoutPayoutStepRange | PayoutStepRange |
CashoutSettings | PayoutSettings |
CashoutFundingIssueKind | PayoutFundingIssueKind |
CashoutFundingIssueKindType | PayoutFundingIssueKindType |
CashoutFundingIssueResolution | PayoutFundingIssueResolution |
CashoutFundingIssueResolutionType | PayoutFundingIssueResolutionType |
CashoutFundingTokenOption | PayoutFundingTokenOption |
CashoutSettingsView | PayoutSettingsView |
MerchantCashoutFundingIssue | MerchantPayoutFundingIssue |
CashoutPayoutMethodView | PayoutMethodView |
MerchantCashoutView | MerchantPayoutView |
CashoutTimelineEntry | PayoutTimelineEntry |
CashoutWebhookDeliveryView | PayoutWebhookDeliveryView |
MerchantCashoutDetail | MerchantPayoutDetail |
CreateMerchantCashoutResponse | CreateMerchantPayoutResponse |
ListMerchantCashoutsResponse | ListMerchantPayoutsResponse |
CashoutClientOptions | PayoutClientOptions |
CreateCashoutOptions | CreatePayoutOptions |
ListCashoutsParams | ListPayoutsParams |
cancelCashout | cancelPayout |
createCashout | createPayout |
getCashout | getPayout |
listCashouts | listPayouts |
Routes and JSON fields
| Surface | Old | New |
|---|---|---|
| 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:
| v1 | v2 |
|---|---|
CASHOUT_ORDER_CREATED | PAYOUT_ORDER_CREATED |
CASHOUT_ORDER_CANCELLED | PAYOUT_ORDER_CANCELLED |
CASHOUT_ORDER_PARTIALLY_FUNDED | PAYOUT_ORDER_PARTIALLY_FUNDED |
CASHOUT_ORDER_FUNDED | PAYOUT_ORDER_FUNDED |
CASHOUT_ORDER_MATCHED | PAYOUT_ORDER_MATCHED |
CASHOUT_ORDER_PARTIALLY_PAID | PAYOUT_ORDER_PARTIALLY_PAID |
CASHOUT_ORDER_SETTLED | PAYOUT_ORDER_SETTLED |
CASHOUT_ORDER_EXPIRED | PAYOUT_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.
| Old | New |
|---|---|
CASHOUTS_DISABLED | PAYOUTS_DISABLED |
CASHOUT_FUNDING_AMOUNT_TOO_LOW | PAYOUT_FUNDING_AMOUNT_TOO_LOW |
CASHOUT_FUNDING_DETECTED | PAYOUT_FUNDING_DETECTED |
CASHOUT_FUNDING_TOKEN_UNSUPPORTED | PAYOUT_FUNDING_TOKEN_UNSUPPORTED |
CASHOUT_MERCHANT_WALLET_MISSING | PAYOUT_MERCHANT_WALLET_MISSING |
CASHOUT_NOT_CANCELLABLE | PAYOUT_NOT_CANCELLABLE |
CASHOUT_NOT_FOUND | PAYOUT_NOT_FOUND |
CASHOUT_NO_RAILS_AVAILABLE | PAYOUT_NO_RAILS_AVAILABLE |
CASHOUT_PAYOUT_ABOVE_MAX | PAYOUT_ABOVE_MAX |
CASHOUT_PAYOUT_NOT_STEP_MULTIPLE | PAYOUT_NOT_STEP_MULTIPLE |
CASHOUT_PAYOUT_TOKEN_UNSUPPORTED | PAYOUT_TOKEN_UNSUPPORTED |
CASHOUT_PRIVY_WALLET_FAILED | PAYOUT_PRIVY_WALLET_FAILED |
CASHOUT_RELAY_QUOTE_FAILED | PAYOUT_RELAY_QUOTE_FAILED |
CASHOUT_RELAY_STATUS_FAILED | PAYOUT_RELAY_STATUS_FAILED |
CASHOUT_REQUESTED_RAILS_UNAVAILABLE | PAYOUT_REQUESTED_RAILS_UNAVAILABLE |
CASHOUT_ROUTE_UNQUOTABLE | PAYOUT_ROUTE_UNQUOTABLE |
CASHOUT_SANDBOX_UNSUPPORTED | PAYOUT_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 command | New command |
|---|---|
cashouts | payouts |
cashouts create | payouts create |
cashouts get | payouts get |
cashouts list | payouts list |
cashouts cancel | payouts cancel |
cashouts configure-local | payouts configure-local |
cashouts simulate-funding | payouts simulate-funding |
cashouts sweep-local | payouts sweep-local |
cashouts simulate-credential | payouts simulate-credential |
cashouts resolve-send | payouts resolve-send |
cashouts simulate-payout-quote | payouts simulate-payout-quote |
cashouts simulate-payout-bridge | payouts simulate-payout-bridge |
cashouts simulate-payment | payouts simulate-payment |
cashouts simulate-peer-withdraw | payouts simulate-peer-withdraw |
cashouts simulate-wallet-balance | payouts simulate-wallet-balance |
cashouts emails | payouts emails |
cashouts funding-issues | payouts funding-issues |
cashouts resolve-funding-issue | payouts resolve-funding-issue |
cashouts timeline | payouts timeline |
local admin cashout | local admin payout |
local admin cashout-settings | local 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
idand stored ids as opaque deduplication keys. An id such ascashout_<storedId>_CASHOUT_ORDER_SETTLEDkeeps that spelling; do not infer the public event type from it, rewrite it, or reset deduplication records. - The player API remains
/api/v1/cashout-checkoutwithx-cashout-token, and the hosted player URL remains/c/:id. Types such asCashoutCheckoutState,CashoutPayoutQuoteandSetCashoutPayoutMethodRequestkeep their names. - Peer admin
/api/v1/admin/cashoutsroutes andAdminCashout*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
-
WebhookPayloadisOrderWebhookPayload | CashoutWebhookPayload. Narrow before readingdata.order.isCashoutWebhookis 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.
}
} -
WebhookEventTypeValueincludes 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_SETTLEDandCASHOUT_ORDER_EXPIRED. Update exhaustive switches, or useOrderWebhookEventTypeValuefrom@zkp2p/pay-sharedfor order-only handlers.
New
createCashout,getCashout,listCashoutsandcancelCashout, withCashoutClientOptions,CreateCashoutOptionsandListCashoutsParams: see SDK cashouts.- Typed cashout views, lifecycle enums and webhooks, including
CASHOUT_WEBHOOK_VERSIONandCashoutPartialPayment: 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
-
CheckoutOrderis a union onamountMode. Narrow before reading amounts:if (order.amountMode === 'OPEN' && order.requestedUsdcAmount === null) {
// The buyer has not chosen an amount yet.
} -
createCheckoutresults withidempotentReplay: truehavecheckoutUrl: null; keep the URL from the first call. -
feePayercan be'SPLIT'(withbuyerFeeShareBps); handle it wherever you switch onFeePayerType. -
CheckoutAggregate.orderis aHostedCheckoutOrder. Narrow onamountModethe same way. -
PaymentDeeplinkResponse.intentHashandproofSubmissionUrlcan benull. -
CreatePaymentRequesttakesamount,amountCurrencyandexpectedAmountVersionall together or not at all. -
New required fields matter only if you build these objects yourself, for example in test fixtures:
CheckoutPayment.penalties;buyerFeeShareBpsonCheckoutOrderandMerchantConfig;MerchantConfig.applePayAvailable,masterMerchantEnabledandmasterMerchantMaxFeeBps;MerchantProfile.apiKeyIpAllowlistandmasterMerchantId; andMerchantUser.removalLocked. -
createCheckoutthrows 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, matchProvide exactly one of requestedUsdcAmount, requestedFiatAmount + requestedFiatCurrency, or openAmount.
New
createCheckout({ openAmount: { currency: 'USD', minAmount: '10', maxAmount: '2000', presets: ['25', '50', '100'] } }).- Webhook
ORDER_AMOUNT_SETwithdata.amountChange; subscribe to it explicitly. Credit the buyer onORDER_FULFILLED, as before. amountChange.fiat.currencyandopenAmount.savedInput.currencyare the buyer's selected checkout currency, which can differ fromopenAmount.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.
| v3 | v4 | |
|---|---|---|
| Type | 'FREE' | 'PRO' | MerchantTierName | null where MerchantTierName = 'BASE' | 'PRO' | 'CONCIERGE' |
Meaning of null | The merchant has not selected a plan. Live order creation is rejected with 403 MERCHANT_TIER_FORBIDDEN until one is chosen in the dashboard. | |
| Legacy mapping | FREE | null |
PRO | CONCIERGE (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.statusis nowPaymentBridgeStatusType(PENDING,SUBMITTED,COMPLETED,FAILED), exported with thePaymentBridgeStatusconstant.BridgeStatusResponse.statusis unchanged and keeps its own lowercase union:'pending' | 'completed' | 'failed' | 'not_applicable'.OnboardingStepState.idis nowOnboardingStepIdValueinstead ofstring, andOnboardingStepIdgainedPLAN. Code that assigned arbitrary strings toidno 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_CHARGEBACKEDORDER_PARTIALLY_CHARGEBACKEDORDER_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 helpersencodeBase58/doubleSha256next to the existingisValidTronAddress.- Integration types (
IntegrationStatusResponse,SandboxTestOrderRequest, ...) for the/api/v1/integrationendpoints.
v4 checklist
- Replace
tier === 'FREE'checks withtier === null; treatCONCIERGEas the formerPRO. - Replace message-string matching in
catchblocks withPayApiErrorfields. - 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.
| Removed | From | Replace with |
|---|---|---|
CheckoutCreateResponse | @zkp2p/pay-sdk | CreateCheckoutResult |
CheckoutCreateResponse | @zkp2p/pay-shared | CreateOrderResponse |
CreateCheckoutResponse | @zkp2p/pay-shared | CreateOrderResponse |
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:
DashboardPrivyAppDashboardPrivyAppTypeMerchantMigrationStatusMerchantMigrationStatusType
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.
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.
CheckoutNearbySuggestionandCheckoutNearbySuggestionsare 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-sharedgained address validators (isValidSolanaAddress,isValidTronAddress, base58 helpers), merchant onboarding step types, and referral fee-cap helpers.
v3 checklist
- Add a
checkout.closedarm to any exhaustiveswitchoverEmbedCheckoutEventType, and a handler that closes the iframe if you embed checkout. A non-exhaustiveifchain still compiles but silently strands the customer. - Drop the first argument from any
getMerchant(merchantId, options)call. - Replace
CheckoutCreateResponse/CreateCheckoutResponsewithCreateCheckoutResult(SDK) orCreateOrderResponse(shared). - If you point the SDK at a mock or proxy, make sure it returns the full
{ success, message, responseObject, statusCode }envelope. - Replace reads of
MerchantProfile.migrationStatus, and addintegrationPath,onboardingCompletedAtandonboardingSkippedAtto anyMerchantProfilefixtures or mocks you construct. - Search your codebase for
WebhookWithSecretand replace it. - Update webhook-creation handling to read
response.webhook.*instead ofresponse.*, keepingresponse.secretas is. - Delete any references to
DashboardPrivyApporMerchantMigrationStatus.
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.railis alwayszellefor Zelle payments, in API responses, checkout aggregates, and webhook payloads.- The customer's bank pick is exposed separately as the new
payment.paymentMethodIdfield, typedstring | null(currentlyzelle-chase|zelle-bofa|zelle-citi,nullfor non-Zelle rails). It is display metadata, not a rail. enabledRailsandpreselectedMethodcontinue 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_PLATFORMSexpandZelleQuotePlatformszelleCompatibleMethodsSUPPORTED_ZELLE_RAIL_VARIANTSisSupportedZelleRailVariant
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 bypaymentMethodId).
New in v2
payment.paymentMethodIdon payment objects (see TypeScript Types).- Create-payment accepts an optional
paymentMethodIdwhenrailiszelle(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-sharedhelper: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
railvalues 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, andORDER_FULFILLED;PAYMENT_EXPIREDis 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 modes | XOR create request (requestedUsdcAmount OR requestedFiatAmount + requestedFiatCurrency) | Migrate request payload builders to XOR amount mode |
| Session-level fiat currency defaults | Merchant config defaultPaymentCurrency + checkout geolocation fallback | Move default currency logic to merchant config + checkout |
enabledPaymentPlatforms request field | enabledRails request field | Rename the request field |
metadata request field | notes request field | Rename 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:
createCheckoutgetMerchantgetCheckoutUrlredirectToCheckoutcreateCheckoutAndRedirect
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 |
|---|---|---|
merchantId | Removed | Merchant context comes from apiKey |
amountUsdc | requestedUsdcAmount | Required in v1 |
recipientAddress | destinationAddress | Optional in v1 (merchant defaults can apply) |
destinationChainId | destinationChainId | Same meaning |
destinationToken | destinationToken | Same meaning (alias/address support) |
enabledPaymentPlatforms | enabledRails | Rename |
metadata | notes | Rename + broader value type |
successUrl | successUrl | Same field; nullable in API shape |
cancelUrl | cancelUrl | Same field; nullable in API shape |
checkoutMode | Removed | No exact-fiat request mode in v1 API |
fiatAmount + fiatCurrency | requestedFiatAmount + requestedFiatCurrency | Fiat create mode in v1 |
maxFeePercentage | Removed | Configure fee limits from the Merchant Dashboard settings |
Response mapping
| Previous response (v0) | Current response (v1) |
|---|---|
session.id | order.id |
sessionToken | orderToken |
checkoutUrl | checkoutUrl (still returned) |
bridge | Bridge lifecycle represented in webhook events (PAYMENT_BRIDGE_*) |
API endpoint mapping
| Previous endpoint (v0) | Current endpoint (v1) |
|---|---|
POST /api/checkout/sessions | POST /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.created | ORDER_CREATED | Order lifecycle creation |
order.payment_sent | No direct equivalent (PAYMENT_CREATED is the closest lifecycle signal) | v1 does not expose a dedicated payment_sent event |
order.fulfilled | PAYMENT_SETTLED and ORDER_FULFILLED | Subscribe to both for final success handling |
order.failed | PAYMENT_FAILED | Payment failure signal; usually terminal, though a stalled crypto attempt can still settle late |
order.expired | PAYMENT_EXPIRED | Customer payment-window expiration signal; cancellation flows can also emit cancel events |
session.completed | ORDER_FULFILLED | Use ORDER_FULFILLED as the canonical checkout-complete event |
session.started | ORDER_CREATED | Use ORDER_CREATED as the canonical checkout-start event |
session.abandoned | No equivalent | Orders no longer expire |
payout.submitted | PAYMENT_BRIDGE_SUBMITTED | Crypto payout/bridge lifecycle |
payout.completed | PAYMENT_BRIDGE_COMPLETED | Crypto payout complete |
payout.failed | PAYMENT_BRIDGE_FAILED | Crypto payout failed |
Payload shape migration
| Previous payload path (v0) | Current payload path (v1) |
|---|---|
data.session.id | data.order.id (and data.payment.orderId when payment is present) |
data.session.status | data.order.status and data.payment.status |
data.order.paymentPlatform | data.payment.rail |
data.order.fiatCurrency | data.payment.currency |
data.order.fulfillTx | data.payment.fulfillTransaction |
data.order.errorCode / data.order.errorMessage | data.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 objects | data.paymentBridge on PAYMENT_BRIDGE_* events |
Recommended v1 webhook subscriptions
[
"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/embeddedplus parent-windowcheckout.success/checkout.failedevents. - For crypto payout tracking, subscribe to bridge lifecycle webhooks:
PAYMENT_BRIDGE_PENDING,PAYMENT_BRIDGE_SUBMITTED,PAYMENT_BRIDGE_COMPLETED,PAYMENT_BRIDGE_FAILED.
v1 checklist
- Replace
createCheckoutSessionwithcreateCheckout. - Replace
createSessionAndRedirectwithcreateCheckoutAndRedirect. - Update
getMerchantcalls togetMerchant(opts). - Rename request fields:
amountUsdc->requestedUsdcAmount,recipientAddress->destinationAddress,metadata->notes,enabledPaymentPlatforms->enabledRails. - Update create-order payload builders to use XOR amount mode (
requestedUsdcAmountORrequestedFiatAmount+requestedFiatCurrency). - Update webhook subscriptions from dotted v0 names to uppercase v1 event names.
- Update webhook handlers to use
data.order,data.payment, anddata.paymentBridge. - Verify one happy-path payment (
PAYMENT_SETTLED+ORDER_FULFILLED), one failed path, one payment-window expiry path, and one bridge payout path (PAYMENT_BRIDGE_*).