SaaS Usage Metering & Verifiable Receipts
Track daily per-customer API usage with the Balance API and generate receipts your customers can independently verify.
This example shows the complete receipt-and-verification workflow: flush daily usage totals, lock the period, generate a receipt, embed the proof root in your invoice, and give your customer the returned verifyUrl so they can verify independently.
Prerequisites
- A Balance API key
- Node.js 18+ or Python 3.8+
- The PacSpace SDK (
npm install @pacspace-io/sdk) orrequestslibrary for Python
The Workflow
- Flush daily usage - each 24-hour metering window becomes one verified delta
- Lock the period - checkpoint at month-end to freeze the billing window
- Generate a proof - pull the receipt with the proof root and all deltas
- Build the invoice - embed the proof root and a verification link
- Customer verifies - your verification endpoint lets them independently confirm every charge
Step 1: Flush Daily API Usage
At the end of each metering day, aggregate the customer's usage in your system and emit one delta for that 24-hour window. The Balance API handles fingerprinting and permanent verification automatically.
import { PacSpace } from '@pacspace-io/sdk';
const pac = new PacSpace({ apiKey: process.env.PACSPACE_API_KEY });
async function flushDailyUsage(
customerId: string,
usageDate: string,
totalApiCalls: number,
) {
const result = await pac.balance.emit(
customerId,
-totalApiCalls,
`daily_usage_flush:${usageDate}`,
{ referenceId: `usage:${customerId}:${usageDate}` },
);
console.log(`Flushed ${totalApiCalls} calls for ${customerId} on ${usageDate}`);
return result;
}Step 2: Check Usage Mid-Period
At any time, derive the current balance to see how much daily-flushed usage has accumulated:
const balance = await pac.balance.derive('cust_acme_001');
console.log(`Total usage: ${Math.abs(balance.computedBalance)} API calls`);
console.log(`Daily flushes verified: ${balance.deltasCount}`);Step 3: Lock the Period
At month-end, checkpoint to freeze the period. This produces a proof root - a single fingerprint covering every verified daily delta.
const checkpoint = await pac.balance.checkpoint('cust_acme_001', {
period: '2026-02',
});
// Store this - it goes on the invoice
const proofRoot = checkpoint.proofRoot;
console.log(`Period locked. Proof root: ${proofRoot}`);Step 4: Generate the Receipt
Pull the complete receipt for the period - all daily deltas, the proof root, and verification data.
const checkpoint = await pac.balance.checkpoint('cust_acme_001', { period: '2026-02' });
const receipt = await pac.balance.receipt('cust_acme_001', { period: '2026-02' });
console.log(`Proof root: ${receipt.proofRoot ?? checkpoint.proofRoot}`);
console.log(`Delta count: ${receipt.deltaCount}`);
console.log(`Final balance: ${receipt.finalBalance}`);Step 5: Build the Invoice
Embed the proof root and a verification link in your invoice:
async function generateInvoice(customerId: string, period: string, ratePerUnit: number) {
const checkpoint = await pac.balance.checkpoint(customerId, { period });
const receipt = await pac.balance.receipt(customerId, { period });
const proofRoot = receipt.proofRoot ?? checkpoint.proofRoot;
const verifyUrl = receipt.verifyUrl;
const totalUsageUnits = Math.abs(receipt.finalBalance);
const invoice = {
invoiceId: `inv-${customerId}-${period}`,
customerId,
period,
totalUsageUnits,
recordedDays: receipt.deltaCount,
ratePerUnit,
subtotal: totalUsageUnits * ratePerUnit,
proofRoot,
verifyUrl,
};
// Store in your database, send to customer, etc.
return invoice;
}
const invoice = await generateInvoice('cust_acme_001', '2026-02', 0.002);Expected Output
============================================================
INVOICE: inv-cust_acme_001-2026-02
============================================================
Customer: cust_acme_001
Period: 2026-02
Usage Units: 1,247
Recorded Days: 28
Rate: $0.0020 / call
Subtotal: $2.49
Proof root: 0x7f3a9b2c...
Verify at: https://customer-links.pacspace.io/c/cus_abc123
============================================================Step 6: Customer Verification
Give your customer a way to independently verify the invoice. Share the returned verifyUrl from the receipt response; treat it as an opaque URL because it may be a Shared Record permalink or a proof-root verification URL.
Option A (recommended): Store and show the returned receipt URL:
invoice.verifyUrl = receipt.verifyUrlNo API key is required for the outer verification page. See Receipts & Verification.
Option B: If you need custom logic (e.g., combine with your invoice data), expose a custom verify endpoint in your own server:
import express from 'express';
import { PacSpace } from '@pacspace-io/sdk';
const app = express();
const pac = new PacSpace({ apiKey: process.env.PACSPACE_API_KEY });
// Optional: redirect to the stored opaque verification URL or add custom logic
app.get('/verify/:invoiceId', async (req, res) => {
const invoice = await db.invoices.find(req.params.invoiceId);
res.redirect(invoice.verifyUrl);
});
// Or derive for custom verification
app.get('/verify/custom/:customerId/:period', async (req, res) => {
const { customerId, period } = req.params;
const lastInvoice = await db.invoices.findPrevious(customerId, period);
const result = await pac.balance.derive(customerId, {
startingBalance: lastInvoice?.endingBalance ?? 0,
startingCheckpoint: lastInvoice?.proofRoot,
startingCheckpointType: 'proofRoot',
});
const currentInvoice = await db.invoices.findByPeriod(customerId, period);
res.json({
period,
invoicedBalance: currentInvoice.endingBalance,
computedBalance: result.computedBalance,
verified: currentInvoice.endingBalance === result.computedBalance,
deltasVerified: result.deltasCount,
proofRoot: result.latestReceiptId,
});
});
// Dispute endpoint - when a customer disagrees
app.post('/dispute/:customerId/:period', async (req, res) => {
const { customerId, period } = req.params;
const { claimedBalance } = req.body;
const invoice = await db.invoices.findByPeriod(customerId, period);
const comparison = await pac.balance.compare(customerId, {
yours: invoice.endingBalance,
theirs: claimedBalance,
});
res.json({
invoicedBalance: invoice.endingBalance,
claimedBalance,
verifiedBalance: comparison.computedBalance,
matchesInvoice: comparison.matchesYours,
matchesClaim: comparison.matchesTheirs,
resolution: comparison.discrepancy?.resolution,
});
});Full Working Example
Here's the complete workflow from start to finish:
import { PacSpace } from '@pacspace-io/sdk';
const pac = new PacSpace({ apiKey: process.env.PACSPACE_API_KEY });
async function runBillingCycle() {
const customerId = 'cust_acme_001';
const period = '2026-02';
// 1. Flush usage once per metering day
console.log('Flushing daily usage...\n');
await pac.balance.emit(customerId, -420, 'daily_usage_flush:2026-02-01', {
referenceId: 'usage:cust_acme_001:2026-02-01',
});
await pac.balance.emit(customerId, -390, 'daily_usage_flush:2026-02-02', {
referenceId: 'usage:cust_acme_001:2026-02-02',
});
await pac.balance.emit(customerId, -437, 'daily_usage_flush:2026-02-03', {
referenceId: 'usage:cust_acme_001:2026-02-03',
});
// 2. Check mid-period usage
const balance = await pac.balance.derive(customerId);
console.log(`Mid-period usage: ${Math.abs(balance.computedBalance)} units\n`);
// 3. Lock the period
const checkpoint = await pac.balance.checkpoint(customerId, { period });
console.log(`Period locked. Proof root: ${checkpoint.proofRoot}\n`);
// 4. Generate receipt
const receipt = await pac.balance.receipt(customerId, { period });
const proofRoot = receipt.proofRoot ?? checkpoint.proofRoot;
const verifyUrl = receipt.verifyUrl;
console.log('============================================================');
console.log(` INVOICE: inv-${customerId}-${period}`);
console.log('============================================================');
console.log(` Usage Units: ${Math.abs(receipt.finalBalance)}`);
console.log(` Recorded Days: ${receipt.deltaCount}`);
console.log(` Rate: $0.0020 / call`);
console.log(` Subtotal: $${(Math.abs(receipt.finalBalance) * 0.002).toFixed(2)}`);
console.log(` Proof root: ${proofRoot}`);
console.log(` Verify at: ${verifyUrl}`);
console.log('============================================================');
}
runBillingCycle().catch(console.error);Next Steps
- Receipts & Verification guide - detailed walkthrough of verification tiers and dispute handling
- Checkpoint Reference - full checkpoint endpoint documentation
- Customer Records - how per-customer isolation works
- Webhook Setup - get real-time notifications when deltas are verified