Skip to content

TypeScript / Node.js SDK

Install and use the official PacSpace SDK for TypeScript and Node.js. Zero dependencies, fully typed, built for Node.js 18+.

The official PacSpace SDK for TypeScript and Node.js. It wraps the Balance API in a typed, ergonomic client that handles authentication, retries, response parsing, and webhook verification — so you can focus on your integration.

  • Zero dependencies — uses native fetch (Node.js 18+)
  • Fully typed — complete TypeScript definitions for every request and response
  • Automatic retries — exponential backoff on transient errors
  • Environment detection — auto-selects Sandbox or Production from your API key prefix
  • MIT licensedview on npm

Installation

bash
npm install @pacspace-io/sdk

Requires Node.js 18+ and TypeScript 5+ (for full type inference).


Quick Setup

typescript
import { PacSpace } from '@pacspace-io/sdk';

const pac = new PacSpace({
  apiKey: process.env.PACSPACE_API_KEY,
});

// Record a delta
const delta = await pac.balance.emit('cust_123', -42.50, 'usage_charge');

// Derive the balance
const { computedBalance } = await pac.balance.derive('cust_123');

The SDK reads your API key prefix to detect the environment automatically and routes requests to the correct API endpoint:

  • pk_test_* keys → Sandbox API endpoint (dedicated sandbox infrastructure)
  • pk_live_* keys → Production API endpoint (api.pacspace.io)

The SDK handles URL routing transparently — no extra configuration needed.

URL Auto-Routing

When you use a pk_test_* key, the SDK automatically routes to the Sandbox API endpoint. When you use a pk_live_* key, it routes to the Production API endpoint (api.pacspace.io). This ensures your requests always hit the correct environment's dedicated infrastructure.

Overriding Endpoints

You can override the default endpoints if needed:

typescript
const pac = new PacSpace({
  apiKey: 'pk_test_PUBLIC.SECRET',
  sandboxUrl: 'https://custom-sandbox.example.com',  // Override sandbox endpoint
  productionUrl: 'https://api.pacspace.io',          // Override production endpoint
});

Note: In most cases, you won't need to override endpoints. The SDK's automatic routing based on API key prefix is sufficient for all standard use cases.


Configuration

Pass options when creating the client:

typescript
const pac = new PacSpace({
  apiKey: 'pk_live_PUBLIC.SECRET',  // Required - SDK auto-routes based on prefix
  maxRetries: 2,       // Retries on transient errors (default: 2)
  timeout: 30000,      // Request timeout in ms (default: 30000)
  webhookSecret: 'whsec_...',  // Enables pac.webhooks (optional)
  // Optional endpoint overrides (usually not needed):
  // sandboxUrl: 'https://custom-sandbox.example.com',
  // productionUrl: 'https://api.pacspace.io',
});

Options Reference

OptionTypeDefaultDescription
apiKeystringrequiredYour PacSpace API key. SDK auto-routes based on prefix (pk_test_* → Sandbox, pk_live_* → Production)
sandboxUrlstringAuto-detectedSandbox API endpoint (override only if needed)
productionUrlstringhttps://api.pacspace.ioProduction API endpoint (override only if needed)
maxRetriesnumber2Max retries on 5xx and network errors
timeoutnumber30000Request timeout in milliseconds
webhookSecretstringYour webhook signing secret. Enables pac.webhooks

Balance API

All Balance API methods are available on pac.balance.

emit()

Record a delta for a customer. Returns immediately with a QUEUED status.

typescript
const delta = await pac.balance.emit(
  'cust_123',    // customerId
  -42.50,        // delta amount (positive = increase, negative = decrease)
  'usage_charge', // reason (for audit trail)
  {
    referenceId: 'inv_001',           // your internal reference (optional)
    metadata: { plan: 'growth' },     // arbitrary metadata (optional)
    idempotencyKey: 'idem_abc',       // prevent duplicate processing (optional)
  }
);

console.log(delta.receiptId);  // store for later verification
console.log(delta.status);     // 'QUEUED'

emitAndWait()

Record a delta and block until it reaches a terminal status (VERIFIED or FAILED). Useful for synchronous workflows.

