Inline Checkout (inline.js)
Add a Flowwithlit payment button to any website with a single script tag. Choose popup (iframe overlay) or redirect (Paystack-style hosted page). 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 modal iframe | 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 |
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 | |
Pattern 1 — Popup (iframe overlay)
- You embed
inline.jsand callFlowPay.init()withmode: 'popup'(or omitmode— popup is the default). - Our script draws an iframe overlay pointing to
checkout.flowwithlit.com. - The customer pays inside the secure iframe.
- On success, the iframe sends a
postMessageto your page with the transaction reference. 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 to the hosted checkout page (full page, not an iframe).
- 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.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. |
amount required |
number | Amount in the lowest denomination of the currency (kobo for NGN, cents for USD). E.g. ₦5,000 → 500000. |
currency optional |
string | ISO 4217 currency code. Defaults to NGN. Supported: NGN, USD. |
email required |
string | 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) — iframe overlay on your page. '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 — if set, the customer is also redirected here after success (in addition to onSuccess).
|
display optional |
string | 'modal' or 'fullscreen'. Popup only. Defaults to fullscreen on mobile viewports. |
platform optional |
string | 'web' or 'mobile'. Hint for checkout layout (WebViews, native apps). Auto-detected on narrow screens. |
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 modal without paying. |
onError optional |
function | Called with an error object if the checkout encounters an error. |
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 iframe overlay:
You can close the modal from your own code — useful for handling timeout scenarios:
FlowPay.close();
postMessage Protocol
Under the hood, the checkout iframe communicates with your page via
window.postMessage. The inline.js handles this automatically, but 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 |