v2 to v3 Migration Guide
This guide covers what changed between v2 (@zkp2p/pay-sdk@2.0.0 /
@zkp2p/pay-shared@2.0.0) and v3 (@zkp2p/pay-sdk@3.0.0 /
@zkp2p/pay-shared@3.0.0).
Required SDK Upgrade
npm install @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. Its @zkp2p/pay-shared
dependency is now ^3.0.0.
Every break in v3 is compile-time. Upgrade both packages together and typecheck; TypeScript will point at each site that needs attention.
Breaking Changes — @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, because 'checkout.closed' is not never:
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 will silently ignore
checkout.closed, leaving 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 });
This is not a new restriction. The merchant has always been determined by the
API key, and the extra merchantId was discarded — so v2 code passing one was
already getting the API key's merchant, not the one it named. The v0→v1 guide
documented the overload as removed; it survived in code until now. If you were
relying on it to fetch a different merchant, it never did that.
Response aliases removed
Three aliases are gone. They collided by name across the two packages while
resolving to different shapes, which made CheckoutCreateResponse mean
different things depending on where you imported it from.
| 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 previously accepted a raw JSON body with no success property, and fell
back to returning the whole envelope when it carried no responseObject. Both
paths are gone — the SDK now requires a well-formed
{ success, message, responseObject, statusCode } envelope and throws otherwise.
The Pay API has always sent that envelope on every endpoint the SDK calls, so
this changes nothing against a real API. It matters only if you point the SDK at
a mock, proxy, or fixture that returns bare JSON — those now throw
Malformed API response envelope instead of silently handing back a wrongly
typed object.
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 — @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 };
Legacy merchant-migration exports removed
These were internal to the retired merchant-migration app and the dual-Privy setup, both of which have been removed. They have no v3 replacement — delete any references:
DashboardPrivyAppDashboardPrivyAppTypeMerchantMigrationStatusMerchantMigrationStatusType
New in v3
The checkout.closed event itself
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 no payment rail has liquidity for the amount — it shows a Go Back button that posts:
{
"channel": "zkp2p_checkout_embed_v1",
"type": "checkout.closed",
"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.
Purely additive
Nothing in this section requires a change to existing code.
Dynamic order resize types
CheckoutNearbySuggestion and CheckoutNearbySuggestions are now re-exported
from @zkp2p/pay-sdk, describing the nearby-amount suggestions offered to
dynamic-orders merchants when an exact amount has no liquidity.
Additional shared utilities
@zkp2p/pay-shared also gained address validators (isValidSolanaAddress,
isValidTronAddress, base58 helpers), merchant onboarding step types, and
payee referral fee-cap helpers.
Upgrade Checklist
- Install
@zkp2p/pay-sdk@3.0.0and@zkp2p/pay-shared@3.0.0together. - 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. - Typecheck. Every breaking change in v3 surfaces at compile time.