Credo Popup
Accept payments without leaving your site using the Credo inline JavaScript widget.
The Credo popup is an inline payment widget that lets customers pay directly on your website. No redirects, no separate checkout page - the payment modal opens on top of your page and handles card entry, bank transfer, OTP, and 3D Secure automatically.
How it works
Quick start
Add the inline script
Include the Credo inline script in your HTML page:
<script src="https://pay.credocentral.com/inline.js"></script>Configure and initialize
Set up the widget with your payment details and attach it to a button:
<button onClick="handler.openIframe()">Pay Now</button>
<script>
const handler = CredoWidget.setup({
key: "0PUBxxxxxxxxxxxxxxxxxxxxxxxx",
email: "customer@example.com",
amount: 150000,
currency: "NGN",
channels: ["card", "bank"],
reference: "ORD-20260208-001",
callbackUrl: "https://yoursite.com/payment/success",
onClose: () => {
console.log("Payment modal closed");
},
callBack: (response) => {
console.log("Payment complete:", response);
window.location.href = response.callbackUrl;
},
});
</script>Verify the transaction
After the callBack fires, verify the transaction server-side before fulfilling the order. See Accept Payments for verification details.
Configuration reference
Required fields
| Field | Type | Description |
|---|---|---|
key | string | Your public key. Use sandbox key (0PUB...) for testing, production key (1PUB...) for live. |
email | string | Customer's email address. |
amount | number | Amount in lowest currency unit (kobo for NGN, cents for USD). 150000 = NGN 1,500. |
callbackUrl | string | URL to redirect to after payment completes. |
callBack | function | Called when the transaction completes. Receives the transaction response object. |
onClose | function | Called when the customer closes the payment modal without completing payment. |
Optional fields
| Field | Type | Description |
|---|---|---|
currency | string | Currency code (NGN, USD). Defaults to NGN. |
reference | string | Your unique transaction reference. Auto-generated if omitted. Must be alphanumeric. |
channels | array | Payment methods to show: "card", "bank", or both. Shows all if omitted. |
customerFirstName | string | Customer's first name. |
customerLastName | string | Customer's last name. |
customerPhoneNumber | string | Customer's phone number. |
serviceCode | string | Pre-configured split settlement code from your dashboard. |
metadata | object | Custom data to attach to the transaction. |
Callback response
The callBack function receives a response object with the transaction details:
callBack: (response) => {
console.log(response.transRef); // Credo transaction reference
console.log(response.reference); // Your business reference
console.log(response.callbackUrl); // The callbackUrl you provided
}Always verify server-side
The callBack response confirms the widget flow completed, but it does not guarantee the payment was successful. Always call the verification endpoint from your server before fulfilling orders.
Full example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Credo Inline Payment</title>
</head>
<body>
<h1>Checkout</h1>
<p>Total: NGN 1,500.00</p>
<button onclick="handler.openIframe()">Pay NGN 1,500</button>
<script src="https://pay.credocentral.com/inline.js"></script>
<script>
const handler = CredoWidget.setup({
key: "0PUBxxxxxxxxxxxxxxxxxxxxxxxx",
email: "customer@example.com",
amount: 150000,
currency: "NGN",
channels: ["card", "bank"],
reference: "ORD-" + Date.now(),
customerFirstName: "Ciroma",
customerLastName: "Adekunle",
customerPhoneNumber: "08012345678",
metadata: {
customFields: [
{
variable_name: "order_id",
value: "ORD-001",
display_name: "Order ID",
},
],
},
callbackUrl: "https://yoursite.com/payment/success",
onClose: () => {
console.log("Widget closed");
},
callBack: (response) => {
// Redirect to your server to verify the transaction
window.location.href =
"/verify?transRef=" + response.transRef;
},
});
</script>
</body>
</html>Using with frameworks
The integration has two parts: loading the script (once, at the app entry point) and calling CredoWidget.setup() (in your payment component). Keep these separate - the script should load globally so CredoWidget is available anywhere.
React (Vite / CRA)
1. Add the script to your index.html:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My App</title>
</head>
<body>
<div id="root"></div>
<!-- Load Credo inline script globally -->
<script src="https://pay.credodemo.com/inline.js"></script>
</body>
</html>Use https://pay.credodemo.com/inline.js for sandbox and https://pay.credocentral.com/inline.js for production.
2. Create a pay button component:
// components/CredoPayButton.tsx
import { useCallback, useRef } from "react";
interface Props {
amount: number;
email: string;
reference?: string;
onSuccess?: (response: any) => void;
onClose?: () => void;
}
export function CredoPayButton({
amount,
email,
reference,
onSuccess,
onClose,
}: Props) {
const handlerRef = useRef<{ openIframe(): void } | null>(null);
const handleClick = useCallback(() => {
handlerRef.current = window.CredoWidget.setup({
key: import.meta.env.VITE_CREDO_PUBLIC_KEY,
email,
amount,
currency: "NGN",
channels: ["card", "bank"],
reference: reference ?? `ORD-${Date.now()}`,
callbackUrl: `${window.location.origin}/payment/success`,
onClose: () => {
console.log("Widget closed");
onClose?.();
},
callBack: (response: any) => {
onSuccess?.(response);
},
});
handlerRef.current.openIframe();
}, [amount, email, reference, onSuccess, onClose]);
return (
<button onClick={handleClick}>
Pay NGN {(amount / 100).toLocaleString()}
</button>
);
}Next.js (App Router)
1. Add the script to your root layout:
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
</body>
<Script
src="https://pay.credodemo.com/inline.js"
strategy="lazyOnload"
integrity="sha384-AMfqsefNpjx1BKaHHqKlQ1BMQOZatylARy5P1g4vGyf+V1DUi6qnmkp93GsZo1Pq%"
crossOrigin="anonymous"
/>
</html>
);
}| Prop | Value | Purpose |
|---|---|---|
strategy | "lazyOnload" | Loads after all other resources for better page performance |
integrity | "sha384-..." | Subresource integrity - ensures the script hasn't been tampered with |
crossOrigin | "anonymous" | Required when using integrity with a cross-origin script |
2. Create a pay button component:
// components/CredoPayButton.tsx
"use client";
import { useCallback, useRef } from "react";
interface Props {
amount: number;
email: string;
reference?: string;
onSuccess?: (response: any) => void;
onClose?: () => void;
}
export function CredoPayButton({
amount,
email,
reference,
onSuccess,
onClose,
}: Props) {
const handlerRef = useRef<{ openIframe(): void } | null>(null);
const handleClick = useCallback(() => {
handlerRef.current = window.CredoWidget.setup({
key: process.env.NEXT_PUBLIC_CREDO_KEY!,
email,
amount,
currency: "NGN",
channels: ["card", "bank"],
reference: reference ?? `ORD-${Date.now()}`,
callbackUrl: `${window.location.origin}/payment/success`,
onClose: () => {
console.log("Widget closed");
onClose?.();
},
callBack: (response: any) => {
onSuccess?.(response);
},
});
handlerRef.current.openIframe();
}, [amount, email, reference, onSuccess, onClose]);
return (
<button onClick={handleClick}>
Pay NGN {(amount / 100).toLocaleString()}
</button>
);
}Next.js (Pages Router)
1. Add the script to your custom _document:
// pages/_document.tsx
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
<script
src="https://pay.credodemo.com/inline.js"
integrity="sha384-AMfqsefNpjx1BKaHHqKlQ1BMQOZatylARy5P1g4vGyf+V1DUi6qnmkp93GsZo1Pq%"
crossOrigin="anonymous"
defer
/>
</body>
</Html>
);
}2. Use the same pay button component from the App Router example above (without the "use client" directive, since Pages Router components are client-side by default).
TypeScript support
If you're using TypeScript, add type definitions for the global CredoWidget object. Create a credo-widget.d.ts file in your project:
interface CredoWidgetPaymentProps {
/** Public key (0PUB... for sandbox, 1PUB... for production) */
key?: string;
/** Customer email address */
email: string;
/** Amount in lowest currency unit */
amount: number;
/** Pre-generated payment link (alternative to key) */
paymentLink?: string;
/** URL to redirect after payment */
callbackUrl?: string;
/** Called when user closes the payment widget */
onClose: () => void;
/** Called on completed transaction */
callBack: (data: any) => void;
/** Additional transaction fields */
[key: string]: any;
}
interface CredoWidgetInstance {
/** Open the payment modal */
openIframe(): void;
/** Redirect to the payment page */
redirect(url?: string): void;
}
interface CredoWidgetStatic {
setup(props: CredoWidgetPaymentProps): CredoWidgetInstance;
}
declare global {
interface Window {
CredoWidget: CredoWidgetStatic;
}
var CredoWidget: CredoWidgetStatic;
}
export {};The key field is optional in the type definition because you can alternatively provide a paymentLink for pre-generated payment links. In practice, most integrations will use key.
Metadata
Attach custom data to the transaction using the metadata field:
metadata: {
bankAccount: "0114877128",
customFields: [
{
variable_name: "gender",
value: "Male",
display_name: "Gender",
},
{
variable_name: "plan",
value: "Premium",
display_name: "Subscription Plan",
},
],
}Metadata is returned in the verification response and webhook payload. Use customFields for structured key-value pairs that are also visible in the dashboard.
Split settlement
To split the transaction amount between multiple accounts, pass a serviceCode referencing a pre-configured split rule from your dashboard:
const handler = CredoWidget.setup({
key: "0PUBxxxxxxxxxxxxxxxxxxxxxxxx",
email: "customer@example.com",
amount: 100000,
currency: "NGN",
serviceCode: "00R284us01",
// ... other fields
});See the Settlement System guide for configuring split rules.
Popup vs redirect
| Credo Popup | Redirect (API) | |
|---|---|---|
| Customer stays on your site | Yes | No |
| Code complexity | Low (script tag + config) | Medium (server-side API calls) |
| Customization | Widget styling | Full control |
| Server required | Only for verification | Yes (initialize + verify) |
| Best for | Websites, landing pages | Apps, custom flows |
Troubleshooting
Widget doesn't open
- Verify the
inline.jsscript has loaded (check the Network tab) - Ensure
handler.openIframe()is called afterCredoWidget.setup()completes - Check the browser console for errors
Payment completes but callBack doesn't fire
- Ensure
callBack(with capital B) is spelled correctly - notcallback - Check that the function doesn't throw an error
"Invalid key" error
- Confirm you're using a public key (not secret key)
- Sandbox keys (
0PUB...) only work with the sandbox script - Production keys (
1PUB...) only work with the production script
Next steps
Was this page helpful?
Last updated on
