Skip to content

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.

Every Sanwo provider is a SanwoProviderDefinition — an object with metadata and an HTML template. When you call sanwo(options), the engine:

  1. Injects a bridge into your template ({{sanwoBridge}}) — this defines the sanwoCallback() function
  2. Injects the checkout params ({{params}}) — public key, amount, currency, email, etc.
  3. Renders the template in an iframe
  4. 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.

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
}

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

Let’s walk through how Paystack’s provider is built. This is the exact pattern you’d follow for any gateway.

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>`;
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"],
};
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.

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.

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"],
};

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 the onLoad callback for the user.
  • Always handle script load failures with onerror — call sanwoCallback('error', { message }).
  • Include reference in success data — it’s used to populate result.reference.
  • Include raw in success data — the full provider response is available as result.raw for users who need provider-specific fields.
  • Use params for everything — all standard checkout fields and sanwoProviderOptions are available on the params object.
  • Wrap initPayment in try/catch — unexpected SDK errors should call sanwoCallback('error'), not crash silently.

Custom providers work with every Sanwo SDK. The provider object is the same everywhere — write the template once, use it on any platform.

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 { 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" },
})} />
);
}

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>

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.