Embedded checkout
A payment’s hosted checkout can open in a modal frame on your shop’s page instead of replacing it. The frame shows
the checkout’s own pages, with the same rules as the redirect: the payer confirms there, and your server learns the
outcome from Futa, never from the page. Below 720 px the modal fills the screen; from 720 px it is a sheet 400 px
wide and up to 760 px tall, with a « Fermer » button above the frame. Its label is in the
first of checkout_url’s lang and your page’s <html lang> that is French, English, Lingala or Swahili, else in
French: “Close” in English, “Kokanga” in Lingala, “Funga” in Swahili.
Create the payment for a frame
Section titled “Create the payment for a frame”On your server, create the payment with embedding, naming the origin of the page that will open it:
{ "amount": 2500000, "currency": "CDF", "merchant_reference": "order-10482", "description": "Order 10482", "return_url": "https://shop.example/thanks", "expires_at": "2026-10-20T12:00:00Z", "embedding": { "mode": "frame", "origin": "https://shop.example" }}The origin is your page’s, as window.location.origin gives it: https://, a host name in lower case, and a port
only when it is not 443, with no path, not even a final /, at most 255 characters. An IP address in any spelling (a
last label all digits, or 0x followed by hexadecimal digits), localhost and any name under it are refused over
https; the sandbox alone, hosted or on your machine, also takes http://127.0.0.1:<port> and
http://localhost:<port>. Any other spelling
answers 400 INVALID_REQUEST, never rewritten. Compute expires_at once per order, within the next 24 hours, as the
quick start says: the instant above is an example. The Idempotency-Key binds embedding with the
other terms, direct mode (method and msisdn) refuses it, and payment links never take one. Only that origin may
frame the payment’s pages; without embedding they refuse every frame, as before.
Open it on your page
Section titled “Open it on your page”Load the opener with a <script> tag in your page’s document, from the checkout’s origin, as a classic script or
type="module": in the hosted sandbox, https://checkout.sandbox.futa-app.com, as below; on your machine,
http://127.0.0.1:18140. It sets window.FutaCheckout:
<script src="https://checkout.sandbox.futa-app.com/sdk/v1/checkout.js"></script>The opener takes the checkout’s origin from its own <script> tag and from nothing else, so loading it with
import(), which leaves no tag, or from a script inside a shadow root is not supported. It is served at
/sdk/v1/checkout.js only, and nothing is published to npm.
// checkoutUrl: the checkout_url your server received for this order's payment, written into the page by your// server. Never take it from the page's address or from anything a customer typed.function pay(checkoutUrl, payButton) { if (!window.FutaCheckout) return window.location.assign(checkoutUrl); try { FutaCheckout.open({ checkoutUrl, onState: ({ payment, state, final }) => showProgress(payment, state, final), onClose: ({ final, reason }) => readOrderFromServer(), returnFocus: payButton, }); } catch { window.location.assign(checkoutUrl); }}| Option | What it does |
|---|---|
checkoutUrl |
The payment’s checkout_url exactly as Futa issued it: /pay/<id> on the checkout’s origin with one token and at most a lang. Any other address throws. |
onState |
Called with { payment, state, final } each time the checkout’s page shows a state. Advisory. |
onClose |
Called once with { final, reason }: always for closed, and for the other reasons only with fallback: "none"; with the default, the payer simply leaves for the checkout’s own page. |
fallback |
"redirect", the default: when the frame cannot show the checkout, the payer goes to the checkout’s own page. "none": the payer stays, and onClose says why. |
returnFocus |
The element the focus returns to on close. |
open returns { close(), fallBack() }: close() closes the modal as « Fermer » does, and fallBack() leaves it
for the fallback. When open throws, because the opener cannot tell the checkout’s origin or checkoutUrl is not the
checkout’s address, it has gone nowhere; send the payer to checkout_url yourself, as you do when window.FutaCheckout
is missing because the script was blocked or failed to load.
What the events mean
Section titled “What the events mean”state is the state the server rendered the page in: CREATED, PENDING_APPROVAL, UNCERTAIN, APPROVED,
DECLINED, EXPIRED or CANCELLED. final says the page leaves the payer nothing more to do there, not that
anything was paid: a decline, an expiry or a cancellation with no replacement offered is final too. payment is the
payment the frame shows, which after a decline may be a replacement Futa started on its page rather than the one you
opened, so match payments by id on your server.
Events are advisory. Confirm a payment on your server, by its signed webhook or
GET /v1/payments/{id}, and fulfil from there. The frame’s messages follow
checkout-frame-message-v1.schema.json, with its
vectors: they go to your payment’s origin alone, never to "*", carry the
payment’s id, its state and final, and never a capability, an address, a number, a participant, a method or an
amount. The checkout takes no command from your page.
Closing and fallbacks
Section titled “Closing and fallbacks”When it is called, onClose gets final and one of the reasons below. For closed, final is as the checkout last
said it, false before it said any; for every other reason, it is false, even after a final state.
reason |
When |
|---|---|
closed |
The payer pressed « Fermer » or Escape, the browser closed the dialog (Android’s back gesture), or you called close(). |
timeout |
The frame said nothing within 15 seconds. |
refused |
The frame’s load ended without a word: the browser refused it, as it does when your page’s origin is not the one the payment names. |
unsupported |
Opera Mini, or a browser without postMessage: no frame can talk to your page. |
requested |
You called fallBack(). |
With the default fallback, every reason but closed sends the payer to the checkout’s own page, where they pay as
with a redirect, and onClose is not called; with fallback: "none", the payer stays, and onClose gets
final: false and the reason.
Leaving the frame
Section titled “Leaving the frame”« Retour à … » and the bank’s handoff open at the top level: issuer pages refuse frames, and bank credentials are
never typed inside your page. A bank payment started in the modal is therefore finished at the top level, and your
return_url brings the payer back. Receipts download from the frame. A framed payment never offers to remember the
payer’s method on the device.
Your page
Section titled “Your page”- Content Security Policy. If your page sends one, allow the checkout’s origin in
script-src, for the opener, and inframe-src, for its pages: in the hosted sandbox,frame-src https://checkout.sandbox.futa-app.com; on your machine,frame-src http://127.0.0.1:18140. - Permissions Policy. The frame is allowed
web-shareandclipboard-write(itsallowattribute), which the checkout’s share and copy buttons use: aPermissions-Policyon your page that denies them silences those buttons. - A frame you build yourself. Use the opener. If you frame
checkout_urlwithout it and set asandboxattribute, give it the opener’s own tokens,allow-scripts allow-forms allow-same-origin allow-downloads allow-top-navigation-by-user-activation. Withoutallow-same-origineach of the checkout’s forms answers403; withoutallow-formsthe browser sends nothing; withoutallow-top-navigation-by-user-activationthe bank’s handoff and « Retour à … » are blocked, so a bank payment cannot start; withoutallow-downloadsthe receipts are; withoutallow-scriptsthe frame tells your page nothing. The payer is then stuck in your modal, though nothing claims an outcome. - The capability in the frame’s address. The frame’s
srcis the payment’scheckout_url, which opens that payment’s pages and receipts. Block the checkout’s frame in session-replay tools, withiframe[src^="https://checkout.sandbox.futa-app.com/pay/"]in the hosted sandbox oriframe[src^="http://127.0.0.1:18140/pay/"]on your machine, and keep the checkout’s/pay/addresses out of what monitoring and analytics agents record, resource-timing names included. - Focus. The focus starts on « Fermer »; Escape closes the modal while the focus is outside the frame. On close
it returns to
returnFocus, else to what had it at open, or to the control the payer pressed inside that. A « Payer » button inside an auto popover (popover) closes with it as the modal opens, since a modal dialog closes every auto popover, and the focus then has nowhere to return: passreturnFocusa control outside it, or make the popoverpopover="manual".