typescript
const verified = await pac.balance.emitAndWait(
  'cust_123',
  -42.50,
  'usage_charge',
  {
    timeout: 30_000,    // max wait time in ms (default: 60000)
    pollInterval: 1000,  // polling frequency in ms (default: 2000)
  }
);

console.log(verified.status);  // 'VERIFIED' or 'FAILED'

derive()

Derive a customer's balance from all verified deltas.

typescript
const result = await pac.balance.derive('cust_123');

console.log(result.computedBalance);   // derived balance
console.log(result.deltasCount);       // number of verified deltas
console.log(result.latestReceiptId);   // use as checkpoint for next call

Paginate the deltas array with limit and offset:

typescript
const result = await pac.balance.derive('cust_123', {
  limit: 50,
  offset: 100,
});
console.log(result.pagination.total); // Total deltas available

The computedBalance is always derived from all verified deltas, regardless of pagination.

For large histories, use checkpointing to avoid replaying from the beginning:

typescript
const next = await pac.balance.derive('cust_123', {
  startingBalance: result.computedBalance,
  startingCheckpoint: result.latestReceiptId,
});

compare()

Compare your balance against a counterparty's. PacSpace derives the neutral truth and tells you who matches.

typescript
const report = await pac.balance.compare('cust_123', {
  yours: 95000,
  theirs: 98000,
});

console.log(report.matchesYours);   // true or false
console.log(report.matchesTheirs);  // true or false
console.log(report.neutralBalance); // the independently derived balance

if (report.discrepancyReport) {
  console.log(report.discrepancyReport.amount);
  console.log(report.discrepancyReport.resolution);
}

receipt()

Generate a verifiable receipt containing all verified deltas for a customer.

typescript
const receipt = await pac.balance.receipt('cust_123');

console.log(receipt.finalBalance);
console.log(receipt.deltasCount);
console.log(receipt.verification.itemHashes);  // content fingerprints

checkpoint()

Commit a period-end checkpoint. Creates an immutable proof root over all verified deltas in the billing window.

typescript
const checkpoint = await pac.balance.checkpoint('cust_123', {
  period: '2026-02',  // YYYY-MM format (default: current period)
});

console.log(checkpoint.merkleRoot);  // proof root — include in your invoice
console.log(checkpoint.deltaCount);
console.log(checkpoint.status);      // 'QUEUED'

Omit customerId to checkpoint all customers:

typescript
const all = await pac.balance.checkpoint();

listCheckpoints()

Retrieve committed checkpoints with optional filtering by customer or period.

typescript
const { checkpoints } = await pac.balance.listCheckpoints({
  period: '2026-02',
  limit: 10,
});

for (const cp of checkpoints) {
  console.log(cp.checkpointId, cp.merkleRoot, cp.status);
}

Filter by customer:

typescript
const { checkpoints } = await pac.balance.listCheckpoints({
  customerId: 'cust_123',
  period: '2026-01',
  limit: 20,
  offset: 0,
});

deltaStatus()

Check the processing status of a previously emitted delta. For batch anchors, returns all deltas in the anchor.

typescript
const status = await pac.balance.deltaStatus('anc_abc123');

console.log(status.status);     // 'ANCHORED'
console.log(status.deltaCount); // 1 for single emit, N for batch

// Batch anchors include all deltas
for (const d of status.deltas) {
  console.log(d.customerId, d.delta);
}

emitBatch()

Record up to 100 deltas in a single request. Each result includes an index (position in your input array) and an itemHash (individual content hash for Merkle verification).

typescript
const result = await pac.balance.emitBatch([
  { customerId: 'cust_123', delta: -100, reason: 'usage' },
  { customerId: 'cust_456', delta: -200, reason: 'usage' },
]);

console.log(result.totalQueued); // 2

// Per-item results with index correlation
for (const r of result.results) {
  console.log(r.index, r.customerId, r.itemHash, r.status);
}

// Efficiency summary — shows how deltas were packed into verified operations
console.log(result._efficiency?.message);
// "2 delta(s) packed into 2 verified operation(s)"

Webhook Delivery History

typescript
const { deliveries } = await pac.balance.listWebhookDeliveries({
  status: 'failed',
});

