JavaScript SDK for Funnelfox billing with Primer integration
npm install @funnelfox/billingA 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
``html
`
`bash`
npm install @funnelfox/billing @primer-io/checkout-web
If you are developing locally, install dev tooling for TypeScript builds/tests:
`bash`
npm i -D @rollup/plugin-typescript ts-jest @types/jest
Then build:
`bash`
npm run build
`javascript
import { 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.
`javascript
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.
`javascript
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 codeoptions.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:
`css`
#checkout-container {
position: relative;
min-height: 200px; / Adjust based on your layout /
}
- position: relative - Required because the loading overlay uses position: absolute to cover the containermin-height
- - Required to ensure the loader is visible during initialization. Recommended minimum is 200px
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
---
Create a client session manually (for advanced integrations).
`javascript
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 }>
---
#### Properties
- 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
#### Events
##### 'success'
Emitted when payment completes successfully.
`javascript`
checkout.on('success', result => {
console.log('Order ID:', result.orderId);
console.log('Status:', result.status); // 'succeeded'
console.log('Transaction:', result.transactionId);
});
##### 'error'
Emitted when payment fails or encounters an error.
`javascript`
checkout.on('error', error => {
console.error('Error:', error.message);
console.error('Code:', error.code);
console.error('Request ID:', error.requestId); // For support
});
##### 'status-change'
Emitted when checkout state changes.
`javascript${oldState} → ${newState}
checkout.on('status-change', (newState, oldState) => {
console.log();`
// States: initializing, ready, processing, action_required, completed, error
});
##### 'destroy'
Emitted when checkout is destroyed.
`javascript`
checkout.on('destroy', () => {
console.log('Checkout cleaned up');
});
#### Methods
##### updatePrice(priceId)
Updates the checkout to use a different price.
`javascript`
await checkout.updatePrice('price_yearly');
Note: Cannot update price while payment is processing.
##### getStatus()
Returns current checkout status.
`javascript`
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 status
##### destroy()
Destroys the checkout instance and cleans up resources.
`javascript`
await checkout.destroy();
##### isReady()
Check if checkout is ready for payment.
`javascript`
if (checkout.isReady()) {
console.log('Ready to accept payment');
}
##### isProcessing()
Check if payment is being processed.
`javascript`
if (checkout.isProcessing()) {
console.log('Payment in progress...');
}
---
`html
rel="stylesheet"
href="https://sdk.primer.io/web/v2.57.3/Checkout.css"
/>
`
The SDK provides specific error classes for different scenarios:
`javascript
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:
`typescript
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);
});
`
`javascript
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:
`javascript
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:
`javascript
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:
`javascript
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, or APPLE_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 optionsonRenderSuccess
- Callbacks (all optional): , onRenderError, onLoaderChange, onPaymentStarted, onPaymentSuccess, onPaymentFail, onPaymentCancel, onErrorMessageChange, onMethodsAvailable
Returns: Promise 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:
`javascript
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