Documentation
Connect your sources, read the numbers, and see how data is handled.
Updated
Quickstart
Install the Python SDK, set two environment variables, and wrap AI calls with track() to attribute them to a customer and feature. Create the ingestion key under Integrations → SDK & API.
pip install spendbook
export SPENDBOOK_API_KEY=afk_your_key # ingestion key, shown once
export SPENDBOOK_ENDPOINT=https://your-api # your AI Spendbook API URL
# in your app
from spendbook import trackSPENDBOOK_API_KEY: the ingestion key. It starts withafk_and is shown once.SPENDBOOK_ENDPOINT: the base URL of your AI Spendbook API.- Billing data comes from the provider connectors below. The SDK adds who and what each request was for.
Getting started
- Create an account and a workspace. Choose your main providers so the app can suggest connectors. To look around first, pick See a sample workspace. Every sample figure is labelled as sample data.
- Connect billing: Anthropic and OpenAI admin keys, or the Google Cloud Billing export for Gemini. This gives billed cost and provider-reported usage.
- Connect attribution: map the table where your app logs AI calls (Supabase, Postgres or Firestore), import a CSV, or send events with the SDK or ingestion API. This tells AI Spendbook who and what each request was for.
- Optionally connect revenue (Stripe or CSV) to see AI cost as a share of each customer's revenue.
- Set budgets, explore the dashboards and ask the AI Analyst.
Roles: viewers can read everything. Analysts can also create scenarios and exports and use the AI Analyst. Admins manage connections, keys, budgets, members and settings. Owners can also delete the workspace.
Anthropic & OpenAI
Both connectors use admin API keys, because organization usage and cost reports are only available to admin keys. The keys are used only to read those reports. Regular inference keys are rejected.
Anthropic
- An organization admin creates an Admin key (it starts with
sk-ant-admin) in the Claude Console under organization settings, Admin keys. - Endpoints read: the Messages usage report, the cost report, and workspace and API key names for labels.
- Usage usually appears within minutes. Cost data can be revised for several days, so recent days are fetched again on each sync and corrected.
- The cost report doesn't include Priority Tier charges. Those days are shown as estimates.
OpenAI
- An organization owner creates an Admin key in the OpenAI platform under organization settings, Admin keys.
- Endpoints read: the organization Usage API (completions, embeddings and other usage types) and the Costs API.
- Costs are reported per day and per project or line item. Usage provides model-level detail.
After you save a key, AI Spendbook validates it, shows the permissions it detected, and runs an initial sync (90 days by default). After that it syncs incrementally. Keys are stored encrypted and never shown again. You can rotate them from the connection page.
Google Gemini
Gemini spend comes from the Cloud Billing export to BigQuery. Turn on the export for the billing account first, then give AI Spendbook a dedicated service account that can only read the export.
- Roles:
roles/bigquery.dataVieweron the billing export dataset, androles/bigquery.jobUseron the project that runs the queries. - Create a JSON key for the service account and upload it in the Google connector. It is stored encrypted and never shown again.
- Rows read: Cloud Billing export rows for Gemini and Vertex AI services, with SKU, project, usage amounts and billed cost after credits.
- Export rows can arrive with a delay, so the newest days may show as estimates until the export catches up.
Supabase / Postgres
Create a dedicated read-only role that can only SELECT from the tables that log AI requests. Don't use the postgres user or a service-role key.
-- Run in the Supabase SQL editor (or psql) as an admin.
-- 1. A login role that can only read what you grant.
create role spendbook_reader with login password 'use-a-long-random-password';
alter role spendbook_reader set statement_timeout = '30s';
alter role spendbook_reader set default_transaction_read_only = on;
-- 2. Grant SELECT on the specific tables that log AI requests (not the whole schema).
grant usage on schema public to spendbook_reader;
grant select on public.ai_requests to spendbook_reader;
grant select on public.customers to spendbook_reader; -- optional: for customer attribution
-- Optional: limit to specific columns instead of whole tables.
-- revoke select on public.ai_requests from spendbook_reader;
-- grant select (id, created_at, model, input_tokens, output_tokens, customer_id, feature)
-- on public.ai_requests to spendbook_reader;
-- 3. If Row Level Security is enabled on those tables, allow this role to read them.
create policy spendbook_read on public.ai_requests for select to spendbook_reader using (true);- Connect with the host, port, database name and the
spendbook_readercredentials. TLS is required. - The wizard lists the schemas and tables this role can see. You choose tables and columns. Columns that look sensitive (emails, names, prompts, tokens, addresses) are flagged and left unselected by default.
- Map columns to canonical fields: timestamp, id, model and token counts are required.
customer_id,end_user_idandfeatureenable attribution. Relationships can pull a field such ascustomer_idfrom a joined table. - Preview and test the mapping before the first sync. Only the columns you approved are ever read.
Firebase / Firestore
Use a dedicated service account with read-only Firestore access (roles/datastore.viewer). Don't use the default App Engine or Firebase Admin SDK account.
# Create a dedicated, read-only service account (Google Cloud CLI).
PROJECT_ID=your-firebase-project
gcloud iam service-accounts create spendbook-reader \
--project "$PROJECT_ID" --display-name "AI Spendbook (read-only)"
# Cloud Datastore Viewer = read-only access to Firestore documents.
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member "serviceAccount:spendbook-reader@$PROJECT_ID.iam.gserviceaccount.com" \
--role "roles/datastore.viewer"
# Create a key and upload/paste the JSON in AI Spendbook, then delete the local file.
gcloud iam service-accounts keys create spendbook-reader.json \
--iam-account "spendbook-reader@$PROJECT_ID.iam.gserviceaccount.com"- Upload or paste the JSON key in the Firestore wizard. It is stored encrypted and never shown again.
- Choose collections, then allowlist the fields to read. Fields that look sensitive start unchecked.
- Map fields to canonical fields (nested fields use dotted paths like
usage.input_tokens), test the mapping, then sync.
Stripe revenue
Create a restricted key with read access to the resources the connector lists. Don't paste a full secret key. Revenue is matched to usage through customer_id, so use the same customer identifiers in both places or import a mapping.
CSV & JSON imports
Upload a file, choose its type, confirm the suggested column mapping, and review validation results. Rejected rows are listed with their row numbers before anything is written. Importing the same rows again doesn't create duplicates, because rows are keyed by event_id or a stable hash.
Provider costs / invoices
Provider cost exports or invoice lines. Stored as billed spend, which is the source of truth for totals.
| Column | Required | Meaning |
|---|---|---|
| date | yes | Billing day or period start. |
| provider | yes | anthropic, openai, google, … |
| cost | yes | Charged amount (in the row's currency). |
| currency | ISO code; defaults to USD. | |
| model | If the line is model-specific. | |
| line_item | Invoice description / SKU. | |
| cost_type | tokens, web_search, code_execution, … | |
| project_id | Provider project id. | |
| workspace_id | Provider workspace id. | |
| end_date | Period end, if a line spans several days. | |
| event_id | Stable id; otherwise a hash of the row is used. |
Provider token usage
Token usage aggregates per day, model and project, as exported from a provider console.
| Column | Required | Meaning |
|---|---|---|
| date | yes | Usage day. |
| provider | yes | anthropic, openai, … |
| model | yes | Model id. |
| input_tokens | Uncached input tokens. | |
| output_tokens | Output tokens. | |
| cache_read_tokens | Prompt-cache reads. | |
| cache_write_tokens | Prompt-cache writes. | |
| request_count | Number of requests. | |
| project_id | Provider project id. | |
| service_tier | standard, batch, priority, … | |
| event_id | Stable id; otherwise a hash of the row is used. |
Model usage summary
Requests, tokens and reported cost per model and period.
| Column | Required | Meaning |
|---|---|---|
| date | yes | Period day. |
| model | yes | Model id. |
| request_count | yes | Number of requests. |
| provider | Inferred from the model when missing. | |
| input_tokens | Input tokens. | |
| output_tokens | Output tokens. | |
| cost | Reported cost. | |
| currency | ISO code; defaults to USD. | |
| project_id | Project id. | |
| event_id | Stable id; otherwise a hash of the row is used. |
Request-level records
One row per model call, for example a log export. Enables attribution by customer, feature and user.
| Column | Required | Meaning |
|---|---|---|
| timestamp | yes | When the call happened (with a time zone, or set one in options). |
| model | yes | Model id. |
| provider | Inferred from the model when missing. | |
| input_tokens | Uncached input tokens. | |
| output_tokens | Output tokens. | |
| cache_read_tokens | Prompt-cache reads. | |
| cache_write_tokens | Prompt-cache writes. | |
| customer_id | Your customer/account id. | |
| end_user_id | Individual user id. | |
| feature | Feature / workflow that made the call. | |
| category | Usage category. | |
| request_id | Provider request id (de-duplication). | |
| cost | Your recorded cost, if any. | |
| currency | ISO code; defaults to USD. | |
| status | success / error / HTTP status. | |
| latency_ms | Latency in milliseconds. | |
| event_id | Stable id; otherwise a hash of the row is used. |
Customer mappings
Maps external ids (for example Stripe customer ids) to your customer_id.
| Column | Required | Meaning |
|---|---|---|
| external_id | yes | Id in the other system. |
| customer_id | yes | Your customer id used on usage. |
| customer_name | Display name. | |
| source | System the external id comes from. |
Customer revenue
Monthly revenue per customer, used for AI cost as a share of revenue and margin after AI.
| Column | Required | Meaning |
|---|---|---|
| customer_id | yes | Must match customer_id on usage. |
| month | yes | YYYY-MM (or a date in the month). |
| revenue | yes | Revenue for the month. |
| currency | ISO code; defaults to USD. |
customer_id,month,revenue_usd,customer_name
acme,2026-10,12000.00,Acme Corp
globex,2026-10,4500.00,GlobexTimestamps need a time zone (ISO-8601 with an offset, or epoch seconds or milliseconds). If yours have none, set “assume time zone” in the mapping options. Money is in US dollars.
Python SDK
Install with pip install spendbook and configure it with SPENDBOOK_API_KEY and SPENDBOOK_ENDPOINT. Wrap AI calls with track() from from spendbook import track to attribute them. It works as a context manager, an async context manager or a decorator, and nested scopes merge.
# pip install spendbook
# Configure with environment variables (never hard-code keys):
# export SPENDBOOK_API_KEY=afk_your_key
# export SPENDBOOK_ENDPOINT=http://localhost:3000
import anthropic
import spendbook
from spendbook import track
spendbook.init() # reads SPENDBOOK_API_KEY / SPENDBOOK_ENDPOINT
spendbook.instrument() # records usage from the anthropic and openai clients
client = anthropic.Anthropic()
with track(customer_id="acme", feature="support-bot", user_id="u_123"):
client.messages.create(
model="claude-opus-5-5",
max_tokens=512,
messages=[{"role": "user", "content": "Hello"}],
)
# Token counts, model, latency and attribution are sent. Prompt content is not.- Events are queued in memory and sent in background batches. If AI Spendbook can't be reached, your AI calls are never blocked.
- Content capture is off by default. Set
SPENDBOOK_DISABLED=1to turn the SDK off completely.
Ingestion API
Send events from any language. Use POST http://localhost:3000/api/v1/ingest/events for a single event or /ingest/batch with {"events": [...]} for up to 500, with Authorization: Bearer afk_…. The response reports accepted, duplicate and rejected events.
// Send one event per AI request from any backend (Node 18+).
const SPENDBOOK_URL = "http://localhost:3000/api/v1/ingest/events";
export async function recordAiUsage(e: {
requestId: string; model: string; inputTokens: number; outputTokens: number;
customerId?: string; feature?: string; latencyMs?: number;
}) {
const res = await fetch(SPENDBOOK_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPENDBOOK_API_KEY}`, // afk_… ingestion key
"Content-Type": "application/json",
},
body: JSON.stringify({
event_id: e.requestId,
occurred_at: new Date().toISOString(),
provider: "anthropic",
model: e.model,
input_tokens: e.inputTokens,
output_tokens: e.outputTokens,
customer_id: e.customerId,
feature: e.feature,
request_id: e.requestId,
latency_ms: e.latencyMs,
status: "success",
}),
});
if (!res.ok) console.warn("AI Spendbook ingest failed", res.status);
}
// Batch up to 500 events with POST /api/v1/ingest/batch {"events": [...]}.curl -sS -X POST "http://localhost:3000/api/v1/ingest/events" \
-H "Authorization: Bearer $SPENDBOOK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_id": "test-'"$(date +%s)"'",
"occurred_at": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
"provider": "anthropic",
"model": "claude-haiku-5-5",
"input_tokens": 1200,
"output_tokens": 300,
"feature": "connection-test"
}'| Field | Type | Description |
|---|---|---|
| event_id * | string | Unique per request; re-sending the same id is a no-op (idempotent). |
| occurred_at * | ISO-8601 timestamp | When the request happened, with a time zone (UTC recommended). |
| provider * | string | anthropic, openai, google, azure_openai, aws_bedrock, … |
| model * | string | Model id as sent to the provider. |
| input_tokens * | integer | Uncached input tokens. |
| output_tokens * | integer | Output tokens (including reasoning tokens if billed as output). |
| cache_read_tokens | integer | Prompt-cache read tokens. |
| cache_write_tokens | integer | Prompt-cache write tokens. |
| customer_id | string | Your customer or account ID. Powers customer economics. |
| end_user_id | string | Individual end user (hash it if it's personal data). |
| feature | string | Product feature that made the call. |
| category | string | Usage category; otherwise rules / classification apply. |
| project_id | string | Project or environment. |
| request_id | string | Provider request id; used to de-duplicate against provider data. |
| latency_ms | integer | End-to-end latency. |
| status | string | success | error | cancelled. |
| metadata | object | Small, allow-listed key/values. Never put prompts or secrets here. |
The full schema is in the API's OpenAPI document at http://localhost:3000/api/v1/openapi.json.
Reconciliation
AI Spendbook keeps three ledgers and never adds them together:
- Billed: charges from provider cost reports, billing exports or invoices. When billed data exists for a day, it is the total for that day.
- Provider usage estimate: token usage reported by the provider, priced at published (or your contract) rates. Used for days the provider hasn't billed yet.
- Telemetry estimate: request-level events from your app or SDK, priced the same way. Used where there's no provider data.
Request-level data explains the total by customer, feature and user. The part of billed spend that request-level data doesn't cover is shown as unattributed. It isn't spread across customers. The reconciliation report shows, per day and provider, the billed amount, each estimate, and how far they differ.
“Billing complete through” marks the last day for which provider billing is final. Days after it are estimates and can change. If the same request arrives from two sources (for example the SDK and a database mapping), it's matched by provider request id and only counted once.
AI Analyst
- It answers by calling tools that query your workspace's ledger, and each answer cites the tool calls and data it used.
- When the data can't answer a question, it says what it couldn't determine instead of guessing.
- Analysts, admins and owners can use it. It only sees the workspace you are in.
Security & telemetry
- Credentials are stored in a secret manager and never returned by the API. You only see a masked hint.
- Every request checks your workspace membership and role on the server. Non-members get “not found”.
- Connector and settings changes, key creation and member changes are recorded in the audit log (Settings → Audit log).
- Data policy (Settings → Data & privacy), per workspace: store request metadata (on by default), allow AI classification of usage categories (off by default; sends request metadata to the configured model provider when on), allow content capture (off by default), and retention in days.
What the SDK sends
Model, provider, token counts, latency, status, your attribution fields (customer, user, feature, category, project) and metadata keys you allowlist. Prompt and completion text are sent only if both the SDK option and the workspace content-capture policy are enabled. To turn the SDK off, set SPENDBOOK_DISABLED=1 or revoke the ingestion key.
Questions? Contact support.