How it works
1
Register your website's domain
One-time setup. Ionic only allows registered domains to embed checkout.
2
Create a session on your server
POST /v1/checkout/sessions with ui_mode: "embedded", using your secret
key. Compute prices server-side.3
Mount the checkout on your page
The browser SDK frames the session and manages the secure handshake,
sizing, and lifecycle callbacks. React apps use the
<CheckoutEmbed>
component; everything else uses the JavaScript SDK directly.4
Fulfill on the webhook
Fulfill after a verified
checkout.session.completed webhook. The browser callback
is a UX signal only.1. Register your website’s domain
Register each origin that will embed checkout. Until an origin is registered, the frame refuses to render on it — this is what prevents another site from embedding checkout as your store.domain field accepts a bare domain (shop.example.com, treated as
https://) or a full origin with scheme and port. For local development,
register your localhost origin — http:// is allowed for localhost only:
2. Create a session on your server
Create the session from your server with your secret key. Always compute prices server-side; never trust amounts sent from the browser.
The
client_secret is scoped to this single session and expires with it. It’s
safe to send to the buyer’s browser — don’t log it or reuse it across
sessions.
3. Mount the checkout on your page
Three ways in, one integration underneath. All three load the same runtime fromhttps://js.ionicfi.com/v1/ionic.js at mount time — the card-handling
code is never bundled into your app. The /v1 URL stays current, so checkout
updates reach your integration without requiring an SDK upgrade.
- React
- JavaScript
- Script tag
<CheckoutEmbed>:wallets={[]} for card only, or a specific list to restrict the offer. See
Apple Pay below.Handling errors
Recoverable problems — network blips, card declines — keep their own retry UI inside the frame and never reach your code. What does reach youronError
carries a terminal flag, and that flag is what you branch on, not the code:
expired, not_found, init_timeout,
payment_failed, tokenization_failed (all terminal) and checkout_timeout
(not terminal). New codes can appear as the product grows — an unrecognized
code still carries terminal, so handlers written against the flag keep
working.
terminal is a statement about this mounted frame, never about the payment.
A payment can succeed and the frame still die afterward — fulfillment stays
keyed to the webhook regardless of what onError reports.
In React, recovering from a terminal error is a remount: create a new session
on your server and pass it down (a changed session remounts on its own), or
bump a key on <CheckoutEmbed> to retry the same session after a load
failure:
4. Fulfill on the webhook
Fulfill from the verifiedcheckout.session.completed webhook delivered to
your server, idempotently, keyed by the session id. See
Webhooks for receiver setup — on Node,
webhooks.unwrap() from @ionicfi/sdk verifies and types the event in one
call.
To check a payment without waiting for a webhook — on your confirmation page’s
server route, or in local development where webhooks cannot reach you — read
the session directly:
status: "complete" with payment_status: "paid" means the payment
succeeded. The webhook drives fulfillment; the read answers whether one
specific session paid, on demand.
Local development
Ionic cannot deliver webhooks tolocalhost. During local development,
either retrieve the session as shown above, or expose your receiver with a
tunnel (for example ngrok http 3000) and register the tunnel URL as a
webhook endpoint. Remember to update the endpoint URL when the tunnel
address changes.
Handling declines
A declined card keeps the sessionopen and the form interactive — the
buyer sees the decline inside the frame and can retry with another card. No
onError fires; a decline is not a terminal state.
To see why a payment declined, follow the session to its payment intent:
Content Security Policy
If your page sends aContent-Security-Policy header, it must allow the
checkout to load. A blocked script leaves a page with no payment form, so check
this before debugging anything else. The same directives apply to all three
integration paths — the npm packages load the runtime from the CDN too.
A minimal policy for a page that embeds checkout:
Take
frame-src from the embed_url in your create-session response rather
than copying a hostname from these docs. The value is issued per session, and
reading it from the response keeps your policy correct if it changes.Testing
Use the test card numbers in test mode. Two behaviors worth knowing before your first test purchase:- Repeating the same card and amount within about 20 minutes is declined as a duplicate. Vary the amount between repeated test runs.
- Any amount under $1.00 always declines — useful for testing your decline handling.
Apple Pay
Eligible buyers see an Apple Pay button above the card form, in your page — never inside the frame — with an “Or pay with card” divider. Wallet payments complete through the sameonComplete and the same webhook as card payments.
Eligibility is decided server-side per session (device capability, domain
registration, amount and currency), so you never compute it in the browser:
- React: wallets are offered automatically. Restrict them with the
walletsprop —wallets={["apple_pay"]}offers only Apple Pay,wallets={[]}is card only. When a requested wallet can’t be offered, the form still renders and the console names the exact setup step that’s missing. - JavaScript / script tag: pass
applePay: truetomountSession.