// Retry a failed delivery
await pac.balance.retryWebhook(deliveries[0].eventId);

Customer Ledgers

Every unique customerId you pass to emit() automatically creates an isolated ledger. Use these methods to list and inspect your customer ledgers.

customers()

List all customer ledgers for your account:

typescript
const { customers, pagination } = await pac.balance.customers();

for (const c of customers) {
  console.log(c.customerId, c.totalDeltas, c.ledgerIdentifier);
}

// Search and paginate
const filtered = await pac.balance.customers({
  search: 'partner_',
  limit: 10,
  page: 2,
});

customer()

Get full detail for a specific customer's ledger:

typescript
const ledger = await pac.balance.customer('cust_001');

console.log(ledger.ledgerIdentifier); // 0xA1b2...Ef34
console.log(ledger.computedBalance);  // 4500.00
console.log(ledger.totalDeltas);      // 42

// Access recent activity
for (const item of ledger.recentActivity.items) {
  console.log(item.amount, item.reason, item.status);
}

// Paginate activity
const page2 = await pac.balance.customer('cust_001', {
  deltaPage: 2,
  deltaLimit: 20,
});

Privacy note: PacSpace stores only the derived ledger identifier for each customer. Store the mapping between your customer IDs and your internal records in your own system.


Receipts & Verification

Generate receipt-ready proof data for a customer's period. Checkpoint first, then fetch the receipt.

receipt()

Generate a receipt for embedding in an invoice:

typescript
const checkpoint = await pac.balance.checkpoint('cust_001', { period: '2026-02' });
const receipt = await pac.balance.receipt('cust_001', { period: '2026-02' });

console.log(receipt.verification?.itemsRoot ?? receipt.receiptId ?? checkpoint.merkleRoot);  // Include in your invoice
console.log(receipt.finalBalance);   // The verified ending balance
console.log(receipt.deltasCount);   // Number of verified deltas in the period

The response includes everything you need:

typescript
// receipt.receiptId / itemsRoot — proof root for the period
// receipt.deltasCount         — number of verified deltas
// receipt.finalBalance        — balance at end of period
// receipt.deltas[]            — all verified deltas with reason, referenceId, itemHash
// receipt.verification        — verification metadata

Default period is the current month. Pass period to generate proof for a specific period.

Full Receipt Workflow

typescript
// 1. Record deltas throughout the month
await pac.balance.emit({ customerId: 'cust_001', delta: -150, reason: 'api_usage' });

// 2. Lock the period
const checkpoint = await pac.balance.checkpoint('cust_001', { period: '2026-02' });

// 3. Generate receipt
const receipt = await pac.balance.receipt('cust_001', { period: '2026-02' });

// 4. Include in your invoice
const invoice = {
  total: Math.abs(receipt.finalBalance),
  proofRoot: receipt.receiptId ?? receipt.verification?.itemsRoot ?? checkpoint.merkleRoot,
  verifyUrl: `https://balance-api.pacspace.io/api/v1/verify/${receipt.receiptId ?? checkpoint.merkleRoot}`,
};

See the Receipts & Verification guide for the complete workflow including the public verify endpoint and dispute handling.


Webhook Verification

The SDK can verify incoming webhook signatures to ensure they came from PacSpace and haven't been tampered with.

Requires a webhookSecret in the constructor. Get your webhook secret from the PacSpace dashboard.

verify()

Verify a signature and parse the event:

typescript
const pac = new PacSpace({
  apiKey: process.env.PACSPACE_API_KEY,
  webhookSecret: process.env.PACSPACE_WEBHOOK_SECRET,
});

const event = pac.webhooks.verify(
  req.headers['x-pacspace-signature'],
  req.headers['x-pacspace-timestamp'],
  rawBody,  // raw string body, before JSON parsing
);

if (event.event === 'delta.verified') {
  console.log(event.data.receiptId);
}

verifyFromHeaders()

Convenience method that extracts the signature and timestamp from a headers object:

typescript
const event = pac.webhooks.verifyFromHeaders(req.headers, rawBody);

middleware()

Drop-in Express/Connect middleware that verifies the signature and attaches the parsed event to the request:

