Skip to content

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) or requests library for Python

The Workflow

  1. Flush daily usage - each 24-hour metering window becomes one verified delta
  2. Lock the period - checkpoint at month-end to freeze the billing window
  3. Generate a proof - pull the receipt with the proof root and all deltas
  4. Build the invoice - embed the proof root and a verification link
  5. 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)

typescript
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

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)

typescript
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

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)

typescript
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

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)

typescript
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

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)

typescript
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:

typescript
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:

typescript
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