DocumentationSaaS Integration

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

1

Define Metrics

Set up usage metrics with rates in your billing plan (e.g., $0.50 per package)

2

Record Usage

Call the SDK whenever a billable event occurs in your application

3

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

npm install @inkress/admin-sdk

Environment Variables

INKRESS_API_KEY

Checklist

1

SDK Configuration

Initialize the Inkress SDK with your API credentials. This instance will be used for all billing operations.

lib/inkress.ts
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;
2

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).

Creating a plan with usage metrics
// 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 CaseMetric NameExample Rate
Shipping Platformpackages$50 per package
E-commerceorders$25 per order
API Serviceapi_calls$0.01 per call
Storagestorage_gb$100 per GB
Supportsupport_tickets$500 per ticket
3

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.

services/subscriptions.ts
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 
  });
}
4

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.

services/usage.ts
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.
5

Advanced: Usage with Idempotency

For critical billing events, use idempotency keys to prevent duplicate charges when retrying failed requests.

services/usage-advanced.ts
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));
    }
  }
}
6

Webhook Integration

Handle subscription lifecycle events and usage billing notifications via webhooks.

api/webhooks.ts
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 };
}
7

Viewing Current Usage

Retrieve the current billing period's usage to display to customers or for internal reporting.

services/billing-dashboard.ts
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.

shipping-platform-example.ts
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,
  };
}