Custom Providers
Sanwo ships with providers for Paystack, Flutterwave, Razorpay, Monnify, and Interswitch — but you can build a provider for any payment gateway. A provider is just a plain object with an HTML template. No SDK needed.
How it works
Section titled “How it works”Every Sanwo provider is a SanwoProviderDefinition — an object with metadata and an HTML template. When you call sanwo(options), the engine:
- Injects a bridge into your template (
{{sanwoBridge}}) — this defines thesanwoCallback()function - Injects the checkout params (
{{params}}) — public key, amount, currency, email, etc. - Renders the template in an iframe
- Listens for
sanwoCallback()events and resolves the checkout promise
Your template just needs to load the provider’s SDK, start the payment, and call sanwoCallback() with the right events.
The provider interface
Section titled “The provider interface”interface SanwoProviderDefinition { id: string; // unique identifier name: string; // machine-readable name displayName: string; // human-readable name template: string; // HTML template string website?: string; // provider's website documentation?: string; // link to provider docs supportedCurrencies?: string[]; // ISO 4217 codes supportedCountries?: string[]; // ISO 3166-1 alpha-2 codes paymentMethods?: string[]; // e.g. ["card", "bank_transfer"] amountInMinorUnit?: boolean; // true = pass amount as-is, false = convert from minor to major}The callback events
Section titled “The callback events”Your template communicates back to Sanwo by calling sanwoCallback(event, data):
| Event | Data | When to call |
|---|---|---|
loaded |
{} |
Provider SDK loaded and payment UI ready |
success |
{ reference, raw } |
Payment completed successfully |
cancelled |
{} |
User closed the checkout |
closed |
{} |
Checkout UI dismissed |
error |
{ message } |
Something went wrong |
Building a provider: Paystack example
Section titled “Building a provider: Paystack example”Let’s walk through how Paystack’s provider is built. This is the exact pattern you’d follow for any gateway.
Step 1: Write the HTML template
Section titled “Step 1: Write the HTML template”The template is a full HTML page that loads the provider’s JavaScript SDK, initializes a payment, and calls sanwoCallback() for each lifecycle event.
const template = `<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sanwo Checkout</title></head><body> <script> // 1. The bridge is injected here — it defines sanwoCallback() {{sanwoBridge}}
// 2. Checkout params are injected here var params = {{params}};
function initPayment() { try { // 3. Use the provider's SDK var paystack = new PaystackPop(); var config = { key: params.publicKey, email: params.email, amount: params.amount, currency: params.currency,
// 4. Map provider events to Sanwo callbacks onSuccess: function(response) { sanwoCallback('success', { reference: response.reference, transaction_id: response.trans, message: response.message, raw: response }); }, onCancel: function() { sanwoCallback('cancelled', {}); }, onClose: function() { sanwoCallback('closed', {}); } };
// 5. Pass through any provider-specific options if (params.reference) config.ref = params.reference; if (params.channels) config.channels = params.channels; if (params.metadata) config.metadata = params.metadata;
// 6. Signal ready and start sanwoCallback('loaded', {}); paystack.checkout(config); } catch(e) { sanwoCallback('error', { message: e.message }); } }
// 7. Load the provider SDK dynamically (not static <script>) var script = document.createElement('script'); script.src = 'https://js.paystack.co/v2/inline.js'; script.onload = initPayment; script.onerror = function() { sanwoCallback('error', { message: 'Failed to load Paystack SDK' }); }; document.body.appendChild(script); <\/script></body></html>`;Step 2: Define the provider
Section titled “Step 2: Define the provider”import type { SanwoProviderDefinition } from "@sanwohq/types";
export const myPaystackProvider: SanwoProviderDefinition = { id: "paystack", name: "paystack", displayName: "Paystack", template, website: "https://paystack.com", documentation: "https://paystack.com/docs", amountInMinorUnit: true, supportedCurrencies: ["NGN", "GHS", "ZAR", "USD"], paymentMethods: ["card", "bank", "bank_transfer", "ussd"],};Step 3: Use it
Section titled “Step 3: Use it”import { createSanwo } from "@sanwohq/web";import { myPaystackProvider } from "./my-paystack-provider";
const sanwo = createSanwo({ provider: myPaystackProvider, publicKey: "pk_test_xxxxxxxxxxxx",});
const result = await sanwo({ amount: 500000, currency: "NGN", customer: { email: "customer@example.com" },});That’s it — Sanwo handles the iframe, bridge injection, event listening, and promise resolution. Your provider just needs the template.
Template params
Section titled “Template params”Your template receives these params automatically via {{params}}:
| Param | Type | Always present | Source |
|---|---|---|---|
publicKey |
string |
Yes | createSanwo({ publicKey }) |
amount |
number |
Yes | checkout({ amount }) — auto-converted if amountInMinorUnit: false |
currency |
string |
Yes | checkout({ currency }) |
reference |
string |
Yes | checkout({ reference }) or auto-generated |
email |
string |
Yes | checkout({ customer: { email } }) |
firstName |
string |
If provided | customer.firstName |
lastName |
string |
If provided | customer.lastName |
phone |
string |
If provided | customer.phone |
metadata |
object |
If provided | checkout({ metadata }) |
description |
string |
If provided | checkout({ description }) |
* |
unknown |
If provided | Everything from sanwoProviderOptions is spread into params |
Provider-specific options passed via sanwoProviderOptions are merged directly into params. For example:
await sanwo({ amount: 500000, currency: "NGN", customer: { email: "user@example.com" }, sanwoProviderOptions: { channels: ["card"], splitCode: "SPL_xxx", },});In the template, params.channels and params.splitCode are available.
Building your own
Section titled “Building your own”Here’s the minimal skeleton for any provider:
import type { SanwoProviderDefinition } from "@sanwohq/types";
const template = `<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sanwo Checkout</title></head><body> <script> {{sanwoBridge}} var params = {{params}};
function initPayment() { try { // Initialize your provider SDK with params // Map success/cancel/error to sanwoCallback sanwoCallback('loaded', {});
YourSDK.pay({ key: params.publicKey, amount: params.amount, email: params.email, currency: params.currency, onSuccess: function(res) { sanwoCallback('success', { reference: res.ref, raw: res }); }, onCancel: function() { sanwoCallback('cancelled', {}); } }); } catch(e) { sanwoCallback('error', { message: e.message }); } }
var s = document.createElement('script'); s.src = 'https://cdn.yourprovider.com/sdk.js'; s.onload = initPayment; s.onerror = function() { sanwoCallback('error', { message: 'Failed to load SDK' }); }; document.body.appendChild(s); <\/script></body></html>`;
export const myProvider: SanwoProviderDefinition = { id: "my-provider", name: "my-provider", displayName: "My Provider", template, amountInMinorUnit: true, supportedCurrencies: ["USD", "NGN"],};The amountInMinorUnit flag
Section titled “The amountInMinorUnit flag”If your provider expects amounts in minor units (kobo, cents), set amountInMinorUnit: true — the amount is passed through as-is.
If your provider expects major units (naira, dollars), set amountInMinorUnit: false — Sanwo automatically divides by 100 before injecting into params.
| Provider expects | Set amountInMinorUnit to |
checkout({ amount: 500000 }) becomes |
|---|---|---|
500000 (kobo) |
true |
params.amount = 500000 |
5000.00 (naira) |
false |
params.amount = 5000 |
- Always call
sanwoCallback('loaded', {})before starting the payment — this triggers theonLoadcallback for the user. - Always handle script load failures with
onerror— callsanwoCallback('error', { message }). - Include
referencein success data — it’s used to populateresult.reference. - Include
rawin success data — the full provider response is available asresult.rawfor users who need provider-specific fields. - Use
paramsfor everything — all standard checkout fields andsanwoProviderOptionsare available on the params object. - Wrap
initPaymentin try/catch — unexpected SDK errors should callsanwoCallback('error'), not crash silently.
Using with every SDK
Section titled “Using with every SDK”Custom providers work with every Sanwo SDK. The provider object is the same everywhere — write the template once, use it on any platform.
Web frameworks
Section titled “Web frameworks”import { createSanwo } from "@sanwohq/web";import { myProvider } from "./my-provider";
const sanwo = createSanwo({ provider: myProvider, publicKey: "pk_test_xxx" });
const result = await sanwo({ amount: 500000, currency: "NGN", customer: { email: "customer@example.com" },});import { SanwoReactProvider, useSanwoCheckout } from "@sanwohq/react";import { myProvider } from "./my-provider";
function App() { return ( <SanwoReactProvider provider={myProvider} publicKey="pk_test_xxx"> <Checkout /> </SanwoReactProvider> );}
function Checkout() { const { checkout } = useSanwoCheckout();
return ( <button onClick={() => checkout({ amount: 500000, currency: "NGN", customer: { email: "customer@example.com" }, })}> Pay with My Gateway </button> );}import { SanwoPlugin } from "@sanwohq/vue";import { myProvider } from "./my-provider";
app.use(SanwoPlugin, { provider: myProvider, publicKey: "pk_test_xxx" });<script setup>import { useSanwoCheckout } from "@sanwohq/vue";const { checkout } = useSanwoCheckout();</script>
<template> <button @click="checkout({ amount: 500000, currency: 'NGN', customer: { email: 'customer@example.com' }, })"> Pay with My Gateway </button></template><script> import { createSanwoContext } from "@sanwohq/svelte"; import { myProvider } from "./my-provider";
createSanwoContext({ provider: myProvider, publicKey: "pk_test_xxx" });</script>
<slot /><script> import { createSanwoCheckout } from "@sanwohq/svelte"; const store = createSanwoCheckout();</script>
<button on:click={() => store.checkout({ amount: 500000, currency: "NGN", customer: { email: "customer@example.com" },})}> Pay with My Gateway</button>import { Component, OnDestroy } from "@angular/core";import { createSanwo, type SanwoInstance } from "@sanwohq/web";import { myProvider } from "./my-provider";
@Component({ selector: "app-checkout", standalone: true, template: `<button (click)="pay()">Pay with My Gateway</button>`,})export class CheckoutComponent implements OnDestroy { private sanwo: SanwoInstance;
constructor() { this.sanwo = createSanwo({ provider: myProvider, publicKey: "pk_test_xxx" }); }
async pay() { const result = await this.sanwo({ amount: 500000, currency: "NGN", customer: { email: "customer@example.com" }, }); }
ngOnDestroy() { this.sanwo.destroy(); }}Mobile
Section titled “Mobile”import { SanwoProvider, useSanwoCheckout } from "@sanwohq/react-native";import { myProvider } from "./my-provider";
function App() { return ( <SanwoProvider provider={myProvider} publicKey="pk_test_xxx"> <Checkout /> </SanwoProvider> );}
function Checkout() { const checkout = useSanwoCheckout();
return ( <Button title="Pay with My Gateway" onPress={() => checkout({ amount: 500000, currency: "NGN", customer: { email: "customer@example.com" }, })} /> );}import 'package:sanwo_flutter/sanwo_flutter.dart';
final myProvider = SanwoProviderDefinition( id: 'my-provider', name: 'my-provider', displayName: 'My Provider', template: myTemplateString, amountInMinorUnit: true, supportedCurrencies: ['USD', 'NGN'],);
final sanwo = Sanwo(provider: myProvider, publicKey: 'pk_test_xxx');
final result = await sanwo.checkout( CheckoutOptions( amount: 500000, currency: 'NGN', customer: Customer(email: 'customer@example.com'), ),);import Sanwo
let myProvider = SanwoProviderDefinition( id: "my-provider", name: "my-provider", displayName: "My Provider", template: myTemplateString, amountInMinorUnit: true, supportedCurrencies: ["USD", "NGN"])
let sanwo = Sanwo(provider: myProvider, publicKey: "pk_test_xxx")
let result = try await sanwo.checkout( amount: 500000, currency: "NGN", customer: Customer(email: "customer@example.com"))import com.sanwohq.android.Sanwoimport com.sanwohq.android.SanwoProviderDefinition
val myProvider = SanwoProviderDefinition( id = "my-provider", name = "my-provider", displayName = "My Provider", template = myTemplateString, amountInMinorUnit = true, supportedCurrencies = listOf("USD", "NGN"))
val sanwo = Sanwo(provider = myProvider, publicKey = "pk_test_xxx")
val result = sanwo.checkout( amount = 500000, currency = "NGN", customer = Customer(email = "customer@example.com"))Server-side
Section titled “Server-side”Server-side SDKs use the Sanwo embed script, so custom providers work via template-url or inline template — no provider object import needed.
Host your template file and use the template-url prop:
<x-sanwo-checkout :amount="500000" email="customer@example.com" provider="custom" public-key="pk_test_xxx" template-url="https://example.com/my-provider-template.html" button-text="Pay with My Gateway"/>Or use the programmatic approach for full control:
<x-sanwo-scripts />
<button id="pay-btn">Pay with My Gateway</button>
<script> var sanwo = Sanwo.create({ provider: 'custom', publicKey: '{{ env("SANWO_PUBLIC_KEY") }}', templateUrl: 'https://example.com/my-provider-template.html', providerId: 'my-gateway', });
document.getElementById('pay-btn').addEventListener('click', async function() { var result = await sanwo.checkout({ amount: 500000, currency: 'NGN', customer: { email: 'customer@example.com' }, }); });</script><sanwo-checkout amount="500000" email="customer@example.com" provider="custom" public-key="pk_test_xxx" template-url="https://example.com/my-provider-template.html" button-text="Pay with My Gateway" />Or programmatically:
<sanwo-scripts />
<button id="pay-btn">Pay with My Gateway</button>
<script> var sanwo = Sanwo.create({ provider: 'custom', publicKey: '@Configuration["Sanwo:PublicKey"]', templateUrl: 'https://example.com/my-provider-template.html', providerId: 'my-gateway', });
document.getElementById('pay-btn').addEventListener('click', async function() { var result = await sanwo.checkout({ amount: 500000, currency: 'NGN', customer: { email: 'customer@example.com' }, }); });</script>No-code / Embed
Section titled “No-code / Embed”No JavaScript imports needed — just data attributes and a script tag. Perfect for static sites, CMS pages, and no-code platforms.
With a hosted template URL:
<script src="https://cdn.jsdelivr.net/npm/@sanwohq/embed/dist/sanwo.global.js"></script>
<button data-sanwo-provider="custom" data-sanwo-key="pk_test_xxx" data-sanwo-template-url="https://example.com/my-provider-template.html" data-sanwo-amount="500000" data-sanwo-currency="NGN" data-sanwo-email="customer@example.com"> Pay with My Gateway</button>With an inline template (via JavaScript):
<script src="https://cdn.jsdelivr.net/npm/@sanwohq/embed/dist/sanwo.global.js"></script>
<script> var sanwo = Sanwo.create({ provider: 'custom', publicKey: 'pk_test_xxx', template: '<!DOCTYPE html>...', // your full template HTML providerId: 'my-gateway', });
document.getElementById('pay-btn').addEventListener('click', async function() { var result = await sanwo.checkout({ amount: 500000, currency: 'NGN', customer: { email: 'customer@example.com' }, });
if (result.status === 'successful') { alert('Paid! Reference: ' + result.reference); } });</script>
<button id="pay-btn">Pay with My Gateway</button>Register once, use everywhere:
<script> Sanwo.registerProvider('my-gateway', Sanwo.createCustomProvider({ template: '<!DOCTYPE html>...', id: 'my-gateway', name: 'my-gateway', displayName: 'My Gateway', amountInMinorUnit: true, }));</script>
<!-- Now use it like any built-in provider --><button data-sanwo-provider="my-gateway" data-sanwo-key="pk_test_xxx" data-sanwo-amount="500000" data-sanwo-currency="NGN" data-sanwo-email="customer@example.com"> Pay with My Gateway</button>This works on any no-code platform — WordPress, Shopify, Webflow, Wix, Squarespace, Bubble, Framer, and anywhere you can add a <script> tag. See the no-code guides for platform-specific instructions.