Usage-Based Billing for SaaS
Implement metered billing for your SaaS platform. Define usage metrics, track consumption, and automatically charge customers based on their usage.
How It Works
Define Metrics
Set up usage metrics with rates in your billing plan (e.g., $0.50 per package)
Record Usage
Call the SDK whenever a billable event occurs in your application
Auto-Charge
Inkress calculates and charges usage fees at the end of each billing cycle
Prerequisites
Before you begin, ensure you have the following set up.
Installation
Environment Variables
INKRESS_API_KEYChecklist
- Register a Merchant account.
- Generate an API Key with Admin permissions.
- Create a Subscription Plan with usage metrics configured.
SDK Configuration
Initialize the Inkress SDK with your API credentials. This instance will be used for all billing operations.
import InkressSDK from "@inkress/admin-sdk";
const sdk = new InkressSDK({
accessToken: process.env.INKRESS_API_KEY,
mode: process.env.NODE_ENV === 'production' ? 'live' : 'sandbox',
});
export default sdk;Define Usage Metrics
Usage metrics are defined in your billing plan's data.usage_metrics field. Each metric has a name and a rate (in the plan's currency).
Usage Metrics Structure
billing_plan.data.usage_metrics = [{metric: 'packages', rate: 0.50}, {metric: 'returns', rate: 1.00}]Rates are in the billing plan's currency (e.g., JMD, USD).
// When creating or updating a billing plan via API
const plan = await sdk.billingPlans.create({
title: 'Business Plan',
description: 'Perfect for growing businesses',
price: 2999.00, // Base monthly fee
interval: 'month',
currency_code: 'JMD',
data: {
usage_metrics: [
{ metric: 'packages', rate: 50.00 }, // $50 JMD per package shipped
{ metric: 'returns', rate: 100.00 }, // $100 JMD per return processed
{ metric: 'api_calls', rate: 0.10 }, // $0.10 JMD per API call (overage)
]
}
});
console.log('Plan created:', plan.uid);Common Usage Metric Examples
| Use Case | Metric Name | Example Rate |
|---|---|---|
| Shipping Platform | packages | $50 per package |
| E-commerce | orders | $25 per order |
| API Service | api_calls | $0.01 per call |
| Storage | storage_gb | $100 per GB |
| Support | support_tickets | $500 per ticket |
Create Subscription
Create a subscription for your customer using a plan that has usage metrics defined. The customer will be charged the base fee plus any usage at the end of each billing cycle.
import sdk from '../lib/inkress';
async function createSubscription(customer, planUid: string) {
const result = await sdk.subscriptions.createLink({
title: 'Business Subscription',
plan_uid: planUid,
customer: {
first_name: customer.firstName,
last_name: customer.lastName,
email: customer.email,
},
meta_data: {
return_url: 'https://your-app.com/billing/success',
internal_customer_id: customer.id,
},
reference_id: `sub_${customer.id}_${Date.now()}`,
});
// Send customer to payment page
return result.payment_urls.short_link;
}
// After subscription is activated (via webhook), store the subscription UID
async function onSubscriptionActivated(webhookData) {
const subscriptionUid = webhookData.subscription.uid;
const customerId = webhookData.subscription.meta_data?.internal_customer_id;
// Store subscriptionUid in your database linked to the customer
await db.customers.update(customerId, {
inkress_subscription_uid: subscriptionUid
});
}Recording Usage
Call sdk.subscriptions.usage() whenever a billable event occurs. Inkress will automatically calculate the additional fee based on your plan's usage metrics.
Important
metric name must exactly match one defined in the billing plan's usage_metrics array.import sdk from '../lib/inkress';
// Record usage whenever a billable event occurs
async function recordUsage(subscriptionUid: string, metric: string, count: number = 1) {
await sdk.subscriptions.usage(subscriptionUid, {
metric: metric,
metric_count: count,
});
}
// Example: When a package is shipped
async function onPackageShipped(customerId: string, packageId: string) {
const customer = await db.customers.get(customerId);
await recordUsage(customer.inkress_subscription_uid, 'packages', 1);
console.log(`Recorded 1 package usage for customer ${customerId}`);
}
// Example: When a return is processed
async function onReturnProcessed(customerId: string, returnId: string) {
const customer = await db.customers.get(customerId);
await recordUsage(customer.inkress_subscription_uid, 'returns', 1);
}
// Example: Batch recording (e.g., daily API call summary)
async function recordDailyApiUsage(customerId: string, callCount: number) {
const customer = await db.customers.get(customerId);
await recordUsage(customer.inkress_subscription_uid, 'api_calls', callCount);
}Usage Recording Best Practices
- Record immediately: Call the usage endpoint right when the billable event occurs for accurate tracking.
- Batch when appropriate: For high-frequency events (like API calls), consider batching and recording hourly or daily totals.
- Handle errors gracefully: Queue failed usage records for retry to ensure accurate billing.
- Use idempotency: Include a unique reference to prevent duplicate charges if retrying.
Advanced: Usage with Idempotency
For critical billing events, use idempotency keys to prevent duplicate charges when retrying failed requests.
import sdk from '../lib/inkress';
// Usage with idempotency to prevent duplicates
async function recordUsageWithIdempotency(
subscriptionUid: string,
metric: string,
count: number,
eventId: string // Unique ID for this billable event
) {
await sdk.subscriptions.usage(subscriptionUid, {
metric: metric,
metric_count: count,
reference_id: eventId, // Prevents duplicate recording
});
}
// Example: Robust usage recording with retry
async function recordUsageSafely(
subscriptionUid: string,
metric: string,
count: number,
eventId: string,
maxRetries = 3
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await sdk.subscriptions.usage(subscriptionUid, {
metric,
metric_count: count,
reference_id: eventId,
});
return { success: true };
} catch (error) {
if (attempt === maxRetries) {
// Log for manual review
console.error(`Failed to record usage after ${maxRetries} attempts`, {
subscriptionUid, metric, count, eventId, error
});
// Queue for later retry
await usageRetryQueue.add({ subscriptionUid, metric, count, eventId });
return { success: false, queued: true };
}
// Exponential backoff
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 100));
}
}
}Webhook Integration
Handle subscription lifecycle events and usage billing notifications via webhooks.
export async function webhookHandler(request) {
const payload = await request.json();
const { event, data } = payload;
switch (event) {
// Subscription activated - store the UID for usage tracking
case 'subscriptions.activated':
await db.customers.update(data.subscription.meta_data?.internal_customer_id, {
inkress_subscription_uid: data.subscription.uid,
subscription_status: 'active',
});
break;
// Subscription renewed - includes usage charges from previous period
case 'subscriptions.renewed':
const usageCharges = data.invoice?.usage_charges || [];
await logBillingEvent({
customerId: data.subscription.meta_data?.internal_customer_id,
baseAmount: data.invoice.base_amount,
usageAmount: data.invoice.usage_amount,
totalAmount: data.invoice.total,
usageBreakdown: usageCharges,
});
break;
// Payment successful for subscription (including usage fees)
case 'subscriptions.payment_succeeded':
await updateCustomerBillingStatus(data.customer.email, 'paid');
break;
// Payment failed - may need to restrict access
case 'subscriptions.payment_failed':
await handlePaymentFailure(data.subscription.meta_data?.internal_customer_id);
break;
// Subscription cancelled
case 'subscriptions.cancelled':
await db.customers.update(data.subscription.meta_data?.internal_customer_id, {
subscription_status: 'cancelled',
});
break;
}
return { received: true };
}Viewing Current Usage
Retrieve the current billing period's usage to display to customers or for internal reporting.
import sdk from '../lib/inkress';
// Get current period usage for a subscription
async function getCurrentUsage(subscriptionUid: string) {
const subscription = await sdk.subscriptions.get(subscriptionUid);
return {
currentPeriodStart: subscription.current_period_start,
currentPeriodEnd: subscription.current_period_end,
usage: subscription.current_usage || [],
// Example: [{ metric: 'packages', count: 47, amount: 2350.00 }]
};
}
// Display usage summary to customer
async function getCustomerBillingSummary(customerId: string) {
const customer = await db.customers.get(customerId);
const usage = await getCurrentUsage(customer.inkress_subscription_uid);
const plan = await sdk.billingPlans.get(customer.plan_uid);
const usageTotal = usage.usage.reduce((sum, u) => sum + u.amount, 0);
return {
planName: plan.title,
basePrice: plan.price,
usageCharges: usage.usage,
usageTotal,
estimatedTotal: plan.price + usageTotal,
billingPeriodEnds: usage.currentPeriodEnd,
};
}Complete Example: Shipping Platform
A full example showing how a shipping platform might implement usage-based billing.
import sdk from './lib/inkress';
// 1. Define your plan with usage metrics (done once in dashboard or via API)
const SHIPPING_PLAN_UID = 'plan_abc123';
// Plan config:
// - Base price: $2,999 JMD/month
// - usage_metrics: [
// { metric: 'packages', rate: 50.00 },
// { metric: 'returns', rate: 100.00 }
// ]
// 2. When a merchant signs up
async function onMerchantSignup(merchant) {
const paymentLink = await sdk.subscriptions.createLink({
plan_uid: SHIPPING_PLAN_UID,
customer: {
first_name: merchant.ownerFirstName,
last_name: merchant.ownerLastName,
email: merchant.email,
},
meta_data: {
return_url: 'https://shipfast.com/dashboard',
merchant_id: merchant.id,
},
});
// Redirect merchant to payment page
return paymentLink.payment_urls.short_link;
}
// 3. When subscription is activated (webhook)
async function handleSubscriptionActivated(data) {
await db.merchants.update(data.subscription.meta_data.merchant_id, {
subscription_uid: data.subscription.uid,
subscription_status: 'active',
});
}
// 4. In your shipping service - record usage
async function createShipment(merchantId, shipmentData) {
// Your shipping logic...
const shipment = await shippingService.create(shipmentData);
// Record the usage
const merchant = await db.merchants.get(merchantId);
await sdk.subscriptions.usage(merchant.subscription_uid, {
metric: 'packages',
metric_count: 1,
});
return shipment;
}
// 5. In your returns service - record usage
async function processReturn(merchantId, returnData) {
// Your returns logic...
const returnOrder = await returnsService.process(returnData);
// Record the usage
const merchant = await db.merchants.get(merchantId);
await sdk.subscriptions.usage(merchant.subscription_uid, {
metric: 'returns',
metric_count: 1,
});
return returnOrder;
}
// 6. Show merchant their current bill
async function getMerchantBill(merchantId) {
const merchant = await db.merchants.get(merchantId);
const subscription = await sdk.subscriptions.get(merchant.subscription_uid);
// subscription.current_usage = [
// { metric: 'packages', count: 150, amount: 7500.00 },
// { metric: 'returns', count: 12, amount: 1200.00 }
// ]
const basePrice = 2999.00;
const usageTotal = subscription.current_usage.reduce((sum, u) => sum + u.amount, 0);
return {
base: basePrice,
usage: subscription.current_usage,
usageTotal,
total: basePrice + usageTotal, // $2,999 + $8,700 = $11,699
periodEnds: subscription.current_period_end,
};
}Tips for Usage-Based Billing
- Start with simple metrics (e.g., count-based) before adding complex ones
- Consider offering usage bundles/tiers (e.g., first 100 packages free)
- Provide real-time usage dashboards so customers aren't surprised by bills
- Set up alerts when customers approach usage thresholds
- Test thoroughly in sandbox mode with various usage scenarios