Embedded Checkout
Available via: API (private key) + SDK (public key) ·
@inflow_pay/sdk1.10 or later
Overview
Embedded Checkout puts the complete Checkout V2 hosted page in an iframe on your site. The customer pays without leaving your page, and you get everything the hosted checkout already does:
- Email and billing country collection
- Order summary, coupons and tax
- Local currency for one-time payments
- Cards with 3D Secure, Apple Pay, Google Pay, and local bank transfer for one-time payments
- One-time payments and subscriptions
The iframe resizes itself to its content, so it blends into your page like a normal block.
| Hosted checkout | Embedded Checkout | SDK payment form | |
|---|---|---|---|
| Where customer pays | Inflow page (redirect) | Your page (iframe) | Your page (iframe) |
| UI you build | None | None | Everything except the payment form |
| Email, country, tax, summary | ✅ | ✅ | You build it |
| Apple Pay / Google Pay | ✅ | ✅ | ✅ (see SDK Checkout Methods) |
| Local bank transfer | ✅ (one-time) | ✅ (one-time) | ❌ |
| Subscriptions | ✅ | ✅ | Via API |
Prerequisites
- A verified domain (Settings → Domains, see Domains). Only pages on your verified domains (and their subdomains) can display the checkout. Embedded sessions are refused in production when your account has no verified domain. In sandbox, an account without verified domains can test from any page,
localhostincluded. - Your API keys: the private key on your server, the public key in your page. See Get Your API Keys.
If you set a
successUrl, it must also be on one of your verified domains.
How it works
sequenceDiagram
participant Page as Your page
participant Server as Your server
participant API as Inflow API
participant Iframe as Inflow checkout (iframe)
Page->>Server: Customer clicks "Pay"
Server->>API: Create session with uiMode "embedded"
API-->>Server: sessionId, clientSecret
Server-->>Page: clientSecret
Page->>Iframe: SDK mounts the checkout with clientSecret
Iframe-->>Page: onComplete
API-->>Server: checkout_session.completed webhook
Step 1: Create the session (backend)
Create a checkout session with uiMode: "embedded". The response contains a clientSecret: send it to your page, and only to the customer who will pay.
app.post('/api/checkout', async (req, res) => {
const response = await fetch('https://api.inflowpay.com/api/checkout/sessions/one-time-payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Inflow-Api-Key': process.env.INFLOW_PRIVATE_KEY,
},
body: JSON.stringify({
uiMode: 'embedded',
products: [{ name: 'Pro Plan', price: 4900, quantity: 1 }],
currency: 'EUR',
successUrl: 'https://shop.example.com/thanks',
// Optional, pre-fills the checkout:
customerEmail: req.body.email,
billingCountry: 'FR',
}),
});
const session = await response.json();
res.json({ clientSecret: session.clientSecret });
});For a subscription, create the session for an existing offer instead:
await fetch(`https://api.inflowpay.com/api/checkout/sessions/subscription/${subscriptionOfferId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Inflow-Api-Key': process.env.INFLOW_PRIVATE_KEY,
},
body: JSON.stringify({ uiMode: 'embedded', successUrl: 'https://shop.example.com/thanks' }),
});Every other session field from Checkout V2 (customerEmail, expiresIn, metadatas…) works the same way.
Response
{
"sessionId": "sess_1a2b3c",
"checkoutUrl": "https://checkout.inflowpay.com/embed/sess_1a2b3c",
"uiMode": "embedded",
"clientSecret": "sess_1a2b3c_secret_…"
}Keep the
clientSecretout of logs, URLs you share and analytics. It lets anyone display and pay this session. It is returned only once, at creation; Inflow stores only a hash.
Step 2: Display the checkout (frontend)
Install the SDK:
npm install @inflow_pay/sdkReact
import { useEffect, useState } from 'react';
import { InflowPayProvider, EmbeddedCheckout } from '@inflow_pay/sdk/react';
export function CheckoutPage() {
const [clientSecret, setClientSecret] = useState<string | null>(null);
useEffect(() => {
fetch('/api/checkout', { method: 'POST' })
.then((res) => res.json())
.then((data) => setClientSecret(data.clientSecret));
}, []);
if (!clientSecret) return null;
return (
<InflowPayProvider config={{ publicKey: 'inflow_pub_xxx' }}>
<EmbeddedCheckout
clientSecret={clientSecret}
onComplete={({ sessionId }) => console.log('Paid', sessionId)}
onError={(error) => console.error(error.code, error.message)}
/>
</InflowPayProvider>
);
}Vanilla JavaScript
<div id="checkout"></div>
<script src="https://cdn.jsdelivr.net/npm/@inflow_pay/sdk@1/dist/sdk.umd.js"></script>
<script>
const provider = new InflowPaySDK.InflowPayProvider({
config: { publicKey: 'inflow_pub_xxx' },
});
fetch('/api/checkout', { method: 'POST' })
.then((res) => res.json())
.then(({ clientSecret }) => {
const checkout = provider.initEmbeddedCheckout({
clientSecret,
onComplete: ({ sessionId }) => console.log('Paid', sessionId),
onError: (error) => console.error(error.code, error.message),
});
checkout.mount('#checkout');
// Later, to remove it: checkout.destroy();
});
</script>Options
| Option | Default | Description |
|---|---|---|
clientSecret | required | Returned when the session is created with uiMode: "embedded". |
onComplete | – | Called with { sessionId, successUrl } once the customer has paid. |
onError | logs to console | Called with { code, message }. See Errors. |
redirectOnCompletion | "always" | "always": your page goes to the session's successUrl after onComplete. "never": the customer stays on your page and you handle what comes next. |
locale | provider locale | Checkout language: en, de, es, fr, it, nl, pl, pt. |
checkoutUrl | from your public key | Only needed in environments other than production and sandbox: pass the origin of the checkoutUrl returned with the session. |
After payment
- Fulfil the order from the webhook, not from
onComplete: subscribe tocheckout_session.completed(see Overview & Event Types).onCompleteis only meant to update your UI, and it never fires if the customer closes the page during a bank redirect. - Redirects: 3D Secure runs inside the checkout. Payment methods that need to leave the page (for example some bank transfers) open in your page's window, not in the iframe, and bring the customer back to your
successUrl.
Errors
| Code | Meaning | What to do |
|---|---|---|
IFRAME_NOT_READY | The checkout did not load within 15 seconds. Usually the page is not on a verified domain, or the session is not an embedded one. | Check that the page's domain is verified and that the session was created with uiMode: "embedded". |
INVALID_CLIENT_SECRET | The value passed is not a client secret. | Pass the clientSecret from the session response unchanged. |
CHECKOUT_URL_REQUIRED | No default checkout host for this key's environment. | Pass checkoutUrl. |
On the API side, creating an embedded session in production without a verified domain returns checkout.embed_requires_verified_domain (400).
Limitations
- Local currency (adaptive pricing) applies to one-time payments only; subscriptions are charged in the offer's currency.
- One checkout per session: a
clientSecretcannot be reused for another session, and an embedded session cannot be opened as a hosted checkout link.
Updated about 13 hours ago
