Flowwithlit Docs
ACCEPT PAYMENTS

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
⚠️
Popup mode uses a real 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)

  1. You embed inline.js and call FlowPay.init() with mode: 'popup' (or omit mode — popup is the default) from a user click.
  2. Our script opens a secure popup window to checkout.flowwithlit.com.
  3. The customer pays inside that window.
  4. On success, the checkout window sends a postMessage to your page with the transaction reference, then closes.
  5. onSuccess runs → your frontend calls your server → your server verifies with the secret key.

Pattern 2 — Redirect (Paystack-style)

  1. Call FlowPay.init() with mode: 'redirect' and a callback_url.
  2. The browser navigates full-page to the hosted checkout on checkout.flowwithlit.com.
  3. The customer pays on Flowwithlit checkout.
  4. After success, checkout redirects the browser back to your callback_url with:
    ?transaction_ref=FLW_...&status=successful (plus any query params you already put on the callback URL).
  5. Your return page reads transaction_ref, verifies server-side with your secret key, then shows a receipt or fulfils the order.
ℹ️
With 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.
🔒
Because the card form lives on 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.

Your server — create the session
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" }
  }'
Response
{
  "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:

JavaScript
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.
  }
});
⏱️
Sessions expire after 30 minutes and can only be used once — create a new one per checkout attempt (e.g. when the customer clicks "Pay", not on page load).

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:

HTML
<script src="https://js.flowwithlit.com/v1/inline.js"></script>

FlowPay.init() Options

Call FlowPay.init(options) to open the checkout modal. All options:

ParameterTypeDescription
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.

JavaScript — start payment
// 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 — return page (payment/return.php)
<?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

ParameterSet byDescription
transaction_refFlowwithlitTransaction reference — verify this server-side
statusFlowwithlitsuccessful when payment completed
reference / trxrefAliases supported for compatibility with Paystack-style handlers
your paramsYouAnything you add to callback_url before init (e.g. order_id, expected_kobo)
🔒
Live demo: FlowShop is a working example store that runs this exact flow — popup and redirect modes, server-side verification, and a webhook log viewer. View its source in the demo/ folder.

Programmatic Close

Popup mode only — closes the payment window if it is still open:

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

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

CurrencyUnitExample
NGNKobo (1/100 of a Naira)₦5,000 → 500000
USDCents (1/100 of a Dollar)$25.00 → 2500