The API is the same shape on all three: configure the environment once, check that the SDK can present the order, mount it, handle five events, tear down. Only the install and the platform setup differ.
Stripe orders (Apple Pay or card) and Coinbase Apple Pay run only in an iOS app, through the iOS SDK or the React Native SDK, and Coinbase needs iOS 16 or later. See Apple Pay → Stripe and Apple Pay → Coinbase.
Environments
Configure the environment your backend created the order in, once, before you check or mount an order. The SDK defaults to sandbox, so a production build must configure
production.
A Stripe order created in the other environment reports unsupported, and mounting it fails.
Check before you mount
CallMeld.capabilities(order) (Meld.capabilities(for: order) on iOS) before you mount the order, and offer another method when surface is unsupported.
Gate on surface, not on embeddable. On iOS and React Native, Apple Pay and Stripe orders report embeddable: false because the SDK presents them itself, not because they are unsupported. They also report requiresUserGesture: true, so mount them from the customer’s tap.
The five events
onReady and onPaymentSubmitted are lifecycle moments, not statuses. Where a provider never reports readiness, the SDK synthesises onReady so your spinner still clears.
onPaymentSubmitted fires exactly once per mount, however the provider reports the end of the payment, so you need no guard of your own. A failure or a cancellation closes it, so a failed payment is never followed by a submission.
On iOS, each mount ends with exactly one terminal callback. That is onPaymentSubmitted, onCancel, or onError with recoverable: false, and nothing follows it. This holds in the React Native SDK on iOS too. If you unmount before a terminal callback, you get none, and the order’s outcome is whatever your backend reports. Android does not enforce this yet, so ignore anything that arrives after the first terminal callback there.
Status
onStatusChange is the one event that carries state. It delivers a status from a fixed set, whichever provider the order routed to:
Providers each have their own vocabulary —
paid, order_completed, succeeded all arrive as completed — and the SDK maps them before you see them, so you never write a per-provider status table. The raw code rides along as providerStatus for your logs.
onCancel and onError are derived from the same statuses. A failed status fires onStatusChange and onError; a cancelled status fires onStatusChange and onCancel. Handle a terminal state in one place or the other, not both, or you will run it twice.
onError also fires for a failure that never reached the provider, such as its surface failing to load. An embedded surface can report that with recoverable: true, which is not terminal: the customer can carry on with the same order. On iOS, an Apple Pay or Stripe order instead ends the mount with a non-recoverable error, such as PRESENTATION_FAILED or APPLE_PAY_UNAVAILABLE.
completed has no dedicated handler — it arrives through onStatusChange only.
It is also not the signal to act on. Drive your flow from onPaymentSubmitted, which fires once whether or not the provider reports a status, and read completed as the provider’s own view of its order.
None of this is settlement.
completed is the provider’s view of its own order; the money moving is Meld’s TRANSACTION_CRYPTO_COMPLETE webhook to your backend. Credit users there and nowhere else.