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: '[email protected]',
},
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
priceId: 'price_123',
customer: {
externalId: 'user_456',
email: '[email protected]',
countryCode: 'US', // Optional
},
container: '#checkout-container',
// Optional
orgId: 'your-org-id', // If not configured globally
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
// 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 containeroptions.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.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: '[email protected]',
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 '[email protected]'; // 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,
} 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: '[email protected]',
countryCode: 'US',
},
container: '#checkout',
clientMetadata: {
source: 'web',
campaign: 'summer-sale',
},
});
// 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: '[email protected]',
},
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: '[email protected]',
},
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',
});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: '[email protected]',
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