Flowwithlit Docs
ACCEPT PAYMENTS

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)

  1. You embed inline.js and call FlowPay.init() with mode: 'popup' (or omit mode — popup is the default).
  2. Our script draws an iframe overlay pointing to checkout.flowwithlit.com.
  3. The customer pays inside the secure iframe.
  4. On success, the iframe sends a postMessage to your page with the transaction reference.
  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 to the hosted checkout page (full page, not an iframe).
  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.

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

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 iframe overlay:

You can close the modal from your own code — useful for handling timeout scenarios:

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

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