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

FieldTypeDescription
keystringYour public key. Use sandbox key (0PUB...) for testing, production key (1PUB...) for live.
emailstringCustomer's email address.
amountnumberAmount in lowest currency unit (kobo for NGN, cents for USD). 150000 = NGN 1,500.
callbackUrlstringURL to redirect to after payment completes.
callBackfunctionCalled when the transaction completes. Receives the transaction response object.
onClosefunctionCalled when the customer closes the payment modal without completing payment.

Optional fields

FieldTypeDescription
currencystringCurrency code (NGN, USD). Defaults to NGN.
referencestringYour unique transaction reference. Auto-generated if omitted. Must be alphanumeric.
channelsarrayPayment methods to show: "card", "bank", or both. Shows all if omitted.
customerFirstNamestringCustomer's first name.
customerLastNamestringCustomer's last name.
customerPhoneNumberstringCustomer's phone number.
serviceCodestringPre-configured split settlement code from your dashboard.
metadataobjectCustom 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>
  );
}
PropValuePurpose
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.

Credo PopupRedirect (API)
Customer stays on your siteYesNo
Code complexityLow (script tag + config)Medium (server-side API calls)
CustomizationWidget stylingFull control
Server requiredOnly for verificationYes (initialize + verify)
Best forWebsites, landing pagesApps, custom flows

Troubleshooting

Widget doesn't open

  • Verify the inline.js script has loaded (check the Network tab)
  • Ensure handler.openIframe() is called after CredoWidget.setup() completes
  • Check the browser console for errors

Payment completes but callBack doesn't fire

  • Ensure callBack (with capital B) is spelled correctly - not callback
  • 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

On this page