Paysio.js
Client-side SDK for collecting payments securely.
Paysio.js is a client-side SDK for collecting payments. Mount secure card input fields with paysio.mountCardInputs(), then call paysio.createToken() to tokenize securely. Card data never touches your backend. You get back a payment token that you send to your server to vault or charge.
How it works
- Your page loads
paysio.jsand creates an instance with your publishable key. - Call
paysio.mountCardInputs()to render secure card fields in your page. - When the user submits, call
paysio.createToken(). Paysio validates the card and returns a token. - Send that token to your server, which uses a secret key to vault or charge via the Paysio API.
Before you start
- A Paysio account with payment gateway credentials configured (Settings → Payments).
- A publishable API key (
pk_test_...orpk_live_...) — create one in Settings → API Keys. - A secret API key (
sk_test_...orsk_live_...) for your backend to call the Paysio API.
Your page must be served over HTTPS
Card fields will render on HTTP but tokenization will silently fail. Use a tool like cloudflared tunnel for local HTTPS testing.
| Script URL | API key prefix |
|---|---|
| https://paysio.com/paysio.js | pk_live_... |
Quickstart
Include the script
Add the Paysio.js script tag to your HTML. This exposes a global Paysio function. The SDK automatically calls the correct API based on which script URL you use.
<script src="https://paysio.com/paysio.js"></script>Initialize
Call Paysio() with your publishable key. The SDK auto-detects the correct API endpoint from the script URL, so no extra configuration is needed.
// Production (with production script tag)
const paysio = Paysio('pk_live_your_publishable_key');
// Sandbox testing (with staging script tag)
const paysio = Paysio('pk_test_your_publishable_key');
// Custom domain or proxy (override auto-detected base)
const paysio = Paysio('pk_test_...', {
apiBase: 'https://your-domain.com/api/v1'
});The apiBase option is only needed if you're proxying API calls through your own backend or using a custom domain. Otherwise, the SDK handles it automatically. Never use your secret key (sk_*) in client-side code.
Mount card inputs and tokenize
Use mountCardInputs() to render secure card fields in your page. This works with all payment processors (Stripe, NMI, Paysio Debit & Payouts). When the user submits, call paysio.createToken() to tokenize.
Gateway is auto-detected — you never hardcode it
mountCardInputs() and createToken() read the workspace's active gateway from GET /v1/tokenization-key and render the right fields automatically — Paysio Hosted Fields (secure iframes on the Paysio origin) for NMI, Debit & Payouts and Stripe Connect workspaces; Stripe Elements only for Stripe workspaces on their own API keys — with no per-gateway branches in your code. Either way, card data is typed inside an iframe and never enters your page's DOM, keeping you in the lightest PCI scope (SAQ A). When the workspace switches processors in the dashboard, your integration adapts on the next load. (If you mount before switching, call paysio.reset() to re-detect without a full reload.)
The mounted fields always include the CVC on every gateway, and the CVC travels inside the token — so never render your own CVC field and never pass cvc when charging a fresh token. (The cvc body param on POST /charges exists only for charging a saved card later on Debit & Payouts.)
Need the gateway server-side (e.g. to know whether settlement is asynchronous)? GET /v1/tokenization-key returns processor_type ("nmi" | "stripe" | "aptpay"). It's safe to call with a publishable key.
When enabled for your environment, standalone card fields automatically report anonymous visit, interaction, and completed-payment activity to the Paysio dashboard. Tracking is fail-open and never blocks mounting, tokenization, 3DS, or charging.
// Detect the workspace's gateway from your server
const res = await fetch('https://paysio.com/api/v1/tokenization-key', {
headers: { Authorization: 'Bearer pk_live_YOUR_KEY' },
});
const { data } = await res.json();
// data.processor_type -> "nmi" | "stripe" | "aptpay"
// e.g. aptpay charges settle asynchronously (status "pending" -> webhook)
const asyncSettlement = data.processor_type === 'aptpay';<form id="payment-form">
<div id="card-fields"></div>
<button type="submit">Pay $29.99</button>
</form>
<script>
const paysio = Paysio('pk_test_YOUR_KEY');
// Mount secure card fields (renders processor-appropriate inputs)
await paysio.mountCardInputs('#card-fields', {
onReady: () => console.log('Card fields ready'),
onChange: ({ complete, error }) => {
document.querySelector('button').disabled = !complete;
},
});
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
try {
const { token, card } = await paysio.createToken();
console.log('Token:', token); // "pm_xxx" (Stripe) or "ptok_abc123..." (NMI, Debit & Payouts)
console.log('Card:', card.brand, card.last4); // "visa", "1111"
// Send token to YOUR server
await fetch('/your-server/process-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
} catch (err) {
console.error(err.message);
}
});
</script>For custom layouts, mount each field into separate containers:
<div id="card-number"></div>
<div id="card-expiry"></div>
<div id="card-cvc"></div>
<script>
await paysio.mountCardInputs({
cardNumber: '#card-number',
cardExpiry: '#card-expiry',
cardCvc: '#card-cvc',
});
</script>Customize colors (e.g. for dark mode) with the simple style option — works on every gateway:
await paysio.mountCardInputs('#card-fields', {
style: {
backgroundColor: '#1a1a1a',
textColor: '#ffffff',
placeholderColor: '#666666',
borderColor: '#333333',
focusBorderColor: '#3b82f6',
iconColor: '#666666',
},
});For full control on NMI and Debit & Payouts hosted fields, use the appearance option — every part of the inputs is themeable, including custom fonts:
await paysio.mountCardInputs('#card-fields', {
appearance: {
variables: {
// Typography
fontFamily: '"Inter", sans-serif',
fontSize: '15px',
fontWeight: '450',
// Colors
colorText: '#f4f4f5',
colorTextPlaceholder: '#71717a',
colorBackground: '#18181b',
colorBorder: '#3f3f46',
colorBorderFocus: '#6366f1',
colorBorderInvalid: '#ef4444',
colorTextInvalid: '#ef4444',
colorIcon: '#71717a',
caretColor: '#6366f1',
selectionColor: 'rgba(99,102,241,0.3)',
// Shape & layout
borderRadius: '12px',
borderWidth: '1px',
inputHeight: '40px',
inputPaddingX: '12px',
boxShadow: 'none',
boxShadowFocus: '0 0 0 3px rgba(99,102,241,0.25)',
rowGap: '8px', // > 0 renders 3 separate boxes; 0 = fused group (default)
showLockIcon: false, // hide the CVC lock icon
},
// Fine-grained CSS rules on whitelisted selectors
rules: {
'.input::placeholder': { fontStyle: 'italic' },
'.field--focused': { transform: 'scale(1.01)' },
},
// Load custom fonts inside the secure iframe (https only)
fonts: [
{ cssSrc: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500' },
],
},
// Custom placeholder text
placeholders: { number: 'Card number', expiry: 'MM/YY', cvc: 'Security code' },
});appearance.variables values are plain CSS values. Because the fields live inside an iframe, page CSS does not cascade into them — custom fonts must be loaded via appearance.fonts. The legacy style keys map onto the same variables and can be mixed with appearance (appearance wins).
On Paysio Debit & Payouts workspaces
mountCardInputs()renders number, expiry, and CVC fields — exactly like every other gateway. The same mount /createToken()code works unchanged. Do not add your own CVC field: the CVC is captured by the mounted fields and travels inside the token.createToken()returns aptok_token (single-use, expires after 15 minutes). Use it withPOST /charges— without acvcbody param — or with payment methods, payouts, and account checks.- Raw tokenization without mounted fields also works:
createToken({ number, exp_month, exp_year, cvv }). - Apple Pay / Google Pay wallets are not available on Debit & Payouts —
mountWallets()is a no-op. - See the Debit & Payouts guide for sandbox test cards and the settlement model.
Vault the token on your server
On your backend, use the token with the Paysio API to save the card to a customer vault. This requires your secret key.
// Your server (Node.js example)
const response = await fetch('https://paysio.com/api/v1/customers/{customer_id}/payment-methods', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_secret_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
payment_token: token, // The token from Paysio.js
}),
});
const { data } = await response.json();
// data.billing_id — reference for this specific card
// data.customer_vault_id — the customer's vault IDCharge the saved card
Once a card is vaulted, charge it anytime using the Charges API:
const charge = await fetch('https://paysio.com/api/v1/charges', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_secret_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
customer_id: 'customer-uuid',
amount: 2999, // $29.99 in cents
currency: 'USD',
description: 'Monthly subscription',
}),
});Embedded Checkout
Use mountCheckout() when you want Paysio's complete payment block inside your page. It includes email, phone, Quick Checkout, saved and new cards, bank accounts when enabled, address collection, eligible Apple Pay and Google Pay buttons, and 3D Secure. Wallet availability follows your workspace's Gateway settings automatically.
This is different from mountCardInputs(), which mounts only card number, expiry, and CVC so you can build everything else yourself. Embedded Checkout uses one outer Paysio iframe for the complete block, with processor-secure card fields nested inside it.
// Server: create the session with your secret key
const { data: session } = await fetch('https://paysio.com/api/v1/checkout-sessions', {
method: 'POST',
headers: { Authorization: 'Bearer sk_live_...', 'Content-Type': 'application/json' },
body: JSON.stringify({
ui_mode: 'embedded',
allowed_parent_origins: ['https://shop.example.com'],
show_order_summary: true,
line_items: [{ product_id: 'prod_...', quantity: 1 }],
customer_email: '[email protected]',
customer_phone: '+15551234567',
email_field_mode: 'read_only',
phone_field_mode: 'hidden',
quick_checkout_behavior: 'on_load',
}),
}).then(r => r.json());
// Browser: receive only session.client_secret from your server
const checkout = await paysio.mountCheckout('#checkout', {
clientSecret: session.client_secret,
appearance: {
variables: {
colorPrimary: '#635bff', colorBackground: '#ffffff',
colorText: '#18181b', colorTextMuted: '#71717a', colorBorder: '#d4d4d8',
borderRadius: '10px', buttonBorderRadius: '999px',
fontFamily: 'Inter, sans-serif', fontSize: '16px',
},
fonts: [{ cssSrc: 'https://fonts.googleapis.com/css2?family=Inter' }],
},
onComplete: result => console.log(result.orderLabel),
});The iframe resizes automatically. The returned handle supports on(), updateAppearance(), and unmount(). Use the browser completion event for UI only; fulfill from a payment.completed webhook or a server-side session retrieval.
SDK reference
Paysio(key, options?)- Create a Paysio instance.
keyis your publishable key.options.apiBaseoverrides the API URL (for local dev). paysio.mountCheckout(target, opts)- Mount the complete Embedded Checkout.
opts.clientSecretis required; appearance, onReady, onComplete, and onError are optional. onComplete receives transactionId, orderNumber, orderLabel, and confirmationToken. ReturnsPromise<{ iframe, on, updateAppearance, unmount }>. Wallet buttons are excluded. paysio.mountCardInputs(target, opts?)- Mount secure card input fields (iframes — card data never enters your page).
targetis a CSS selector, element, or{ cardNumber, cardExpiry, cardCvc }for separate containers. Options:onReady,onChange,style(backgroundColor, textColor, placeholderColor, borderColor, focusBorderColor, iconColor, fontSize, fontFamily),appearance({ variables, rules, fonts }— full theming, see above),placeholders. ReturnsPromise<{ unmount, focus, clear, updateStyle }>. paysio.createToken(cardData?)- Tokenize from mounted card inputs (no arguments), or pass raw card data for NMI:
{ number, exp_month, exp_year, cvv }. ReturnsPromise<{ token, card: { last4, brand } }>. paysio.reset()- Tears down mounted card inputs and clears the cached processor/tokenization config, so the next
mountCardInputs()re-initializes against the workspace's current gateway. Call this when switching gateways without a full page reload. paysio.elements()- Returns an
Elementsinstance for mounting wallet buttons (Apple Pay / Google Pay). elements.mountWallets(target, opts)- Renders the official Apple Pay / Google Pay buttons into the target (Paysio Wallets).
opts:{ amount, currency, country, collectShipping, buttonHeight, gap, direction, applePay, googlePay }— see “Button appearance” in the Wallets guide for the full option set. elements.updateAmount(amount)- Updates the amount charged when a wallet sheet is next opened (e.g. after a quantity change).
elements.on(event, fn)- Listen for events:
'ready'(wallets loaded),'error', or'walletPayment'— receives{ walletType, paysioWallet, wallet }; return a Promise resolving true/false to control the Apple Pay sheet result. elements.unmount()- Removes wallet buttons and cleans up resources.
paysio.threeDS()- Returns a new
ThreeDSinstance for 3D Secure card verification. threeDS.authenticate(opts)- Initiates 3DS verification. Pass
{ amount, token }(aptok_fromcreateToken()— not consumed) for a new card, or{ amount, paymentMethodId }for a card already saved on file. Returns a Promise resolving with{ status, eci, authenticationValue, dsTransId, threeDSServerTransID, threeDsVersion }— forward the whole result to the charge. threeDS.cancel()- Cancels an in-progress 3DS authentication and removes iframes.
threeDS.reset()- Resets to idle state for a new authentication attempt.
Test cards
| Card | Number | Expiry | CVV |
|---|---|---|---|
| Visa (success) | 4111 1111 1111 1111 | 12/29 | 123 |
| Mastercard (success) | 5431 1111 1111 1111 | 12/29 | 123 |
| Visa (decline) | 4111 1111 1111 1129 | 12/29 | 123 |
Use these with a pk_test_... key. Any future expiry date and any 3-digit CVV will work.
Troubleshooting
401 — “Invalid or missing API key”
- Verify you're using the correct script tag for your environment (staging script with
pk_test_*, production script withpk_live_*). - Check that the key hasn't been revoked in Settings → API Keys.
- Make sure the key belongs to the correct workspace.
CORS errors
- The SDK auto-detects the API base from the script URL. If using a staging script, API calls go to staging automatically.
- If you're behind a proxy or custom domain, set
apiBaseto your proxy URL. - All
/api/v1/*endpoints support CORS from any origin.
“Paysio is not defined”
- Ensure the
<script>tag loads before your code runs. Place it in<head>or before your code. - If using React/Vue, wait for
window.Paysioto be available (e.g. in auseEffect).
Verify your setup with curl
curl -H "Authorization: Bearer pk_live_YOUR_KEY" \
https://paysio.com/api/v1/tokenization-key
# Should return: { "data": { "tokenization_key": "...", ... } }Complete working example
Copy this entire file, replace the key, and open it in a browser. Enter a test card number to tokenize it.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Paysio.js - Payment Form</title>
<script src="https://paysio.com/paysio.js"></script>
<style>
body { font-family: system-ui, sans-serif; max-width: 400px; margin: 40px auto; padding: 0 20px; }
input { display: block; width: 100%; padding: 10px 12px; margin: 8px 0; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; box-sizing: border-box; }
.row { display: flex; gap: 12px; }
.row input { flex: 1; }
button { padding: 10px 24px; background: #0f172a; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; width: 100%; margin-top: 8px; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
#status { font-size: 13px; color: #64748b; margin-top: 12px; }
.error { color: #ef4444; }
.success { color: #22c55e; }
</style>
</head>
<body>
<h2>Add Payment Method</h2>
<form id="payment-form">
<input type="text" id="card-number" placeholder="Card number" required />
<div class="row">
<input type="text" id="card-exp" placeholder="MM / YY" required />
<input type="text" id="card-cvv" placeholder="CVV" required />
</div>
<button type="submit">Save Card</button>
</form>
<p id="status"></p>
<script>
const paysio = Paysio('pk_test_YOUR_KEY_HERE');
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
const statusEl = document.getElementById('status');
const btn = e.target.querySelector('button');
btn.disabled = true;
statusEl.textContent = 'Tokenizing...';
statusEl.className = '';
try {
const [mm, yy] = document.getElementById('card-exp').value.split('/').map(s => s.trim());
const { token, card } = await paysio.createToken({
number: document.getElementById('card-number').value,
exp_month: mm,
exp_year: yy,
cvv: document.getElementById('card-cvv').value,
});
statusEl.textContent = 'Token: ' + token + ' (' + card.brand + ' ****' + card.last4 + ')';
statusEl.className = 'success';
// TODO: Send this token to YOUR server
// await fetch('/api/save-card', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ token, customer_id: '...' }),
// });
} catch (err) {
statusEl.textContent = err.message;
statusEl.className = 'error';
} finally {
btn.disabled = false;
}
});
</script>
</body>
</html>Backend proxy setup
If your frontend proxies API calls through your own backend, proxy all /api/v1/* requests to Paysio. All SDK calls (tokenization, 3DS, tokens) use the same /api/v1/ base URL. Pass { apiBase: "/api/v1" } to route through your proxy:
// Frontend: route SDK through your backend proxy
const paysio = Paysio('pk_test_...', { apiBase: '/api/v1' });
// Backend proxy example (Node.js/Express):
app.all('/api/v1/*', async (req, res) => {
const path = req.url.replace('/api/v1', '');
const response = await fetch(`https://paysio.com/api/v1${path}`, {
method: req.method,
headers: {
'Authorization': 'Bearer sk_live_YOUR_SECRET_KEY',
'Content-Type': 'application/json',
},
body: ['POST','PATCH','PUT'].includes(req.method) ? JSON.stringify(req.body) : undefined,
});
const data = await response.text();
res.status(response.status).type('json').send(data);
});React integration
When using Paysio.js in React, use useRef for the Paysio instance and event handlers to avoid stale closures. Here is a complete example with card tokenization, 3DS, and optional wallet support:
import { useState, useRef, useEffect, useCallback } from 'react';
export function CheckoutForm() {
const [email, setEmail] = useState('');
const [cardComplete, setCardComplete] = useState(false);
const [status, setStatus] = useState('');
const [loading, setLoading] = useState(false);
const paysioRef = useRef(null);
const processRef = useRef(null); // Ref to avoid stale closures
// Initialize Paysio + mount secure card fields once
useEffect(() => {
paysioRef.current = window.Paysio('pk_test_YOUR_KEY');
let handle;
paysioRef.current
.mountCardInputs('#card-fields', {
onChange: ({ complete }) => setCardComplete(complete),
})
.then(h => { handle = h; });
// Optional: mount wallet buttons
// const elements = paysioRef.current.elements();
// elements.mountWallets('#wallet-buttons', { amount: 29.99 });
// elements.on('walletPayment', (data) => processRef.current(data));
return () => handle && handle.unmount();
}, []);
// Process payment — always uses latest state via processRef
const processPayment = useCallback(async (token, threeDsResult) => {
setLoading(true);
try {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, threeDsResult, email, amount: 2999 }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
setStatus('Payment successful!');
} catch (err) {
setStatus('Error: ' + err.message);
} finally {
setLoading(false);
}
}, [email]);
// Keep ref in sync so wallet handlers get fresh state
processRef.current = processPayment;
const handleSubmit = async (e) => {
e.preventDefault();
if (!email) return setStatus('Email is required');
setLoading(true);
try {
// 1. Tokenize the card from the secure hosted fields
const { token } = await paysioRef.current.createToken();
// 2. Run 3DS with the token (not consumed — still chargeable after)
let threeDsResult = null;
try {
threeDsResult = await paysioRef.current.threeDS().authenticate({
amount: 29.99,
token,
email,
iframeTarget: '#threeds-container',
});
} catch (err) {
console.warn('3DS failed:', err.message);
// Continue without 3DS — no liability shift
}
// 3. Process payment
await processPayment(token, threeDsResult);
} catch (err) {
setStatus('Error: ' + err.message);
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={e => setEmail(e.target.value)}
placeholder="Email" required />
<div id="card-fields" />
<div id="threeds-container" />
<button type="submit" disabled={loading || !cardComplete}>
{loading ? 'Processing...' : 'Pay $29.99'}
</button>
{status && <p>{status}</p>}
</form>
);
}