Capay Global

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

Capay.open()
<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.

redirect to the hosted checkout
<!-- 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.

CallbackPayloadWhen 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:

postMessage origin check
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.

mobile apps today
// 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