Drop-in
Server plus a few lines of frontend. Open XPay's checkout in a modal or inline iframe on your domain. The customer never leaves your site.
Drop-in opens XPay's full checkout inside an iframe on your domain. Your server creates a Checkout Session, your frontend calls xpay.checkout({ ... }), and the customer pays without ever navigating away. You write roughly ten lines of frontend code; the iframe handles payment methods, the card form, 3D Secure, and local methods like Valu and Fawry.
Pick this pattern when you want the customer to stay on your domain but you don't want to build the payment form yourself. If you need a custom UI, use Elements. If you don't want any frontend code, use Hosted Checkout or a Payment Link.
The code samples below cover both Vanilla JavaScript (any framework, plain HTML) and React. The two SDKs share the same API; pick the tab that matches your stack. Non-React frameworks (Vue, Svelte, Solid, Angular, Lit) use the vanilla path.
Drop-in is a thin SDK wrapper around the same Checkout Session every other pattern uses. For the full set of fields you can set on POST /checkout/sessions (line items, customer collection, branding, payment methods, fees, metadata, etc.), see the Checkout Session reference.
How it works
- Your server calls
POST /checkout/sessionswithuiMode: "embedded"and the line items. XPay returns a session that includesclientSecretandpaymentMethodTypes. - Your server ships the
clientSecretto your frontend (typically inside an HTML page render or a fetch response). - Your frontend loads the SDK with your publishable key and calls
xpay.checkout({ clientSecret, mode: "modal" }). Callingcheckout.open()shows the iframe. - The customer picks a method, fills the form, completes 3D Secure if required, and pays. All without navigating away.
- The SDK fires your
onCompletecallback with the Payment Intent ID. Your server independently receivescheckout.session.completedto fulfill the order.
The whole interaction takes place in an iframe pointing at https://checkout.xpay.app. Card data never enters your DOM.
Build it
1. Get test API keys
You need both keys for Drop-in:
sk_test_*for your server. Used to create the Checkout Session.pk_test_*for your frontend. Used to load the SDK. Safe to ship to the browser.
In the dashboard, go to Developer → API Keys and copy both. Live keys (sk_live_* / pk_live_*) work the same way once your account is approved.
2. Create a Checkout Session on your server
Two requirements specific to Drop-in:
uiMode: "embedded"tells XPay this session is for an iframe.- The session response includes a
clientSecretthat scopes the SDK to this specific session. You'll ship it to the frontend.
curl -X POST https://api.xpay.app/checkout/sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"uiMode": "embedded",
"afterCompletion": {
"type": "redirect",
"redirect": { "url": "https://yourshop.example/order/{CHECKOUT_SESSION_ID}" }
},
"lineItems": [
{
"priceData": {
"currency": "EGP",
"unitAmount": 149900,
"productData": { "name": "Test product" }
},
"quantity": 1
}
]
}'const res = await fetch("https://api.xpay.app/checkout/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.XPAY_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
uiMode: "embedded",
afterCompletion: {
type: "redirect",
redirect: { url: "https://yourshop.example/order/{CHECKOUT_SESSION_ID}" },
},
lineItems: [
{
priceData: {
currency: "EGP",
unitAmount: 149900,
productData: { name: "Test product" },
},
quantity: 1,
},
],
}),
});
const session = await res.json();
// Send `session.clientSecret` to your frontendimport os, requests
res = requests.post(
"https://api.xpay.app/checkout/sessions",
headers={
"Authorization": f"Bearer {os.environ['XPAY_SECRET_KEY']}",
"Content-Type": "application/json",
},
json={
"uiMode": "embedded",
"afterCompletion": {
"type": "redirect",
"redirect": {"url": "https://yourshop.example/order/{CHECKOUT_SESSION_ID}"},
},
"lineItems": [
{
"priceData": {
"currency": "EGP",
"unitAmount": 149900,
"productData": {"name": "Test product"},
},
"quantity": 1,
}
],
},
timeout=10,
)
session = res.json()
# Send `session["clientSecret"]` to your frontendThe response includes the same Checkout Session shape Hosted Checkout returns. The fields you ship to your frontend:
{
"id": "cs_test_AbC123...",
"clientSecret": "cs_test_AbC123..._secret_xyz789",
"amountTotal": 149900,
"currency": "EGP"
}A few rules:
cancelUrlis rejected foruiMode: "embedded". Drop-in surfaces failures inside the iframe with a retry. There's no "send the customer to a failure URL" path.clientSecretis single-session. It scopes the SDK to this specific session and can't be reused. Don't store it past the session's lifetime.afterCompletion.redirect.urlis still meaningful. When the payment succeeds, the SDK passes that URL into youronCompletecallback asredirectUrl. Your code decides whether to navigate. If you usedhosted_confirmation,redirectUrlisundefined: the iframe shows the success page briefly and auto-closes the modal.
3. Load the SDK on your frontend
Install the loader from npm:
npm install @xpay/sdk-jsThen load and call from your page:
import { loadXPay } from "@xpay/sdk-js";
const xpay = await loadXPay("pk_test_..."); // your publishable key
const checkout = xpay.checkout({
clientSecret: "cs_test_..._secret_xyz789", // from your server
mode: "modal",
onComplete: (result) => {
// result.paymentIntentId is the pi_*
// result.redirectUrl is your afterCompletion.redirect.url (if set)
if (result.redirectUrl) {
window.location.href = result.redirectUrl;
}
},
onClose: () => {
console.log("Customer closed the modal");
},
});
document.getElementById("pay-button")?.addEventListener("click", () => {
checkout.open();
});import { loadXPay } from "@xpay/sdk-js";
import { XPayProvider, CheckoutButton } from "@xpay/sdk-react";
// Load once at module level (not inside a component)
const xpayPromise = loadXPay("pk_test_...");
export function PayPage({ clientSecret }: { clientSecret: string }) {
return (
<XPayProvider xpay={xpayPromise}>
<CheckoutButton
clientSecret={clientSecret}
checkoutOptions={{
onComplete: (result) => {
if (result.redirectUrl) window.location.href = result.redirectUrl;
},
}}
>
Pay now
</CheckoutButton>
</XPayProvider>
);
}<XPayProvider> accepts either an XPayInstance or Promise<XPayInstance>, so passing xpayPromise directly is the idiomatic pattern. Don't call loadXPay() inside a component. Load it once per page at module scope.
The SDK loads from https://checkout.xpay.app/sdk.js once per page; subsequent loadXPay() calls return the same cached instance.
4. Handle the result
xpay.checkout(...) returns a CheckoutInstance you can subscribe to via constructor callbacks or .on(event, handler). Five events fire over the lifetime of one checkout.
| Event | When | Payload |
|---|---|---|
ready | The session loaded and the iframe is ready to render. | The full Checkout SessionAPI. |
confirmed | The customer hit Pay. Useful if you want to disable the close button or show a "processing" state. | None. |
complete | The payment succeeded. The iframe auto-closes the modal 300ms later. | { status: "succeeded", paymentIntentId, chargeId?, redirectUrl? }. |
close | The modal closed. Fires for manual close, after complete, or if checkout.close() is called. | None. |
error | The session failed to load or the SDK encountered a fatal error. Doesn't fire for user-recoverable errors like a card decline. | { message, code? }. |
The complete event is a UX courtesy. Your server must still listen for the checkout.session.completed webhook to mark the order paid. The iframe can be closed early, the network can drop, or your onComplete handler can throw. The webhook is the source of truth.
In React, the same callbacks ride on the checkoutOptions prop of <CheckoutButton />: onComplete, onClose, onReady, onConfirmed, onError. They map one-to-one to the events above.
Modal vs inline
mode controls whether the iframe overlays the page or sits inside a container.
mode: modal
Full-screen overlay with a centered iframe. You call checkout.open() to show it. Click outside, press Escape, or call checkout.close() to dismiss. Best for "Pay now" buttons, post-cart pages, anything event-driven.
mode: inline
Iframe mounted inside a container you provide. No open/close: it's there as soon as you create the instance. Pass container as a CSS selector or an HTMLElement. Best for embedded "checkout" pages where the form is the page.
// Inline mode
const checkout = xpay.checkout({
clientSecret,
mode: "inline",
container: "#checkout-container", // or document.getElementById(...)
onComplete: (result) => { /* ... */ },
});
// On unmount or before re-creating, release the iframe
checkout.destroy();The inline iframe auto-resizes to its content height. You don't need to set a fixed height in your CSS.
Always call checkout.destroy() when you tear down the component that owns the inline checkout. Without it, the iframe stays in the DOM and a fresh xpay.checkout({ mode: "inline" }) call would mount a second one. Modal mode handles its own cleanup on close, but you can still call destroy() to release listeners early.
<CheckoutButton /> is modal-only. For the inline pattern in React, drop down to the JS SDK via useXPay() and call xpay.checkout({ mode: "inline" }) yourself. Remember to call destroy() in a cleanup useEffect.
Customizing appearance and locale
appearance accepts the same shape as the session's brandingSettings. The SDK runtime values win; the session's brandingSettings and your merchant defaults provide fallbacks. For the full field list see Advanced configuration → Branding.
const checkout = xpay.checkout({
clientSecret,
mode: "modal",
locale: "ar",
appearance: {
colorMode: "dark",
borderStyle: "rounded",
colors: { primary: "#635bff" },
},
});<CheckoutButton
clientSecret={clientSecret}
checkoutOptions={{
locale: "ar",
appearance: {
colorMode: "dark",
borderStyle: "rounded",
colors: { primary: "#635bff" },
},
onComplete: (result) => { /* ... */ },
}}
>
Pay now
</CheckoutButton>Useful when the same session needs to look different across surfaces (e.g. matching a dark-mode toggle on your page without persisting it on the session).
Confirm with a webhook
Drop-in's onComplete callback is fast and convenient, but it's not authoritative. The Checkout Session your server creates is the same object every other pattern produces, so the checkout.session.completed webhook handler you'd write for Hosted Checkout works unchanged for Drop-in.
The minimum your handler needs to do:
- Verify the
XPay-Signatureheader. - Parse the JSON body.
- If the event type is
checkout.session.completedand the sessionstatusiscomplete, fulfill the order.
For the full handler walkthrough (signature verification recipe, payload field table, retry behavior), see Hosted Checkout → Confirm with a webhook. The same code applies here.
Test it
In test mode, use the success card 5123 4500 0000 0008 with expiry 01/39 to run the happy path inside the iframe. The card form, 3D Secure challenge, and post-payment success state all run in the iframe; you don't need to switch tabs or pages. For the full test card list and the expiry-to-outcome matrix, see Test mode and test cards.
To exercise your webhook handler locally before deploying, see Local webhook development.
Production checklist
Before flipping to live mode:
- Swap the keys. Replace
sk_test_*withsk_live_*on your server andpk_test_*withpk_live_*on your frontend. The API URL doesn't change. - Set up a live webhook endpoint. Test and live have separate webhook endpoints. Each gets its own
whsec_*signing secret. - Verify signatures on the webhook, not on
onComplete. Treat thecompleteevent as a UX hint, not authorization. - Don't expose the secret key to the browser.
sk_*keys are server-only. The SDK takes a publishable key (pk_*) by design. - Dedupe webhook deliveries on
event.id. XPay retries deliveries on non-2xx or timeout, so the samecheckout.session.completedfor the same session can arrive more than once. Track event IDs you've already processed. - Dedupe your own session creates on
session.id. If yourPOST /checkout/sessionsrequest times out, don't blindly retry. Store the session ID from the first successful response and reuse it. - Set a content-security-policy that allows
https://checkout.xpay.app. If your site uses a strictframe-srcorscript-srcdirective, the SDK script and the embedded iframe both need to be allowed.
Where to next
Object model
How Checkout Session, Payment Intent, Charge, and Customer relate, and which IDs to keep on your order record.
Refunds
Reverse a successful payment using the pi_* from result.paymentIntentId.
Webhooks setup
Add an endpoint and grab the whsec_* signing secret.
Verifying signatures
The full HMAC-SHA256 verification recipe with replay protection.
Test mode and test cards
The test card list and the outcomes you can simulate.
Want a different pattern?
Compare Drop-in against Hosted Checkout, Elements, and Payment Links.
Hosted Checkout
Server-only integration. Create a Checkout Session, redirect the customer to XPay's hosted page, confirm with a webhook.
Elements
Build your own checkout UI. Your form collects customer details, our PaymentElement handles cards and local methods, your code calls confirm(). Maximum control.