Integration → Payment Widget SDK
Payment Widget SDK
The Vittas payment widget is a drop-in checkout overlay. Your backend creates a session and returns aclientSecret(cs_…). Pass it to VittasPay.init()and the widget handles everything — virtual account display, countdown timer, real-time confirmation.
How it works
The widget uses a server-side session model. Amount and currency are locked on your backend before the widget opens, so they cannot be tampered with from the browser:
- Your backend — calls
POST /api/widget/sessionswith your secret key, amount, currency, and payer details. Receives aclientSecret. - Your frontend — receives the
clientSecretand callsVittasPay.init({ clientSecret }). The widget opens as a full-screen overlay. - Widget — provisions a unique temporary virtual account for this session and starts the countdown timer.
- Payer — transfers the exact amount to the displayed account number.
- Backend — the bank fires a webhook when the transfer arrives. Vittas validates it, marks the session
successful, and pushes a real-time update to the open widget. - Widget — renders the success screen and fires your
onSuccesscallback.
Secret key vs. client secret
Your secret key (sk_…) lives on your server only — never send it to the browser. The client secret (cs_…) is a short-lived, single-use token returned by the session endpoint — safe to pass to the frontend.On WordPress or WooCommerce?
Do not wire this up by hand. The official WordPress plugin does the session creation, webhook verification and order settlement for you — install it and paste your secret key.Installation
Add one script tag to your page — no build step or npm package required. Load from the CDN host that matches the environment your secret key belongs to:
| Environment | Secret key prefix | Script URL |
|---|---|---|
| Production | sk_live_ | https://cdn.core.vittasinternational.com/latest/widget.js |
| Test / development | sk_test_ | https://dev-cdn.core.vittasinternational.com/latest/widget.js |
<!-- Production (sk_live_) --> <script src="https://cdn.core.vittasinternational.com/latest/widget.js"></script> <!-- Test / development (sk_test_) --> <script src="https://dev-cdn.core.vittasinternational.com/latest/widget.js"></script>
Pin a version in production
/latest/widget.js always serves the newest release. For stability, pin a specific version by swapping latest for a version tag — e.g. /v0.1.0/widget.js — on either host.Embed the widget
Call VittasPay.init() with the clientSecret your backend returned. The widget opens immediately as a full-screen overlay — no mount point or container element needed.
<script src="https://dev-cdn.core.vittasinternational.com/latest/widget.js"></script> <button id="pay-btn">Pay ₦5,000</button> <script> document.getElementById('pay-btn').addEventListener('click', async () => { // Fetch a fresh clientSecret from YOUR backend (never hard-code it) const { clientSecret } = await fetch('/api/checkout', { method: 'POST' }) .then(r => r.json()); window.VittasPay.init({ clientSecret, onSuccess(ref) { // ref: { id, reference, status } window.location.replace('/order/success?ref=' + ref.reference); }, onError(err) { console.error('Payment failed:', err.message); }, onCancel() { console.log('Payment cancelled'); }, }); }); </script>
Temporary virtual account
When the widget initialises it provisions a unique, single-use virtual account. The account is amount-controlled at the bank level — only a transfer for precisely the session amount is credited, so you never need to handle over- or under-payment logic.Configuration options
| Option | Type | Required | Description |
|---|---|---|---|
| clientSecret | string | Yes | Session client secret (cs_…) returned by POST /api/widget/sessions. Never hard-code — fetch it fresh from your backend. |
| onSuccess | function | Yes | Called when the payment is confirmed. Receives { id, reference, status }. |
| onError | function | Yes | Called on widget or network errors. Receives { message }. |
| onCancel | function | No | Called when the payer explicitly cancels or closes the widget. |
Callback payloads
// onSuccess receives: interface SuccessPayload { id: string; // Session ID reference: string; // Payment reference status: string; // "successful" }
// onError receives: interface ErrorPayload { message: string; }
Widget states
The widget transitions through these screens automatically:
| State | Description |
|---|---|
| INITIALIZING | Loading — widget is bootstrapping the session. |
| AWAITING_PAYMENT | Bank transfer details displayed with a countdown timer. |
| CONFIRMING | Verifying the payment with the backend. |
| SUCCESS | Payment confirmed — auto-redirects to the merchant after 3s. onSuccess is fired. |
| MISMATCH | A transfer arrived but the amount differed. The merchant is notified. |
| EXPIRED | The timer ran out before a transfer was received. Retry or return to merchant. |
| CANCELLED | Payer cancelled — no charge made. onCancel is fired. |
| ERROR | Verification or network error. onError is fired. |
Real-time updates
The widget receives payment status updates in real time — there is no polling. It transitions to SUCCESS within milliseconds of the bank delivering the transfer notification to the backend.Next.js / React
Two options — use the official React npm package, or load the CDN script manually.
Option A — @vittascore/payment-widget-react (recommended)
npm install @vittascore/payment-widget-react
import { PaymentWidget } from '@vittascore/payment-widget-react'; // clientSecret comes from your server (server component, loader, API route, etc.) export default function CheckoutPage({ clientSecret }: { clientSecret: string }) { return ( <PaymentWidget mode="TEST" clientSecret={clientSecret} onSuccess={(ref) => console.log('paid', ref.reference)} onCancel={() => console.log('cancelled')} onError={(err) => console.error(err.message)} /> ); }
Option B — CDN script via next/script
'use client'; import Script from 'next/script'; // clientSecret is fetched server-side and passed as a prop export function PayButton({ clientSecret }: { clientSecret: string }) { const handlePay = () => { window.VittasPay?.init({ clientSecret, onSuccess(ref) { window.location.replace('/order/success?ref=' + ref.reference); }, onError(err) { console.error(err.message); }, onCancel() { // stay on page }, }); }; return ( <> <Script src="https://dev-cdn.core.vittasinternational.com/latest/widget.js" /> <button onClick={handlePay}>Pay Now</button> </> ); }
Content Security Policy
If your site uses a Content Security Policy, add the following directives. The widget renders inside a sandboxed <iframe>, so frame-src is required. In test, swap the CDN host for https://dev-cdn.core.vittasinternational.com.
Content-Security-Policy: script-src 'self' https://cdn.core.vittasinternational.com; connect-src 'self' https://api.core.vittasinternational.com; frame-src 'self' https://cdn.core.vittasinternational.com;