typescript
import express from 'express';

const app = express();

// Capture the raw body (required for signature verification)
app.use('/webhooks', express.json({
  verify: (req, _res, buf) => {
    req.rawBody = buf.toString();
  },
}));

app.post('/webhooks/pacspace', pac.webhooks.middleware(), (req, res) => {
  const event = req.pacspaceEvent;

  switch (event.event) {
    case 'delta.verified':
      console.log('Delta verified:', event.data.receiptId);
      break;
    case 'checkpoint.verified':
      console.log('Checkpoint verified:', event.data.proofHash);
      break;
  }

  res.status(200).json({ received: true });
});

Webhook Event Types

EventWhen It Fires
delta.verifiedA delta has been independently verified
delta.storedDelta fingerprints have been permanently committed
checkpoint.verifiedA period-end checkpoint has been verified
fact.verifiedA fact has been committed and verified
record.transferredOwnership of a record has been transferred and verified

All event types are fully typed. Use event.event to narrow the type:

typescript
if (event.event === 'delta.verified') {
  // event.data is typed as DeltaVerifiedPayload
  console.log(event.data.delta.customerId);
  console.log(event.data.proof.proofHash);
}

Error Handling

The SDK throws typed errors you can catch by class. Every error extends PacSpaceError.

typescript
import {
  PacSpace,
  PlanLimitExceededError,
  RateLimitError,
  ValidationError,
} from '@pacspace-io/sdk';

try {
  await pac.balance.emit('cust_123', -1000, 'charge');
} catch (err) {
  if (err instanceof PlanLimitExceededError) {
    console.log('Plan limit reached — upgrade or wait for next period');
  } else if (err instanceof RateLimitError) {
    console.log(`Retry after ${err.retryAfter} seconds`);
  } else if (err instanceof ValidationError) {
    console.log(`Invalid request: ${err.message}`);
  }
}

Error Classes

Error ClassStatusWhen
ValidationError400Invalid request data
InvalidApiKeyError401Missing or invalid API key
PlanLimitExceededError402Free plan limit reached or no active subscription
NotFoundError404Customer or resource not found
ContractNotDeployedError412No environment activated
RateLimitError429Rate limit exceeded (check err.retryAfter)
TimeoutErrorPolling timed out waiting for verification
WebhookVerificationErrorWebhook signature verification failed

Every error includes:

  • err.message — human-readable description
  • err.statusCode — HTTP status code (or 0 for client-side errors)
  • err.code — machine-readable code (e.g., PLAN_LIMIT_EXCEEDED)
  • err.requestPath — the API path that triggered the error

Response Unwrapping

The PacSpace API wraps all responses in a standard envelope:

json
{
  "success": true,
  "data": { "computedBalance": 95000, "..." }
}

The SDK unwraps this automatically. You always receive the data object directly:

typescript
// API returns: { success: true, data: { computedBalance: 95000 } }
// SDK returns: { computedBalance: 95000 }
const result = await pac.balance.derive('cust_123');
console.log(result.computedBalance);  // 95000

On error responses, the SDK throws a typed error instead of returning the envelope.


Automatic Retries

The SDK retries automatically on transient errors:

ConditionRetried?Strategy
Network failureYesExponential backoff
500, 502, 503, 504YesExponential backoff
429 (rate limited)YesRespects Retry-After header
400, 401, 402, 404NoThrows immediately

Default: 2 retries with exponential backoff and jitter. Configure with maxRetries:

typescript
const pac = new PacSpace({
  apiKey: '...',
  maxRetries: 5,  // up to 5 retries on transient errors
});

Set maxRetries: 0 to disable retries entirely.


Request Cancellation

Cancel in-flight requests using an AbortSignal:

typescript
const controller = new AbortController();

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  await pac.balance.derive('cust_123', {
    signal: controller.signal,
  });
} catch (err) {
  if (err.code === 'CANCELLED') {
    console.log('Request was cancelled');
  }
}

Package Details

DetailValue
npm package@pacspace-io/sdk
Version0.1.0
DependenciesNone
Minimum Node.js18+
TypeScript5+ (recommended)
LicenseMIT
SourceGitHub

Next Steps