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.
TypeScript (SDK)
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,
delta: -totalApiCalls,
reason: `daily_usage_flush:${usageDate}`,
referenceId: `usage:${customerId}:${usageDate}`,
});
console.log(`Flushed ${totalApiCalls} calls for ${customerId} on ${usageDate}`);
return result;
}
Python
import requests
API_BASE = "https://app.pacspace.io"
HEADERS = {
"X-Api-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
}
def flush_daily_usage(customer_id: str, usage_date: str, total_api_calls: int):
"""Flush one day of metered usage as a verified delta."""
payload = {
"customerId": customer_id,
"delta": -total_api_calls,
"reason": f"daily_usage_flush:{usage_date}",
"referenceId": f"usage:{customer_id}:{usage_date}",
}
response = requests.post(
f"{API_BASE}/api/v1/balance/delta", headers=HEADERS, json=payload
)
response.raise_for_status()
result = response.json()["data"]
print(f"Flushed {total_api_calls} calls for {customer_id} on {usage_date}")
return result
Step 2: Check Usage Mid-Period
At any time, derive the current balance to see how much daily-flushed usage has accumulated:
TypeScript (SDK)
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}`);
Python
def get_usage(customer_id: str):
"""Get the current usage total for a customer."""
response = requests.get(
f"{API_BASE}/api/v1/balance/derive/{customer_id}", headers=HEADERS
)
response.raise_for_status()
data = response.json()["data"]
return {
"customer_id": customer_id,
"total_usage_units": abs(data["computedBalance"]),
"verified_deltas": data["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.
TypeScript (SDK)
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}`);
Python
def lock_period(customer_id: str, period: str):
"""Checkpoint the period."""
response = requests.post(
f"{API_BASE}/api/v1/balance/checkpoint",
headers=HEADERS,
json={"customerId": customer_id, "period": period},
)
response.raise_for_status()
data = response.json()["data"]
print(f"Period locked. Proof root: {data['proofRoot']}")
return data
Step 4: Generate the Receipt
Pull the complete receipt for the period - all daily deltas, the proof root, and verification data.
TypeScript (SDK)
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}`);
Python
def generate_receipt(customer_id: str, period: str):
"""Generate the receipt for a period."""
response = requests.get(
f"{API_BASE}/api/v1/balance/receipt/{customer_id}",
headers=HEADERS,
params={"period": period},
)
response.raise_for_status()
return response.json()["data"]
Step 5: Build the Invoice
Embed the proof root and a verification link in your invoice:
TypeScript (SDK)
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.verifyUrl
No 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,
yourBalance: invoice.endingBalance,
theirBalance: 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,
delta: -420,
reason: 'daily_usage_flush:2026-02-01',
referenceId: 'usage:cust_acme_001:2026-02-01',
});
await pac.balance.emit({
customerId,
delta: -390,
reason: 'daily_usage_flush:2026-02-02',
referenceId: 'usage:cust_acme_001:2026-02-02',
});
await pac.balance.emit({
customerId,
delta: -437,
reason: '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