Skip to main content
Embedded checkout renders the Ionic-hosted payment form inside an iframe on your page. Card details are collected inside the frame, so they never touch your page or your servers, and your buyer completes the purchase without leaving your site. If a hosted redirect works for your product, start with Checkout instead — it is less integration surface. Choose embedded when the buyer staying on your domain matters.

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.
The 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:
Domains registered with a test key apply to test mode only; register production domains again with your live key when you go live.

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.
Return exactly three response fields to your page: 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 from https://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.
Fetch the session from your server and hand it to <CheckoutEmbed>:
The component manages the iframe’s lifecycle for you: the frame holds the buyer’s in-progress card entry, so it is torn down and remounted only when the session itself changes — never because a parent re-render produced new callback identities. Passing a new session object with the same values does not remount.Digital wallets are offered automatically when the session is eligible; pass wallets={[]} for card only, or a specific list to restrict the offer. See Apple Pay below.
The frame sizes itself to its content and resizes as the buyer moves through the flow. Give the container your page’s content width. What the SDK handles, and what you handle:

Handling errors

Recoverable problems — network blips, card declines — keep their own retry UI inside the frame and never reach your code. What does reach your onError carries a terminal flag, and that flag is what you branch on, not the code:
Codes you may see today: 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

Do not fulfill orders from onComplete or from your confirmation page loading. Buyers close tabs after paying, and connections drop before callbacks run. Fulfillment triggered only from the browser will miss paid orders.
Fulfill from the verified checkout.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 to localhost. 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 session open 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 a Content-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:
Apple Pay needs two further origins, because the wallet button renders in your page rather than inside the frame. The Apple Pay guide lists them alongside the rest of the wallet setup.
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 same onComplete 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 wallets prop — 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: true to mountSession.
Because embedded checkout runs on your domain, Apple Pay needs a one-time domain setup (registration + an association file) that hosted surfaces don’t. The Apple Pay guide covers the requirements, the reasons behind them, and how to test on a real device.