VittasVittas Docs

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:

  1. Your backend — calls POST /api/widget/sessions with your secret key, amount, currency, and payer details. Receives a clientSecret.
  2. Your frontend — receives the clientSecret and calls VittasPay.init({ clientSecret }). The widget opens as a full-screen overlay.
  3. Widget — provisions a unique temporary virtual account for this session and starts the countdown timer.
  4. Payer — transfers the exact amount to the displayed account number.
  5. 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.
  6. Widget — renders the success screen and fires your onSuccess callback.

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:

EnvironmentSecret key prefixScript URL
Productionsk_live_https://cdn.core.vittasinternational.com/latest/widget.js
Test / developmentsk_test_https://dev-cdn.core.vittasinternational.com/latest/widget.js
html
<!-- 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.

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

OptionTypeRequiredDescription
clientSecretstringYesSession client secret (cs_…) returned by POST /api/widget/sessions. Never hard-code — fetch it fresh from your backend.
onSuccessfunctionYesCalled when the payment is confirmed. Receives { id, reference, status }.
onErrorfunctionYesCalled on widget or network errors. Receives { message }.
onCancelfunctionNoCalled when the payer explicitly cancels or closes the widget.

Callback payloads

onSuccess payload
typescript
// onSuccess receives:
interface SuccessPayload {
  id:        string;   // Session ID
  reference: string;   // Payment reference
  status:    string;   // "successful"
}
onError payload
typescript
// onError receives:
interface ErrorPayload {
  message: string;
}

Widget states

The widget transitions through these screens automatically:

StateDescription
INITIALIZINGLoading — widget is bootstrapping the session.
AWAITING_PAYMENTBank transfer details displayed with a countdown timer.
CONFIRMINGVerifying the payment with the backend.
SUCCESSPayment confirmed — auto-redirects to the merchant after 3s. onSuccess is fired.
MISMATCHA transfer arrived but the amount differed. The merchant is notified.
EXPIREDThe timer ran out before a transfer was received. Retry or return to merchant.
CANCELLEDPayer cancelled — no charge made. onCancel is fired.
ERRORVerification 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)

bash
npm install @vittascore/payment-widget-react
app/checkout/page.tsx
tsx
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

app/checkout/PayButton.tsx
tsx
'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.

bash
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;