Inline Checkout (inline.js)
Add a Flowwithlit payment button to any website with a single script tag. Choose popup (a real browser window) or redirect (Paystack-style full-page hosted checkout). In both cases, card details never touch your server.
Two Integration Patterns
Flowwithlit supports the same two patterns most Nigerian gateways use. Pick the one that fits your product:
| Popup (default) | Redirect | |
|---|---|---|
| Customer experience | Stays on your site; checkout opens in a centered browser popup window | Leaves your site → pays on hosted checkout → returns to your URL |
mode value |
'popup' (default) |
'redirect' |
| Success handling | onSuccess callback fires on the same page (via postMessage) |
Customer lands on your callback_url with query params |
| Best for | SPAs, dashboards, staying on one URL | Simple stores, mobile WebViews, Paystack-like flows |
| Server verify | Required in both patterns — never fulfil from the browser alone | |
window.open payment window (not an on-page iframe).
Call FlowPay.init() from a click handler so browsers allow the popup.
If the customer has blocked pop-ups, onError fires with code POPUP_BLOCKED.
Pattern 1 — Popup (browser window)
- You embed
inline.jsand callFlowPay.init()withmode: 'popup'(or omitmode— popup is the default) from a user click. - Our script opens a secure popup window to
checkout.flowwithlit.com. - The customer pays inside that window.
- On success, the checkout window sends a
postMessageto your page with the transaction reference, then closes. onSuccessruns → your frontend calls your server → your server verifies with the secret key.
Pattern 2 — Redirect (Paystack-style)
- Call
FlowPay.init()withmode: 'redirect'and acallback_url. - The browser navigates full-page to the hosted checkout on
checkout.flowwithlit.com. - The customer pays on Flowwithlit checkout.
- After success, checkout redirects the browser back to your
callback_urlwith:
?transaction_ref=FLW_...&status=successful(plus any query params you already put on the callback URL). - Your return page reads
transaction_ref, verifies server-side with your secret key, then shows a receipt or fulfils the order.
mode: 'redirect', onSuccess does not run on the page that started payment
(the customer has already navigated away). Handle success on your callback_url instead — read
transaction_ref from the query string there and verify it server-side.
checkout.flowwithlit.com (not your domain), card numbers are
completely isolated from your server. This is the same architecture Stripe, Paystack, and Flutterwave use.🔒 Recommended: Lock In the Amount Server-Side
public_key is not secret — it's meant to be visible in your page source (that's how
checkout knows which merchant is being paid). That also means if you pass amount and
email straight to FlowPay.init() like the examples further down, a
customer can open dev tools and edit them before paying, and anyone who has your public key could
call our API directly and fabricate a fake "successful" transaction.
The fix: your own server creates a checkout session first, using
your secret key (never put this in browser code). The amount, currency, and email
get locked in there — the browser only ever sees a one-time session_token, and our
checkout page looks the real amount up by that token instead of trusting the URL.
curl -X POST https://api.flowwithlit.com/v1/checkout/sessions \
-H "Authorization: Bearer flw_sec_test_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 1500000,
"currency": "NGN",
"email": "customer@example.com",
"name": "John Doe",
"ref": "ORDER_1042",
"meta": { "order_id": "1042" }
}'
{
"status": true,
"data": {
"token": "cs_9f2a1c...",
"checkout_url": "https://checkout.flowwithlit.com/?session=cs_9f2a1c...&key=flw_pub_test_...",
"expires_at": "2026-07-26T14:32:00Z"
}
}
Then, on your frontend, pass the token instead of amount / email:
FlowPay.init({
public_key: 'flw_pub_test_YOUR_PUBLIC_KEY',
session_token: token, // from your server's /v1/checkout/sessions call
mode: 'popup',
onSuccess: function (response) {
// Still verify server-side with /v1/transaction/verify/{ref} before fulfilling.
}
});
The amount-based pattern documented below still works, unchanged, for backward
compatibility — but use a session for anything that fulfils a real order.
The cURL example above is enough to start; your own backend should call
POST /v1/checkout/sessions with the secret key, then pass
data.token to FlowPay.init({ session_token }).
Installation
Add this script tag to your HTML page — before </body> is fine:
<script src="https://js.flowwithlit.com/v1/inline.js"></script>
FlowPay.init() Options
Call FlowPay.init(options) to open the checkout modal. All options:
| Parameter | Type | Description |
|---|---|---|
public_key required |
string | Your public API key. Starts with flw_pub_test_ (test) or flw_pub_live_ (live). Safe to include in browser code. |
session_token optional |
string | Recommended. Token from POST /v1/checkout/sessions (secret key, called from your server). When set, it takes priority — amount, currency, email, name, ref, and meta below are ignored in favor of whatever was locked in when the session was created. |
amount required* |
number | *Required unless session_token is set. Amount in the lowest denomination of the currency (kobo for NGN, cents for USD). E.g. ₦5,000 → 500000. Editable in the browser before payment — see the section above. |
currency optional |
string | ISO 4217 currency code. Defaults to NGN. Supported: NGN, USD. |
email required* |
string | *Required unless session_token is set. Customer email address. Used to identify the customer in your dashboard. |
name optional |
string | Customer full name. Shown on the checkout form. |
ref optional |
string | Your own unique transaction reference. If omitted, one is generated automatically (e.g. FLW_TXN_A3F8B2C1). |
meta optional |
object | Any extra data you want attached to this transaction — e.g. {order_id: "123", product: "hoodie"}. Passed through to webhooks. |
mode optional |
string | 'popup' (default) — real browser popup window. 'redirect' — full-page hosted checkout, then return via callback_url. |
callback_url optional |
string |
Redirect mode: required — your return URL after payment (e.g. https://yoursite.com/payment/return).Popup mode: optional — only used if you also set redirect_on_success: true (see below). By default, popup success stays on your page and only runs onSuccess.
|
cancel_url optional |
string | Where checkout sends the customer if they cancel (mainly useful in redirect mode). Popup mode still fires onClose when the window is closed. |
redirect_on_success optional |
boolean | Popup mode only. Default false. If true and callback_url is set, after onSuccess the parent page also navigates to callback_url?transaction_ref=…&status=successful. |
display optional |
string | Layout hint for the hosted page: 'popup' or 'fullscreen'. Usually set automatically from mode. |
platform optional |
string | 'web' or 'mobile'. Hint for checkout layout (WebViews, native apps). Auto-detected on mobile user agents. |
onSuccess optional |
function | Popup mode only — called with { transaction_ref, amount, currency, email, meta } when payment succeeds. Always verify server-side. |
onClose optional |
function | Called when the customer closes the popup without paying. |
onError optional |
function | Called with an error object (e.g. { message, code: 'POPUP_BLOCKED' }) if checkout cannot start or fails. |
Complete Example — Popup
<!DOCTYPE html>
<html>
<head>
<title>My Shop</title>
</head>
<body>
<h1>Premium Hoodie — ₦15,000</h1>
<button id="payBtn">Buy Now</button>
<script src="https://js.flowwithlit.com/v1/inline.js"></script>
<script>
document.getElementById('payBtn').addEventListener('click', function() {
FlowPay.init({
public_key: 'flw_pub_test_YOUR_PUBLIC_KEY',
amount: 15000 * 100, // ₦15,000 in kobo
currency: 'NGN',
email: 'customer@example.com',
name: 'John Doe',
ref: 'ORDER_' + Date.now(),
mode: 'popup', // default — can be omitted
meta: {
product: 'Premium Hoodie',
size: 'XL',
order_id: '1042'
},
onSuccess: function(response) {
// MUST verify on your server before fulfilling!
fetch('/verify.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ref: response.transaction_ref })
})
.then(r => r.json())
.then(data => {
if (data.verified) {
document.querySelector('h1').textContent = '✅ Order Confirmed!';
}
});
},
onClose: function() {
console.log('Checkout closed by customer');
},
onError: function(err) {
alert('Payment error: ' + err.message);
}
});
});
</script>
</body>
</html>
// PayButton.jsx
import { useEffect } from 'react';
export default function PayButton({ amount, email, name, onPaid }) {
useEffect(() => {
// Load inline.js once
if (!window.FlowPay) {
const s = document.createElement('script');
s.src = 'https://js.flowwithlit.com/v1/inline.js';
document.body.appendChild(s);
}
}, []);
const pay = () => {
window.FlowPay?.init({
public_key: process.env.REACT_APP_FLW_PUBLIC_KEY,
amount: amount * 100, // pass naira, convert to kobo
currency: 'NGN',
email,
name,
onSuccess: async (response) => {
const res = await fetch('/api/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ref: response.transaction_ref })
});
const data = await res.json();
if (data.verified) onPaid(data);
},
onClose: () => console.log('closed'),
});
};
return <button onClick={pay}>Pay ₦{amount.toLocaleString()}</button>;
}
<!-- PayButton.vue -->
<template>
<button @click="pay">Pay ₦{{ amount.toLocaleString() }}</button>
</template>
<script setup>
const props = defineProps(['amount', 'email', 'name']);
const emit = defineEmits(['paid']);
function pay() {
if (!window.FlowPay) return alert('FlowPay not loaded');
FlowPay.init({
public_key: import.meta.env.VITE_FLW_PUBLIC_KEY,
amount: props.amount * 100,
currency: 'NGN',
email: props.email,
name: props.name,
async onSuccess(response) {
const res = await fetch('/api/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ref: response.transaction_ref })
});
const data = await res.json();
if (data.verified) emit('paid', data);
},
onClose: () => console.log('closed'),
});
}
</script>
Complete Example — Redirect
Pass order context on the callback URL (order id, expected amount) so your return page knows what to verify.
After payment, checkout appends transaction_ref and status=successful.
// On your product / checkout page
function payWithRedirect(orderId, amountNaira, email) {
const expectedKobo = amountNaira * 100;
// Build return URL with your order context
const returnUrl = new URL('/payment/return.php', window.location.origin);
returnUrl.searchParams.set('order_id', orderId);
returnUrl.searchParams.set('expected_kobo', String(expectedKobo));
FlowPay.init({
public_key: 'flw_pub_test_YOUR_PUBLIC_KEY',
amount: expectedKobo,
currency: 'NGN',
email: email,
mode: 'redirect', // leaves your site
callback_url: returnUrl.toString(), // where customer returns
ref: 'ORDER_' + orderId + '_' + Date.now(),
meta: { order_id: orderId },
});
// Browser navigates away — onSuccess will NOT run here
}
<?php
// Customer lands here after successful payment:
// /payment/return.php?order_id=1042&expected_kobo=1500000&transaction_ref=FLW_...&status=successful
define('FLW_SECRET_KEY', getenv('FLW_SECRET_KEY'));
define('API_BASE', 'https://api.flowwithlit.com');
$ref = $_GET['transaction_ref'] ?? $_GET['reference'] ?? '';
$status = strtolower($_GET['status'] ?? '');
$expectedKobo = (int)($_GET['expected_kobo'] ?? 0);
if ($status !== 'successful' || !$ref || $expectedKobo <= 0) {
http_response_code(400);
exit('Invalid return');
}
// Verify with secret key — never trust query params alone
$ch = curl_init(API_BASE . '/v1/transaction/verify/' . urlencode($ref));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . FLW_SECRET_KEY],
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
$ok = ($res['data']['status'] ?? '') === 'successful'
&& (int)($res['data']['amount'] ?? 0) === $expectedKobo;
if ($ok) {
// Mark order paid, send email, show receipt
echo 'Payment confirmed. Ref: ' . htmlspecialchars($ref);
} else {
http_response_code(402);
echo 'Payment could not be verified.';
}
Return URL query parameters
| Parameter | Set by | Description |
|---|---|---|
transaction_ref | Flowwithlit | Transaction reference — verify this server-side |
status | Flowwithlit | successful when payment completed |
reference / trxref | — | Aliases supported for compatibility with Paystack-style handlers |
| your params | You | Anything you add to callback_url before init (e.g. order_id, expected_kobo) |
demo/ folder.
Programmatic Close
Popup mode only — closes the payment window if it is still open:
FlowPay.close();
postMessage Protocol
Under the hood, the checkout popup window communicates with your page via
window.postMessage. inline.js handles this automatically and
calls your onSuccess / onClose / onError handlers.
You can listen directly if you need to:
window.addEventListener('message', function(event) {
if (event.data?.source !== 'flowwithlit-checkout') return;
if (event.data.type === 'success') {
const { transaction_ref, amount, currency } = event.data.payload;
// verify server-side using transaction_ref
}
if (event.data.type === 'close') {
// customer closed without paying
}
if (event.data.type === 'error') {
console.error(event.data.payload.message);
}
});
Amount in Lowest Denomination
Always pass the amount in the smallest unit of the currency — this avoids floating-point ambiguity:
| Currency | Unit | Example |
|---|---|---|
| NGN | Kobo (1/100 of a Naira) | ₦5,000 → 500000 |
| USD | Cents (1/100 of a Dollar) | $25.00 → 2500 |