Checkout.js
capay.js opens the hosted checkout from your own page. It needs no API key: the payment link id is the credential, exactly like POST /v1/checkout/initiate, so the script is safe to run in a browser.
Get your test keys
Create a free account, then open Developers > API keys in the dashboard. Sandbox keys start with sk_test_ and are shown once, so store the key the moment it appears. Sandbox keys never touch a real provider and never move real money.
Add the script
Create the payment link in the dashboard, then pass its pl_ id to Capay.open(). The call returns a handle whose done promise resolves once, on the first terminal status or when the payer closes the window. You can also open an existing collection by its col_ id. Payment links & checkout
<script src="https://pay.capayglobal.com/capay.js"></script>
<button id="pay">Pay 5,000 XAF</button>
<script>
document.getElementById('pay').addEventListener('click', async () => {
const handle = Capay.open({
link: 'pl_9Xw2mB4qR7Ta', // or collection: 'col_...' created server-side
lang: 'en', // optional: en fr ha ig pcm sw yo
onStatus: (event) => console.log(event.status, event.outcome),
onSuccess: (event) => console.log('paid', event.reference),
onFailed: (event) => console.log('no money', event.reason_code),
onClose: (event) => console.log('closed by', event.reason),
});
const result = await handle.done; // resolves once, never rejects
// result.outcome: 'success' | 'failed' | 'closed'
// Confirm on your server before you fulfil: see "Server truth" below.
});
</script>Never put a secret key on a page. capay.js does not accept one, and a sk_test_ or sk_live_ key in browser JavaScript is a key you have published.
How the checkout opens
capay.js renders a modal overlay on your page and loads the hosted checkout inside it, so the payer never leaves your site. The script talks only to the hosted checkout origin and validates every message it receives.
Prerequisite, true as this page was written: https://pay.capayglobal.com answers X-Frame-Options: SAMEORIGIN, so the modal cannot load it from a merchant origin. Until that response-headers policy changes, send the payer to the hosted checkout instead. The redirect below works on every browser and needs no script.
<!-- Works on every browser and on every deployment: send the payer to the
hosted checkout and let it redirect back to your return URL. -->
<a href="https://pay.capayglobal.com/l/pl_9Xw2mB4qR7Ta">Pay 5,000 XAF</a>Callbacks
Every callback is optional. Status events arrive in order, including replays after a terminal one, and carry the payer-safe projection: no merchant reference and no amounts beyond what the payer already sees.
| Callback | Payload | When it fires |
|---|---|---|
| onReady | { page, public_id, link_status? } | The checkout page loaded. page tells you what it showed; link_status flags a disabled or expired link. |
| onStatus | { status, outcome, public_id, reference, reason_code, is_terminal } | Every status change. outcome is success for completed, failed for failed, cancelled, expired and reversed, and pending for everything else, including a status the script does not know yet. |
| onSuccess | { status: "completed", outcome: "success", reference, ... } | Once, when the collection completes. reference is the receipt reference to show the payer. |
| onFailed | { status, outcome: "failed", reason_code, ... } | Once, on failed, cancelled, expired or reversed. reason_code says why. |
| onClose | { reason, last_status } | Once, when the modal is removed. reason is user or programmatic; last_status is the last status seen, or null. |
| handle.done | { outcome, event } | A promise that resolves once and never rejects: the first terminal status, or a close before one arrived. |
outcome is a convenience, not a verdict. When the payer closes the window the collection may still complete a minute later, so keep listening to webhooks instead of marking the order failed.
Origins and messages
The script loads only https://pay.capayglobal.com and the gateway API at https://api.capayglobal.com/v1. It never posts your page data anywhere else.
If you listen for messages yourself, check the origin first and then the message shape. A message from any other origin, or without the capay-checkout source and version 1, is not from the checkout:
window.addEventListener('message', (event) => {
if (event.origin !== 'https://pay.capayglobal.com') return; // ignore every other origin
const message = event.data;
if (message?.source !== 'capay-checkout' || message.version !== 1) return;
// message.type: 'ready' | 'status'
// status messages carry { status, public_id, reference, reason_code, is_terminal }
});Server truth
The browser callback is a user-experience signal, not proof of payment. Fulfil the order only on the collection.completed webhook or on a server-side GET /v1/collections/{id}. A page can be closed, scripted or replayed; your server cannot be: Webhooks
The payer-safe status projection is { status, reason_code, receipt_ref, requeried }. It is deliberately narrower than the merchant-side collection object, because it is readable by anyone holding the collection id.
Mobile apps
Native mobile SDKs are not available yet, and no date is promised. Android, iOS, Flutter and React Native apps open the hosted checkout URL in a system browser or custom tab today, which is also what the payment security guidance recommends.
// Android (Custom Tabs), iOS (SFSafariViewController), Flutter (url_launcher), // React Native (Linking): open the hosted checkout URL in the system browser. https://pay.capayglobal.com/l/pl_9Xw2mB4qR7Ta