Skip to main content
VoiceGateway’s version 1 accounting ledger is an opt-in companion to the legacy request-cost records. Legacy APIs remain available; new invoice-grade integrations should use /v1/accounting/*.

Guarantees

Pricing has independent acquisition and selling revisions. A revision is identified by tenant scope, side, revision ID, contract version, and the SHA-256 hash of canonical JSON. Repeating identical content is idempotent. Reusing an identity with different content returns 409. Activation is a separate operation and can include expected_current_revision_id to prevent a lost update. Failed creation or activation does not change the previous active revision. POST /v1/accounting/prepare captures active revisions and accounting ownership for one project, component, and offering. The resulting binding is immutable. Provider attempts retain that binding across delayed delivery and collector retries. A later price activation affects only later preparations. Missing revisions produce an unrated event; VoiceGateway never applies the current catalog retroactively. Usage commits transactionally with a per-record receipt. The SQL ledger (SQLite or PostgreSQL) is authoritative. The reserved projection table is not written and pending_delivery is not reported until a real consumer ships; ClickHouse accounting projection remains deferred. Duplicate event payloads return their original receipt, while conflicting content or a second billable producer for the same provider attempt is durably rejected.

Exact units and rounding

Rates and quantities are JSON decimal strings. Binary floating-point is not used by the version 1 rater. Cache quantities are subsets of text input. The rater subtracts cache read and write from ordinary input only when that cache dimension has its own rate. An unpriced cache subset stays in ordinary input and marks the side incomplete instead of silently discounting tokens. A revision must classify every dimension as rated or unsupported. Unknown prices, missing measurements, unsupported measurements, and explicit zero prices are distinct. The usd-v1-half-even-12 profile computes with 60 decimal digits and rounds each event-side total to 12 USD fractional digits using half-even rounding. Stored event totals are aggregated; display rounding is not accounting. Version 1 supports USD only and rejects other currencies.

Security and data minimization

The authenticated principal supplies the tenant. A submitted tenant identifier is never trusted as authority. Project-restricted API keys may ingest and read only their allowed projects. Acquisition rates, acquisition totals, and margin are operator-only; tenant reports, dashboard responses, and the read-only MCP status tool expose selling totals and completeness only. The usage schema is an allowlist. It has no fields for credentials, prompts, transcripts, caller values, arbitrary metadata, or tool arguments. Do not add these fields to accounting extensions. Pricing mutations require both an admin principal and the admin scope. A tenant-bound admin remains bound to that tenant. Producer-facing preparation responses omit acquisition revision IDs; during ingestion, the collector resolves acquisition, selling, and ownership from the stored binding.

Producer delivery

AccountingOutbox.submit() is the durable producer API. It writes the exact envelope and stable event ID to a record- and byte-bounded SQLite outbox before returning. attach() registers its LiveKit component and session event handlers synchronously before it returns. When a native metric arrives, its sink work is scheduled outside provider execution and the accounting decorator calls submit() before writing ordinary telemetry. Collector I/O happens only during drain, so a collector outage cannot delay the voice response. A full disk outbox or persistence failure increments an explicit capture-failure signal, persists that counter when storage is available, and logs an error. enqueue_nowait() remains available for producers that cannot await even a local write. It has a documented process-crash window before its background worker persists the event. attach() does not use that weaker path. The outbox never evicts persisted, unacknowledged usage. It deletes a row only after an accepted or duplicate receipt with a receipt ID, quarantines terminal rejections and HTTP 4xx responses, and retains retryable or missing-receipt rows across restarts. Backoff is bounded and jittered; rows that exhaust the configured attempt limit are quarantined. Operators must monitor pending count, oldest pending age, rejected count, and capture failures. Bounded storage and an indefinitely unavailable collector cannot mathematically guarantee zero loss; this condition is visible rather than silent. Delivery retries preserve event and attempt IDs. A new provider retry or fallback receives a new attempt and event ID. SDK-owned and externally owned accounting are selected in an operator-managed ownership assignment. Diagnostic telemetry may still be emitted by the non-owner, but it must not be submitted as billable usage. Native LiveKit request IDs and streaming segment IDs are retained as non-content correlation metadata. They generate replay-stable accounting event IDs, so replaying one metric deduplicates while different TTS segments that share a request ID remain distinct. Realtime text, audio, and cached-audio token quantities are carried separately. An absent native measurement is emitted as missing, never as a measured zero. The remaining observation boundary is explicit: work completed before attach() returns, a provider that emits neither a metric nor cumulative session usage, and an abrupt process termination before the scheduled metric task starts cannot be reconstructed. Graceful session close reconciles missing per-call metrics from cumulative LiveKit usage. The Wave 0 tracing context does not create accounting records; enabling or propagating that context leaves the single accounting capture sink as the only billable producer.

Compatibility and migration

Database migration a6c9e2f4b817 adds the ledger and optional project allowlists to API keys. Existing keys remain unrestricted within their tenant. Existing request rows keep their stored floating-point totals and are labeled by their legacy provenance; they are not re-rated or assigned invented revisions. Rollout order:
  1. Back up the SQL database and upgrade collectors.
  2. Create and read back acquisition and selling revisions, verify their hashes, then activate each side independently.
  3. Configure ownership, upgrade producers, and monitor unrated ledger rows plus each producer outbox’s pending/rejected health side-by-side with legacy reporting.
  4. Switch invoice exports only after reconciliation passes.
The existing voicegw export-costs command exports legacy request-cost rows. It is not an immutable-ledger export and contains no acquisition revision, acquisition total, or margin fields. A versioned exact-accounting export is deferred; use the tenant-scoped report API for supported ledger reads. To roll back, stop new version 1 producers, drain or preserve their local outboxes, and deploy the earlier application. Do not downgrade the database: the additive tables are inert to older code and retain receipts for a later resume. Migration a6c9e2f4b817 explicitly refuses destructive downgrade so a routine rollback cannot erase ledger data or project allowlists.

Minimal SDK example

Supplying accounting_outbox is the explicit opt-in. Normal telemetry continues through its configured sink, while usage-v1 capture persists without blocking the voice response. If prepared ownership is external, the SDK does not send a duplicate billable record. The read-only MCP accounting tool accepts no tenant argument. Bind it to one tenant with VOICEGW_MCP_ACCOUNTING_TENANT; without that setting it returns accounting_tenant_not_configured and no accounting data. Provider adapters must report unsupported measurements explicitly. They must not infer absent cache, realtime-audio, reasoning, or character measurements. For a complete synchronization and delayed-delivery example, run python examples/accounting_sync.py. It discovers capabilities, synchronizes acquisition and selling independently, verifies readback hashes and dimension sets, prepares a binding, changes the active selling revision, and submits a delayed event against the pinned older revision with a per-record receipt.