A modern TypeScript SDK for subscription payments with Primer Headless Checkout integration.
- 🚀 Modern API: Clean, Promise-based interface with event-driven architecture
- 🔄 Dynamic Pricing: Update prices without page reload
- 🛡️ Type-Safe: Complete TypeScript definitions and type safety
- 🎯 Event-Driven: Handle success, errors, and status changes with ease
- 🔧 Robust: Built-in error handling, retries, and validation
- 📦 Lightweight: Minimal dependencies, browser-optimized
- 🎨 Headless Checkout: Full control over checkout UI with Primer Headless Checkout
<!-- Include Primer Headless Checkout SDK first -->
<script src="https://sdk.primer.io/web/v2.57.3/Primer.min.js"></script>
<link rel="stylesheet" href="https://sdk.primer.io/web/v2.57.3/Checkout.css" />
<!-- Include Funnelfox Billing SDK -->
<script src="https://unpkg.com/@funnelfox/billing@latest/dist/funnelfox-billing.min.js"></script>npm install @funnelfox/billing @primer-io/checkout-webIf you are developing locally, install dev tooling for TypeScript builds/tests:
npm i -D @rollup/plugin-typescript ts-jest @types/jestThen build:
npm run buildimport { Billing } from '@funnelfox/billing';
await Billing.createCheckout({
orgId: 'your-org-id',
priceId: 'price_123',
customer: {
externalId: 'user_456',
email: 'user@example.com',
},
container: '#checkout-container',
});Configure global SDK settings.
import { configure } from '@funnelfox/billing';
configure({
orgId: 'your-org-id', // Required
baseUrl: 'https://custom.api', // Optional, defaults to https://billing.funnelfox.com
region: 'us-east-1', // Optional, defaults to 'default'
});Parameters:
config.orgId(string, required) - Your organization identifierconfig.baseUrl(string, optional) - Custom API URLconfig.region(string, optional) - Region, defaults to 'default'
Creates a new checkout instance.
const checkout = await createCheckout({
// Required
orgId: 'your-org-id',
priceId: 'price_123',
customer: {
externalId: 'user_456',
email: 'user@example.com',
countryCode: 'US', // Optional
},
container: '#checkout-container',
clientMetadata: { source: 'web' },
cardSelectors: {
// Custom card input selectors (optional, defaults to auto-generated)
cardNumber: '#cardNumberInput',
expiryDate: '#expiryInput',
cvv: '#cvvInput',
cardholderName: '#cardHolderInput',
button: '#submitButton',
},
paypalButtonContainer: '#paypalButton', // Optional
googlePayButtonContainer: '#googlePayButton', // Optional
applePayButtonContainer: '#applePayButton', // Optional
paymentMethodOrder: ['PAYMENT_CARD', 'PAYPAL', 'GOOGLE_PAY', 'APPLE_PAY'], // Optional
// Callbacks (alternative to events)
onSuccess: result => {
/* ... */
},
onError: error => {
/* ... */
},
onStatusChange: (state, oldState) => {
/* ... */
},
});Parameters:
options.priceId(string, required) - Price identifieroptions.customer(object, required)customer.externalId(string, required) - Your user identifiercustomer.email(string, required) - Customer emailcustomer.countryCode(string, optional) - ISO country code
options.container(string, required) - CSS selector for checkout container
Container Styling Requirements (Default Skin):
When using the default skin, the container element must have the following CSS properties for proper display of the loading indicator:
#checkout-container {
position: relative;
min-height: 200px; /* Adjust based on your layout */
}position: relative- Required because the loading overlay usesposition: absoluteto cover the containermin-height- Required to ensure the loader is visible during initialization. Recommended minimum is200px
Additional Parameters:
options.orgId(string, optional) - Org ID (if not configured globally)options.clientMetadata(object, optional) - Custom metadataoptions.cardSelectors(object, optional) - Custom card input selectors (defaults to auto-generated)options.paypalButtonContainer(string, optional) - Container selector for PayPal buttonoptions.googlePayButtonContainer(string, optional) - Container selector for Google Pay buttonoptions.applePayButtonContainer(string, optional) - Container selector for Apple Pay buttonoptions.paymentMethodOrder(array, optional) - Custom order for payment methods. Available values:'PAYMENT_CARD','PAYPAL','GOOGLE_PAY','APPLE_PAY'. Defaults to['PAYMENT_CARD', 'PAYPAL', 'GOOGLE_PAY', 'APPLE_PAY']options.onInitialized(function, optional) - Initialized callbackoptions.onSuccess(function, optional) - Success callbackoptions.onError(function, optional) - Error callbackoptions.onStatusChange(function, optional) - State change callback
Returns: Promise<CheckoutInstance>
Create a client session manually (for advanced integrations).
import { createClientSession } from '@funnelfox/billing';
const session = await createClientSession({
priceId: 'price_123',
externalId: 'user_456',
email: 'user@example.com',
orgId: 'your-org-id', // Optional if configured
});
console.log(session.clientToken); // Use with Primer Headless Checkout
console.log(session.orderId);Returns: Promise<{ clientToken: string, orderId: string, type: string }>
id(string) - Unique checkout identifierstate(string) - Current state:initializing,ready,processing,completed,errororderId(string) - Order identifier (available after initialization)isDestroyed(boolean) - Whether checkout has been destroyed
Emitted when payment completes successfully.
checkout.on('success', result => {
console.log('Order ID:', result.orderId);
console.log('Status:', result.status); // 'succeeded'
console.log('Transaction:', result.transactionId);
});Emitted when payment fails or encounters an error.
checkout.on('error', error => {
console.error('Error:', error.message);
console.error('Code:', error.code);
console.error('Request ID:', error.requestId); // For support
});Emitted when checkout state changes.
checkout.on('status-change', (newState, oldState) => {
console.log(`${oldState} → ${newState}`);
// States: initializing, ready, processing, action_required, completed, error
});Emitted when checkout is destroyed.
checkout.on('destroy', () => {
console.log('Checkout cleaned up');
});Updates the checkout to use a different price.
await checkout.updatePrice('price_yearly');Note: Cannot update price while payment is processing.
Returns current checkout status.
const status = checkout.getStatus();
console.log(status.id); // Checkout ID
console.log(status.state); // Current state
console.log(status.orderId); // Order ID
console.log(status.priceId); // Current price ID
console.log(status.isDestroyed); // Cleanup statusDestroys the checkout instance and cleans up resources.
await checkout.destroy();Check if checkout is ready for payment.
if (checkout.isReady()) {
console.log('Ready to accept payment');
}Check if payment is being processed.
if (checkout.isProcessing()) {
console.log('Payment in progress...');
}<!DOCTYPE html>
<html>
<head>
<title>Funnelfox Checkout</title>
<script src="https://sdk.primer.io/web/v2.57.3/Primer.min.js"></script>
<link
rel="stylesheet"
href="https://sdk.primer.io/web/v2.57.3/Checkout.css"
/>
<script src="https://unpkg.com/@funnelfox/billing@latest/dist/funnelfox-billing.min.js"></script>
</head>
<body>
<div id="price-selector">
<button onclick="selectPrice('price_monthly')">Monthly - $9.99</button>
<button onclick="selectPrice('price_yearly')">Yearly - $99.99</button>
</div>
<div id="checkout-container"></div>
<script>
let currentCheckout = null;
// Configure SDK once
Billing.configure({
orgId: 'your-org-id',
});
async function selectPrice(priceId) {
try {
if (currentCheckout && currentCheckout.isReady()) {
// Update existing checkout
await currentCheckout.updatePrice(priceId);
} else {
// Destroy old checkout if exists
if (currentCheckout) {
await currentCheckout.destroy();
}
// Create new checkout
currentCheckout = await Billing.createCheckout({
priceId: priceId,
customer: {
externalId: generateUserId(),
email: getUserEmail(),
},
container: '#checkout-container',
});
// Handle success
currentCheckout.on('success', result => {
alert('Payment successful!');
window.location.href = '/success?order=' + result.orderId;
});
// Handle errors
currentCheckout.on('error', error => {
alert('Payment failed: ' + error.message);
});
// Track state changes
currentCheckout.on('status-change', state => {
console.log('Checkout state:', state);
});
}
} catch (error) {
console.error('Checkout error:', error);
alert('Failed to initialize checkout');
}
}
function generateUserId() {
return 'user_' + Math.random().toString(36).substr(2, 9);
}
function getUserEmail() {
return 'user@example.com'; // Get from your auth system
}
</script>
</body>
</html>The SDK provides specific error classes for different scenarios:
import {
ValidationError,
APIError,
PrimerError,
CheckoutError,
NetworkError,
} from '@funnelfox/billing';
try {
const checkout = await createCheckout(config);
} catch (error) {
if (error instanceof ValidationError) {
// Invalid input
console.log('Field:', error.field);
console.log('Value:', error.value);
console.log('Message:', error.message);
} else if (error instanceof APIError) {
// API error
console.log('Status:', error.statusCode);
console.log('Error Code:', error.errorCode); // e.g., 'double_purchase'
console.log('Error Type:', error.errorType); // e.g., 'api_exception'
console.log('Request ID:', error.requestId); // For support
console.log('Message:', error.message);
} else if (error instanceof PrimerError) {
// Primer SDK error
console.log('Primer error:', error.message);
console.log('Original:', error.primerError);
} else if (error instanceof CheckoutError) {
// Checkout lifecycle error
console.log('Phase:', error.phase);
console.log('Message:', error.message);
} else if (error instanceof NetworkError) {
// Network/connectivity error
console.log('Network error:', error.message);
console.log('Original:', error.originalError);
}
}double_purchase- User already has an active subscriptioninvalid_price- Price ID not foundinvalid_customer- Customer data validation failedpayment_failed- Payment processing failed
The SDK includes comprehensive TypeScript definitions:
import {
configure,
createCheckout,
CheckoutInstance,
PaymentResult,
CheckoutConfig,
PaymentMethod,
} from '@funnelfox/billing';
// Configure
configure({
orgId: 'your-org-id',
});
// Create checkout with type safety
const checkout: CheckoutInstance = await createCheckout({
priceId: 'price_123',
customer: {
externalId: 'user_456',
email: 'user@example.com',
countryCode: 'US',
},
container: '#checkout',
clientMetadata: {
source: 'web',
campaign: 'summer-sale',
},
paymentMethodOrder: [
PaymentMethod.PAYPAL,
PaymentMethod.PAYMENT_CARD,
PaymentMethod.GOOGLE_PAY,
PaymentMethod.APPLE_PAY,
],
});
// Type-safe event handlers
checkout.on('success', (result: PaymentResult) => {
console.log('Order:', result.orderId);
console.log('Status:', result.status);
console.log('Transaction:', result.transactionId);
});const checkout = await createCheckout({
priceId: 'price_123',
customer: {
externalId: 'user_456',
email: 'user@example.com',
},
container: '#checkout',
// Callback style (alternative to .on() events)
onSuccess: result => {
console.log('Success!', result.orderId);
},
onError: error => {
console.error('Error!', error.message);
},
onStatusChange: (newState, oldState) => {
console.log(`${oldState} → ${newState}`);
},
});By default, the SDK automatically generates card input elements. You can provide custom selectors if you want to use your own HTML structure:
const checkout = await createCheckout({
priceId: 'price_123',
customer: {
externalId: 'user_456',
email: 'user@example.com',
},
container: '#checkout',
// Custom card input selectors
cardSelectors: {
cardNumber: '#my-card-number',
expiryDate: '#my-expiry',
cvv: '#my-cvv',
cardholderName: '#my-cardholder',
button: '#my-submit-button',
},
// Custom payment method button containers
paypalButtonContainer: '#my-paypal-button',
googlePayButtonContainer: '#my-google-pay-button',
applePayButtonContainer: '#my-apple-pay-button',
});You can customize the order in which payment methods are displayed to your customers:
const checkout = await createCheckout({
priceId: 'price_123',
customer: {
externalId: 'user_456',
email: 'user@example.com',
},
container: '#checkout',
// Customize payment method order
paymentMethodOrder: ['PAYPAL', 'GOOGLE_PAY', 'APPLE_PAY', 'PAYMENT_CARD'],
});Available payment methods:
'PAYMENT_CARD'- Credit/debit card payment'PAYPAL'- PayPal payment'GOOGLE_PAY'- Google Pay payment'APPLE_PAY'- Apple Pay payment
By default, payment methods are shown in the order: Card, PayPal, Google Pay, Apple Pay. You can reorder them to match your business priorities or regional preferences.
For scenarios where you want to render a single payment method with full control over placement and callbacks:
import { Billing, PaymentMethod } from '@funnelfox/billing';
const container = document.getElementById('payment-container');
const paymentMethod = await Billing.initMethod(
PaymentMethod.PAYMENT_CARD, // or PAYPAL, GOOGLE_PAY, APPLE_PAY
container,
{
// Required
orgId: 'your-org-id',
priceId: 'price_123',
externalId: 'user_456',
email: 'user@example.com',
// Optional - API configuration
baseUrl: 'https://custom.api', // Optional, defaults to https://billing.funnelfox.com
meta: { source: 'web' }, // Optional metadata
// Optional - Primer configuration (for customizing payment method behavior)
style: {
/* Primer style options */
},
card: {
/* Primer card options */
},
applePay: {
/* Primer Apple Pay options */
},
paypal: {
/* Primer PayPal options */
},
googlePay: {
/* Primer Google Pay options */
},
// Callbacks
onRenderSuccess: () => {
console.log('Payment method rendered successfully');
},
onRenderError: method => {
console.error('Failed to render:', method);
},
onLoaderChange: isLoading => {
console.log('Loading state:', isLoading);
},
onPaymentStarted: method => {
console.log('Payment started with:', method);
},
onPaymentSuccess: () => {
console.log('Payment completed successfully!');
},
onPaymentFail: error => {
console.error('Payment failed:', error.message);
},
onPaymentCancel: () => {
console.log('Payment was cancelled');
},
onErrorMessageChange: message => {
console.log('Error message:', message);
},
onMethodsAvailable: methods => {
console.log('Available methods:', methods);
},
}
);
// Control the payment method
paymentMethod.setDisabled(true); // Disable the payment method
paymentMethod.setDisabled(false); // Enable it
// For card payments, you can trigger submit programmatically
if (paymentMethod.submit) {
await paymentMethod.submit();
}
// Clean up when done
await paymentMethod.destroy();Parameters:
method(PaymentMethod, required) - Payment method to initialize:PAYMENT_CARD,PAYPAL,GOOGLE_PAY, orAPPLE_PAYelement(HTMLElement, required) - DOM element where the payment method will be renderedoptions(InitMethodOptions, required):orgId(string, required) - Your organization identifierpriceId(string, required) - Price identifierexternalId(string, required) - Your user identifieremail(string, required) - Customer emailbaseUrl(string, optional) - Custom API URLmeta(object, optional) - Custom metadatastyle,card,applePay,paypal,googlePay(optional) - Primer SDK configuration options- Callbacks (all optional):
onRenderSuccess,onRenderError,onLoaderChange,onPaymentStarted,onPaymentSuccess,onPaymentFail,onPaymentCancel,onErrorMessageChange,onMethodsAvailable
Returns: Promise<PaymentMethodInterface> with methods:
setDisabled(disabled: boolean)- Enable/disable the payment methodsubmit()- Trigger form submission (available for card payments)destroy()- Clean up and remove the payment method
For advanced integrations where you want to control the Primer Headless Checkout directly:
import { createClientSession } from '@funnelfox/billing';
import { Primer } from '@primer-io/checkout-web';
// Step 1: Create session
const session = await createClientSession({
priceId: 'price_123',
externalId: 'user_456',
email: 'user@example.com',
orgId: 'your-org-id',
});
// Step 2: Use with Primer Headless Checkout directly
const headlessCheckout = await Primer.createHeadless(session.clientToken, {
paymentHandling: 'MANUAL',
apiVersion: '2.4',
onTokenizeSuccess: async (paymentMethodTokenData, handler) => {
// Your custom payment logic...
// Call your payment API with paymentMethodTokenData.token
handler.handleSuccess();
},
});
await headlessCheckout.start();- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
See the examples directory for more complete examples:
- Basic Checkout - Simple checkout integration
MIT © Funnelfox