GVGanivra

Integration guide

From model and MCP calls to customer margin.

Two lines of Python, or one POST from any language. Ganivra never sits in your request path: your calls still go straight to OpenAI, Anthropic or Google, and Ganivra prices the usage you send it afterwards.

  • No proxy
  • No prompts or responses needed
  • 14-day free trial

01 · Quickstart

One real call, one acknowledged event.

This Python example makes one synchronous OpenAI Responses call and submits its actual usage to Ganivra. It incurs your provider’s normal usage charge. Use a development workspace and a model available to your provider account. No pricing plan is needed to verify cost ingestion.

  1. Get your workspace key. Sign in, select the intended workspace, then open Profile & setup. An owner or admin can generate a key. Copy a new key when it is shown; later visits show only its masked identifier. Regenerating an existing key invalidates it.
  2. Prepare your environment. Use Python 3.10+ in a virtual environment. Configure GANIVRA_API_KEY, OPENAI_API_KEY, and OPENAI_MODEL through your server’s secret/environment configuration. The Ganivra workspace key and provider key are different credentials.
Terminal · install the documented SDK
python -m pip install "ganivra>=0.6.3,<0.7"

Save the following as first_event.py, then run python first_event.py. This example uses the explicit client only: do not also initialize automatic capture for this call.

Python · first_event.py
import os
from datetime import datetime, timezone
from time import perf_counter
from uuid import uuid4
from openai import OpenAI
from ganivra import Client

# Use a workspace you control; keep both keys on your server.
telemetry = Client(api_key=os.environ["GANIVRA_API_KEY"])
provider = OpenAI()  # Reads OPENAI_API_KEY
execution_id = "exec_" + uuid4().hex
occurred_at = datetime.now(timezone.utc).isoformat()
started = perf_counter()
response = provider.responses.create(
    model=os.environ["OPENAI_MODEL"],
    input="Reply with one short greeting.",
)
latency_ms = (perf_counter() - started) * 1000
usage = response.usage
if usage is None:
    raise RuntimeError("No usage returned; do not invent token counts.")

payload = {
    "event_id": "evt_" + uuid4().hex,
    "execution_id": execution_id,
    "step_id": "step_" + uuid4().hex,
    "timestamp": occurred_at,
    "provider": "openai",
    "model": response.model,
    "operation": "responses.create",
    "input_tokens": usage.input_tokens,
    "output_tokens": usage.output_tokens,
    "cached_tokens": getattr(usage.input_tokens_details, "cached_tokens", 0) or 0,
    "total_tokens": usage.total_tokens,
    "latency_ms": latency_ms,
    "status": "success",
    "environment": "development",
    "attributes": {
        "application": "integration-check",
        "customer_id": "test_tenant_42",
        "workflow": "first_call",
        "feature": "greeting",
    },
}
# For an uncertain delivery, retry this exact payload; do not rerun the model.
result = telemetry.events.create(payload=payload)
print({"execution_id": execution_id, "ingestion": result})

Successful first delivery returns HTTP 202 with {"accepted": 1, "duplicates": 0}. Retrying the identical payload returns {"accepted": 0, "duplicates": 1}. Save the printed execution ID. Acceptance confirms ingestion, not that model pricing or customer revenue is configured.

For Gemini, use the completed-usage example. For Claude or automatic capture, see the SDK reference. For another language, use the REST event contract.

02 · Verify

Find the exact execution you sent.

  1. Open the dashboard in the workspace that owns the ingestion key. Select a period containing the occurrence timestamp and clear unrelated filters.
  2. Find the printed execution ID in Executions, or open /executions/YOUR_EXECUTION_ID on the app. For a client workspace, preserve its workspace_id query parameter.
  3. Check the model, token counts, customer test_tenant_42, workflow first_call, and recorded cost. An unpriced model needs a matching provider/model rate; it is not free usage.
  4. Expect revenue and contribution to remain unknown until you connect a customer plan or reported revenue. Add these in the next section.

Keep test data separate. The environment label does not isolate a workspace or exempt calls from its allowance. Use a separate workspace/key for testing when available. Do not put demonstration usage in a production customer’s records.

03 · Customer economics

Answer a question your provider bill cannot.

“Which customer or feature costs more to serve than it earns?” You do not need every integration to begin. Follow one execution from usage to customer revenue.

Continuing the quickstart: replace test_tenant_42 with your application’s stable tenant ID. In Profile & setup, create a customer pricing plan with its actual rate and an effective date covering the execution. For future calls, add its exact plan_key inside the quickstart payload’s attributes. To connect the already-recorded call, assign that customer to the plan with a covering effective date; do not resend its event ID with changed facts.

The trace example below is for automatic capture. If you use the explicit quickstart client, set the same attribution fields directly on payload["attributes"] instead. Check revenue and contribution after the plan is connected; missing coverage remains unknown.

1. Confirm the connection

Run a supported call in your existing app, then check the dashboard. Open its execution to verify the model, tokens, latency, and cost. An accepted event confirms delivery; a priced event gives you your first cost insight.

The Python SDK captures supported calls made after initialization. REST clients send actual usage. Neither path imports historical provider usage automatically.

2. Add the customer context

Read a stable customer ID from your app. Add application and feature so spend has an owner. Customer IDs appear automatically; no separate customer registration is required.

# Python: wrap your existing AI call
with ganivra.trace(
    application="support-app",
    customer_id=account.id,
    feature="support_assistant",
    workflow="resolve_ticket",
    plan_key="starter",
):
    # Your existing supported AI call runs here
    ...

REST: send these same fields inside attributes. Replace account.id with the customer ID from your own request context.

3. Connect customer revenue

In Profile & setup, define what the customer pays. Send the configured plan_key with each execution or assign the customer to that plan. You can also import confirmed revenue.

Your customer's pricing plan is separate from your Ganivra subscription. Profit requires revenue and complete cost data; unknown revenue is not a confirmed loss.

Configure customer pricing →

4. Choose one improvement

Illustrative example: $50 of customer revenue and $80 of AI cost means a $30 contribution loss. Inspect the model and MCP steps to locate the spend before changing anything.

Test a cheaper model, reduce unnecessary calls, or review your customer pricing in your own app. Compare equivalent periods and check output quality as well as cost. Ganivra does not automatically change your routing or prices.

Read the coverage indicators before acting. Contribution margin here uses the costs and revenue you supplied; it is not total company profit. With only a few calls, prioritize execution details over cohort or trend conclusions.

Managing plans, margins or workspace settings? Read the user manual →

04 · Production setup

Choose the delivery guarantees you need.

Automatic capture

Initialize once in each server/worker process before creating supported provider clients. It sends through a bounded in-memory queue. The model call continues if telemetry cannot be sent; a full queue, exhausted retries, or process termination can lose events.

Explicit client or REST

Wait for acknowledgement and handle exceptions in your application. Client defaults to a 10-second timeout and does not automatically retry writes. For reliable delivery, persist an event in an outbox before sending, then mark it delivered after acknowledgement. Keep the same IDs and payload through retries.

Automatic capture configuration and shutdown
Python · worker configuration
import os
import ganivra

ganivra.init(
    api_key=os.environ["GANIVRA_API_KEY"],
    endpoint="https://api.ganivra.com",
    environment="production",
    queue_size=1000,
    batch_size=20,
    timeout_seconds=2.0,
    max_retries=2,
)
# Initialize once per worker process, before creating provider clients.
# Wrap supported calls with ganivra.trace(...) as shown below.
# At a job/shutdown boundary (allow time before process termination):
drained = ganivra.flush(timeout=5.0)
# True means the queue drained, NOT that every event was accepted.
# Verify delivery in Ganivra; use Client + a durable outbox when required.

The automatic defaults are a 1,000-event queue, batches of 20, a 2-second request timeout and two retries. flush() returns whether queued work finished before its timeout; failed deliveries can also drain the queue. It is not a delivery receipt.

  • Short jobs and serverless: allow time to flush before returning or shutting down. Freeze/termination can prevent background work and exit hooks from running. Use explicit acknowledged submission plus durable retries when event loss is unacceptable.
  • Web servers: initialize per worker after forking; do not create a sender thread in a parent process and expect child workers to inherit it. Avoid reinitializing on every request.
  • Keys and environments: keep keys in server secrets, never browser bundles. Set environment explicitly. The production base URL is https://api.ganivra.com; for a local backend, set endpoint to its actual origin, usually http://localhost:8000. Production and development use different keys when they use different workspaces.
  • Identifiers: one execution per business action, one event and step per actual model/tool call. Use stable pseudonymous tenant IDs for customers and separate user IDs for individual actors. Never send names as customer IDs, prompts, responses, tool arguments, credentials, or contact details.
  • Failures and streams: emit status: "error" with a bounded error_type for observed failed calls. Do not invent usage when the provider returns none. For supported Claude streaming, consume or explicitly close streams; interrupted streams have incomplete usage. For unsupported streaming, aggregate the provider’s final usage once, never once per text chunk.
  • Delivery retries versus model retries: a network retry of telemetry keeps the event ID and timestamp. A new paid model attempt gets a new event/step under the same business execution. Do not submit an automatically captured call again through REST.

05 · Troubleshooting

Start with the response, then the execution.

Keep the status code, event/execution ID, occurrence time, and a redacted validation error for investigation. Never log authorization headers, provider response bodies, or customer content.

SymptomWhat to check
401 · UnauthorizedUse a complete, active Ganivra workspace key for ingestion—not a provider key or masked identifier. Check the Bearer header and update server secrets after key rotation.
403 · ForbiddenIngestion keys are write-only. Reporting and administration require a signed-in user token with access to the selected workspace; management operations need the appropriate role.
409 · Conflicting eventAn existing event ID was sent with changed facts, or a concurrent write conflicted. For delivery retries, reuse the exact saved payload including timestamp. Use a correction endpoint for a factual correction; use a new event ID for an actual new call.
413 · Request too largeThe default request limit is 512 KiB. Split the batch without changing existing IDs or timestamps. Model/MCP batches allow at most 100 events; delivery-cost batches allow at most 500.
422 · Invalid payloadRead the response’s field errors. Check environment, latency_ms, status, IDs, provider/model/operation, non-negative usage, token totals, and a timezone-aware timestamp. Do not retry unchanged invalid data. Unknown top-level fields are rejected.
429 · Too many requestsHonor Retry-After, then retry the same saved payload with backoff and jitter. Keep pending events in your own durable outbox. Avoid synchronized retries from every worker.
Timeout or transient 5xxThe outcome may be uncertain. Retry the same ingestion event or batch rather than generating new IDs. Do not blindly retry non-idempotent operations such as plan creation or key rotation; inspect state first.
Accepted, but missing from the dashboardCheck the workspace selected by the key, occurrence timestamp and dashboard period, filters, and execution ID. A duplicate response does not create a second event. Automatic capture covers only supported APIs called after initialization.
Unpriced or unexpectedly low costCompare the exact provider/model ID and usage components with pricing coverage and any private rates. Missing prices are unknown cost. Inspect cache and thinking tokens; do not assume every quantity in usage_details has a price.
Unknown revenue or contributionSend a configured plan_key or assign the same customer_id to a plan effective at the execution time. Credit/unit plans require their configured usage attribute. Reported transactions are a separate revenue source; avoid recording the same payment twice.

Advanced reference

Provider examples, event fields, and business workflows.

Use these details after verifying your first execution. Existing integrations can link directly to each section.

OpenTelemetry · No SDK

Already exporting OpenTelemetry? Point it at Ganivra.

Ganivra accepts OTLP over HTTP at https://api.ganivra.com/v1/otlp, as protobuf or JSON, optionally gzipped. Model-call spans that follow the GenAI semantic conventions, Claude Code's api_request events and Codex's completed responses become priced events. Other spans and logs are ignored, and prompt and response content is never stored.

Claude Code & OpenAI Codex · Quick setup

Connect both coding agents with one command.

On macOS or Linux, the Ganivra setup command finds Claude Code and Codex on the machine, asks for your workspace key once, checks it, and writes each tool's settings for you. Then label each project with one more command. It works the same whether a developer uses Claude Code, Codex or both.

  1. Create a key. In Profile & setup, create a workspace API key.
  2. Run the setup command on each developer's machine and paste the key when asked. Add --team platform to set a default team.
  3. Label each project once. In the repository, run ganivra label --project acme-checkout --client acme and commit the .claude/settings.json it writes. Everyone who clones the repository then sends the same labels, from Claude Code and Codex alike.
  4. Check it. Run ganivra status to see what is connected and this folder's labels, then open Dashboard → Coding agents.
What it changes
~/.claude/settings.json, ~/.codex/config.toml (both backed up first), one line in ~/.zshrc or ~/.bashrc, and ~/.ganivra. On a Mac with the ChatGPT or Codex app, it also sets the app's team at login
What it never does
Read prompts or code, or replace another telemetry export without asking
Undo
ganivra uninstall removes the export, the shell line and ~/.ganivra
Windows, or by hand
Follow the manual steps for Claude Code and Codex below
SHEach developer's machine · once
curl -fsSL https://ganivra.com/install.sh | sh

# With a default team, without prompts:
curl -fsSL https://ganivra.com/install.sh | GANIVRA_API_KEY=gv_live_... sh -s -- --team platform
SHEach repository · once
cd ~/code/acme-checkout
ganivra label --project acme-checkout --client acme
git add .claude/settings.json && git commit -m "Label AI coding costs"

# Labels only you use (not committed):
ganivra label --local --project acme-spike
SHCheck · any folder
ganivra status

Read the script before running it: ganivra.com/ganivra.sh.

Claude Code

See Claude Code spend by developer, team, project and session.

There is nothing to install in Ganivra. You configure Claude Code on each developer's machine to send its telemetry here. Token counts, model, developer, session and your labels are sent; prompts and code are not.

  1. Create a key. In Profile & setup, create a workspace API key. To keep your own engineering spend apart from your product's customer costs, use a separate workspace for it.
  2. Configure Claude Code. Run quick setup, or add the env block to ~/.claude/settings.json on each machine, or push it once to every developer through managed settings. It lives in the home folder, so the key stays out of your repositories.
  3. Label each project in the project. Commit a .claude/settings.json to each repository with its team, project and client, so every developer sends the same labels. Unlabelled projects fall back to the git origin remote, such as acme/checkout. See teams, projects and clients.
  4. Check the result. After about a minute, open Dashboard → Coding agents and switch between developers, teams, projects and models.

Each API request becomes one event: the developer's email is the user, the session is the conversation, and each prompt is one execution. If Claude Code signs in with an API key and reports no email, add ganivra.user_id to name the developer.

Calls to Anthropic's API are priced from Ganivra's rates, including cache reads, cache writes and fast mode. On Bedrock or Vertex AI, Ganivra keeps the cost Claude Code reports. On a Pro, Max, Team or Enterprise seat, the figures show what the same usage would cost at API rates, not your invoice.

OTEL_RESOURCE_ATTRIBUTES
Your own keys, such as team.id or cost_center, become filterable attributes. It is one variable: a new value replaces the old one, so list every key each time
OTEL_METRICS_INCLUDE_REPOSITORY
true adds the git repository to each request, so spend groups by project with no labels (Claude Code 2.1.269 or later)
ganivra.workflow, ganivra.customer_id
Name the project and client yourself; the short keys project and client work too. A label wins over the repository
Where to set labels
Export settings and a default team in ~/.claude/settings.json; team, project and client in each repository's committed .claude/settings.json, repeating the team keys. The project file overrides your user settings and works in the terminal, IDE and desktop app
OTEL_LOGS_EXPORT_INTERVAL
60 seconds batches each developer's calls into one request per minute
JSON~/.claude/settings.json
{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_METRICS_EXPORTER": "none",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "https://api.ganivra.com/v1/otlp",
    "OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer gv_live_...",
    "OTEL_LOGS_EXPORT_INTERVAL": "60000",
    "OTEL_METRICS_INCLUDE_REPOSITORY": "true",
    "OTEL_RESOURCE_ATTRIBUTES": "team.id=platform,cost_center=eng-123"
  }
}
JSONEach repository · .claude/settings.json (commit it)
{
  "env": {
    "OTEL_RESOURCE_ATTRIBUTES": "team.id=platform,project=acme-checkout,client=acme"
  }
}

OpenAI Codex

See Codex spend by developer, team, project and session.

Codex sends its telemetry from each developer's machine, configured in ~/.codex/config.toml. A project's own .codex/config.toml cannot set telemetry, so use the file in your home folder, or enforce the same settings for everyone through Codex's managed configuration.

  1. Create a key. In Profile & setup, create a workspace API key, or reuse the one your Claude Code export uses.
  2. Add the [otel] block. Paste the key itself after Bearer: Codex sends header values exactly as written and does not fill in ${VARIABLES} from your shell.
  3. Label teams and projects. Codex reports no folder or repository and reads no project files. Quick setup adds a codex shell function (or copy it from teams, projects and clients): it reads each repository's .claude/settings.json, so one file labels both tools. Desktop app sessions get their project in Ganivra's Sessions view.
  4. Check the result. After a response or two, open Dashboard → Coding agents and choose Tools to compare Codex with Claude Code.

Each completed model response becomes one event, priced at OpenAI's list rate: input, cached input and output tokens, with reasoning counted as output, and fast mode at the priority rate. The developer is the email Codex reports when you sign in with ChatGPT, or else the account ID, and each Codex session is one execution. Prompts are not sent while log_user_prompt is false, and Ganivra would not store them anyway.

On a ChatGPT Plus, Pro, Business or Enterprise plan, the figures show what the same usage would cost at API rates, not your bill.

endpoint
Must end in /v1/logs. Codex uses the address exactly as written and does not add the path
protocol
"binary" (protobuf) or "json". Both work
headers
The literal workspace key. Keep this file out of any repository
OTEL_RESOURCE_ATTRIBUTES
team.id, cost_center, project and client. Read once when Codex starts; the shell function sets it per project
TOML~/.codex/config.toml
[otel]
environment = "production"
log_user_prompt = false

[otel.exporter.otlp-http]
endpoint = "https://api.ganivra.com/v1/otlp/v1/logs"
protocol = "binary"
headers = { Authorization = "Bearer gv_live_..." }
SHLabels per project · ~/.zshrc
# Default team when a project has no labels:
export OTEL_RESOURCE_ATTRIBUTES="team.id=platform"

# Then add the ganivra_labels and codex functions from
# "Teams, projects and clients" below. They read each
# repository's .claude/settings.json.

Coding agents · Teams, projects and clients

Label each project once, in the project itself.

Costs arrive without labels, grouped by developer and model. The way that scales to hundreds of projects is one small file per repository: .claude/settings.json, committed with the code, holding the project's team, project and client. Claude Code reads it automatically in the terminal, IDE and desktop app, every developer who clones the repository sends the same labels, and the Codex CLI reads the same file through a short shell function. Nobody has to relabel their machine when they switch projects. All labels travel in one variable, OTEL_RESOURCE_ATTRIBUTES, as comma-separated key=value pairs such as team.id=platform,project=acme-checkout,client=acme. For anything a file can't label, such as the Codex desktop app or past usage, set the project in Ganivra: Dashboard → Coding agents → Sessions.

LabelKeyShown as
Teamteam.id, or team, team.name, cost_center (the first one set wins)Teams view, team budgets, team allocation rules
Projectproject, or ganivra.workflowProjects view and the workflow filter. Without a label, Claude Code can fill it from the git repository
Clientclient, or ganivra.customer_idClient column in Projects, for billing a client or charging back a project
Cost centrecost_centerA filterable attribute; used as the team when no team key is set
Where the agent runsRecommended: per projectFallback
Claude Code: terminal, IDE or desktop appThe repository's committed .claude/settings.json, or .claude/settings.local.json for labels you keep to yourself. Picked up automatically once the folder is trustedThe git origin remote with OTEL_METRICS_INCLUDE_REPOSITORY=true (2.1.269 or later), then the folder's name for desktop sessions that report one, else "No project"
Codex CLI in a terminalThe same .claude/settings.json file, read by the codex shell function below. Codex itself reads no project filesWith the function, the git root's folder name; without it, "No project"
Codex in the ChatGPT or Codex desktop appNot possible: one app process serves every project. Set the team for the app (macOS launchctl setenv, Windows setx) and reopen itAssign projects to its sessions in Ganivra → Coding agents → Sessions

Set it up for every project

  1. Keep the export at user level. Quick setup does this for you. The endpoint and API key stay in ~/.claude/settings.json and ~/.codex/config.toml on each machine, never in a repository. Claude Code ignores exporter settings in project files anyway.
  2. Add one file per repository. Run ganivra label --project name --client name in it, or write .claude/settings.json by hand as shown, then commit it. An existing file keeps its other settings. For many repositories at once, use the loop below, then set each client.
  3. Give Codex the same labels. Quick setup adds a codex function to your shell, or paste the one below into ~/.zshrc or ~/.bashrc. It reads the project's .claude/settings.local.json, then .claude/settings.json, and otherwise names the project after the git root's folder.
  4. Handle the rest in Ganivra. Desktop Codex sessions and usage from before the labels arrive as "No project". Assign them in Sessions.

Rules that keep labels clean

  1. Use simple values. Letters, digits, -, _ and ., such as platform or acme-checkout. No spaces or commas, which break the variable.
  2. Repeat every key. The project file's value replaces the one in your user settings completely, so it must also carry team.id.
  3. Keep it on one line. Write "OTEL_RESOURCE_ATTRIBUTES": "…" on a single line so the codex function can read it.
  4. Know which value wins. For Claude Code: managed settings, then .claude/settings.local.json, then .claude/settings.json, then ~/.claude/settings.json, then the shell. A project or client edited in Ganivra wins over all of them.
  5. Trust the folder. Claude Code applies a shared project file only after you accept the folder trust prompt the first time you open it.
  6. Restart after a change. Start a new Claude Code session or codex run. Labels apply to new requests only; earlier usage keeps its labels unless you edit it in Sessions.
  7. Check it. In Dashboard → Coding agents, choose Teams and Projects. A label you set but don't see usually means the session was not restarted, the folder isn't trusted, or a higher setting overrides it.

Editing in Ganivra: in Dashboard → Coding agents, choose Sessions, then type a project and client for a session, or select several and set them together. The change covers every request in the session, including later ones, and Reset restores what the tool reported. Owners and admins can edit. Only a folder's name is ever kept, never its full path.

JSONEvery repository · <repo>/.claude/settings.json (commit it)
{
  "env": {
    "OTEL_RESOURCE_ATTRIBUTES": "team.id=platform,project=acme-checkout,client=acme"
  }
}
JSONLabels only you use · <repo>/.claude/settings.local.json (not committed)
{
  "env": {
    "OTEL_RESOURCE_ATTRIBUTES": "team.id=platform,project=acme-spike,client=acme"
  }
}
SHCodex CLI · ~/.zshrc or ~/.bashrc
# Codex reads no project files, so take the labels from the project's
# .claude/settings.local.json or .claude/settings.json (the files Claude Code
# reads), else name the project after the folder.
ganivra_labels() {
  local root file labels name
  root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
  for file in "$root/.claude/settings.local.json" "$root/.claude/settings.json"; do
    [ -f "$file" ] || continue
    labels=$(sed -n 's/.*"OTEL_RESOURCE_ATTRIBUTES"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$file" | head -n 1)
    if [ -n "$labels" ]; then printf %s "$labels"; return; fi
  done
  case ",$OTEL_RESOURCE_ATTRIBUTES," in
    *,project=*|*,ganivra.workflow=*) printf %s "$OTEL_RESOURCE_ATTRIBUTES"; return ;;
  esac
  name=${root##*/}; name=${name//[ ,]/-}
  printf %s "${OTEL_RESOURCE_ATTRIBUTES:+$OTEL_RESOURCE_ATTRIBUTES,}project=$name"
}
codex() { OTEL_RESOURCE_ATTRIBUTES="$(ganivra_labels)" command codex "$@"; }
SHMany repositories at once · run from the folder that holds them
# Creates .claude/settings.json named after each folder.
# Skips repositories that already have one. Then add client=
# by hand where it applies, and commit the files.
for repo in */; do
  name=${repo%/}; name=${name//[ ,]/-}
  file="$repo.claude/settings.json"
  if [ -e "$file" ]; then echo "skipped $file (add the env entry by hand)"; continue; fi
  mkdir -p "$repo.claude"
  printf '{\n  "env": {\n    "OTEL_RESOURCE_ATTRIBUTES": "team.id=platform,project=%s"\n  }\n}\n' "$name" > "$file"
done
MACCodex desktop app · team only · Terminal, then reopen the app
launchctl setenv OTEL_RESOURCE_ATTRIBUTES "team.id=platform,cost_center=eng-123"
osascript -e 'quit app "ChatGPT"' && open -a ChatGPT

# launchctl setenv lasts until you restart the Mac.
# To keep it, run the same launchctl line at login,
# for example from a LaunchAgent.
WINCodex desktop app · team only · PowerShell, then reopen the app
setx OTEL_RESOURCE_ATTRIBUTES "team.id=platform,cost_center=eng-123"
# Applies to apps started after this; sign out and in if it doesn't.

Any GenAI instrumentation

GitHub Copilot, OpenLLMetry, OpenAI and Anthropic instrumentations.

Ganivra reads chat, generate_content, text_completion and embeddings spans that carry gen_ai.usage.input_tokens or gen_ai.usage.output_tokens. Agent and tool spans are skipped so their totals are not counted twice. The trace becomes the execution and each span a step.

Add customer context as span or resource attributes. Without it, costs still arrive, grouped by service and model.

ganivra.customer_id
Your customer or tenant ID
ganivra.workflow, ganivra.feature
What the call was for
ganivra.plan_key
The customer's pricing plan configured in Ganivra
deployment.environment.name
Environment; defaults to production
ENVAny OpenTelemetry SDK
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.ganivra.com/v1/otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer gv_live_..."
OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=production"

# On each model-call span, for example in Python:
span.set_attribute("ganivra.customer_id", "customer_123")
span.set_attribute("ganivra.workflow", "support_answer")

Self-hosted models · Own GPUs

Running models on your own GPUs? Get a cost per call anyway.

GPUs bill by the hour, busy or idle, so a call on vLLM or SGLang has no list price. Send those calls with provider self_hosted, add the GPUs as a GPU pool, and Ganivra splits each UTC day's GPU bill across that day's calls. Per-call costs add up to the bill, so cost per customer, feature and workflow works as it does for API calls.

Step 1

Send self-hosted calls.

With the Python SDK, list your model servers once. Calls to those hosts are recorded as self_hosted with the model ID your server returns, and the SDK sends the response id as attributes.request_id.

With REST, set the provider yourself and send the response id as attributes.request_id, so vLLM's GPU time can be matched to the call.

provider
self_hosted
model
The model ID your server serves, e.g. deepseek-ai/DeepSeek-V4.1-Flash
attributes.request_id
The response id, to match GPU time
attributes.customer_id
Required for a pool rented to one customer
PYTHONganivra.init
import ganivra

ganivra.init(
    api_key="gv_live_...",
    self_hosted_hosts=["vllm.internal"],
)

# REST: the usual event fields, plus
{
  "provider": "self_hosted",
  "model": "deepseek-ai/DeepSeek-V4.1-Flash",
  "operation": "chat.completions.create",
  "attributes": {
    "customer_id": "acme",
    "request_id": "chatcmpl-8f2c..."
  }
}

Step 2

Add the GPUs as a pool, shared or rented to one customer.

Use Dashboard → Own GPUs, or the API with an admin token. A pool covers every self-hosted model unless you list its models. To change its size or price, end it and add a new one, so earlier days keep their cost.

Set customer_id when the GPUs are rented to one customer. That pool serves only their calls, and every hour of it, idle included, is their cost. Add sell_hourly_rate_usd, what they pay per GPU-hour, and the dashboard and delivery report show their revenue and margin. Calls on those GPUs need no pricing plan.

hourly_rate_usd
What one GPU-hour costs you
customer_id
Optional. Rents the pool to this customer
sell_hourly_rate_usd
Optional, with customer_id. What they pay per GPU-hour
GET /v1/economics/gpu-tenants
Each rented pool's customer: cost, idle cost, revenue and margin
POST/v1/economics/gpu-pools
{
  "name": "Acme node",
  "gpu_type": "B200",
  "gpu_count": 8,
  "hourly_rate_usd": 6.69,
  "models": ["deepseek-ai/DeepSeek-V4.1-Flash"],
  "effective_from": "2026-11-01T00:00:00Z",
  "customer_id": "acme",
  "sell_hourly_rate_usd": 9.50
}

Optional

Split by real GPU time, and measure idle hours.

Without more data, calls are split by weighted tokens, with output counted 4 times. Point vLLM's traces at Ganivra and each call's share becomes the time it spent in the model. The traces add no calls of their own.

For utilization, an OpenTelemetry Collector scrapes NVIDIA's DCGM exporter, or vLLM's metrics without DCGM, and sets ganivra.gpu_pool to the pool's name. Each hour's unused share becomes idle cost. Collector settings and the limits of these estimates →

SHELLvLLM traces
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf \
OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer gv_…" \
vllm serve deepseek-ai/DeepSeek-V4.1-Flash \
  --otlp-traces-endpoint https://api.ganivra.com/v1/otlp/v1/traces

Provider reference

Submit actual usage from completed calls.

The automatic adapter covers synchronous, non-streaming OpenAI Responses, synchronous OpenAI Chat Completions (streaming included, also for Together AI, Fireworks, DeepInfra and Baseten), and Claude Messages sync, async and streaming with the Anthropic extra. Gemini, asynchronous OpenAI Chat Completions, asynchronous/streaming OpenAI Responses, and other unsupported APIs need explicit submission. These examples use no automatic capture.

Gemini · Python text-only example

Install google-genai alongside ganivra>=0.6.3,<0.7. Configure GEMINI_API_KEY, GEMINI_MODEL, and GANIVRA_API_KEY. This performs a real, billable provider call. It does not cover grounding, tool-use charges, audio, or image pricing.

Python · completed Gemini usage
import os
from datetime import datetime, timezone
from time import perf_counter
from uuid import uuid4
from google import genai
from ganivra import Client

provider = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
telemetry = Client(api_key=os.environ["GANIVRA_API_KEY"])
model = os.environ["GEMINI_MODEL"]
started = perf_counter()
occurred_at = datetime.now(timezone.utc).isoformat()
response = provider.models.generate_content(
    model=model, contents="Reply with one short greeting."
)
latency_ms = (perf_counter() - started) * 1000
usage = response.usage_metadata
if usage is None or usage.prompt_token_count is None or usage.total_token_count is None:
    raise RuntimeError("No complete usage returned; do not report zero cost.")
if usage.tool_use_prompt_token_count:
    raise RuntimeError("This text-only example does not map tool-use charges.")
input_tokens = usage.prompt_token_count
# Includes thinking tokens; cached input is a subset of prompt tokens.
output_tokens = usage.total_token_count - input_tokens
payload = {
    "event_id": "evt_" + uuid4().hex,
    "execution_id": "exec_" + uuid4().hex,
    "step_id": "step_" + uuid4().hex,
    "timestamp": occurred_at,
    "provider": "google",
    "model": response.model_version or model,
    "operation": "generate_content",
    "input_tokens": input_tokens,
    "output_tokens": output_tokens,
    "cached_tokens": usage.cached_content_token_count or 0,
    "total_tokens": usage.total_token_count,
    "latency_ms": latency_ms,
    "status": "success",
    "environment": "development",
    "attributes": {
        "application": "integration-check",
        "customer_id": "test_tenant_42",
        "workflow": "gemini_text",
    },
}
print(telemetry.events.create(payload=payload))

This mapping includes thinking in output tokens and keeps cached tokens inside the input total. Ganivra’s Google event normalization treats residual non-input tokens as output. Verify cost coverage for the resolved model and workload; token acceptance alone does not establish complete cost. See Google’s usage metadata documentation.

Unsupported APIs · adapt your completed usage

This helper is an integration pattern, not a provider client. Supply the actual resolved model, operation, usage counts, duration and occurrence time from your application. Pass your existing business execution ID so multiple steps stay together. The quickstart shows a complete runnable example.

Python · explicit usage adapter
from datetime import datetime, timezone
from uuid import uuid4

def record_completed_call(telemetry, *, provider, model, operation,
                          input_tokens, output_tokens, cached_tokens,
                          total_tokens, latency_ms, occurred_at,
                          execution_id, customer_id):
    # These values must come from the completed provider response and your timer.
    # Map each provider's cache/reasoning semantics before calling this helper.
    payload = {
        "event_id": "evt_" + uuid4().hex,
        "execution_id": execution_id,
        "step_id": "step_" + uuid4().hex,
        "timestamp": occurred_at,
        "provider": provider,
        "model": model,
        "operation": operation,
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
        "cached_tokens": cached_tokens,
        "total_tokens": total_tokens,
        "latency_ms": latency_ms,
        "status": "success",
        "environment": "production",
        "attributes": {"customer_id": customer_id, "workflow": "your_workflow"},
    }
    return payload, telemetry.events.create(payload=payload)

# Capture occurred_at BEFORE your provider call:
# occurred_at = datetime.now(timezone.utc).isoformat()
# Measure elapsed time with perf_counter(), then pass the actual usage above.
# Persist the payload before submitting if reliable retries are required.

Do not infer tokens from characters or assume missing usage means zero. Map cache writes/reads and reasoning according to that provider’s contract. Anthropic’s uncached input, cache reads and cache writes are separate components; use the supported adapter or include its cache-write fields in usage_details. For multimodal, search, and tools, supply supported priced components or a provider-reported per-event cost and verify the breakdown.

Python SDK 0.6.3

Send usage and manage economics from Python

Install ganivra>=0.6.3,<0.7 for the explicit REST client and the current integration helpers. Older releases do not expose the complete surface documented here.

Automatic capture supports synchronous, non-streaming OpenAI Responses, synchronous OpenAI Chat Completions (streaming included, also for Together AI, Fireworks, DeepInfra and Baseten), and Claude Messages sync, async and streaming with the Anthropic extra. Streamed Chat Completions need stream_options={"include_usage": True}; without it the call is recorded as unpriced. For Gemini and unsupported APIs, collect completed usage and submit it explicitly. Do not report the same call through both automatic capture and explicit submission.

Automatic Claude capture (SDK 0.6.3)

Install ganivra[anthropic]>=0.6.3,<0.7. The supported Anthropic version range is 0.125.x. Initialize Ganivra before creating the client.

import os
import ganivra
from anthropic import Anthropic

ganivra.init(api_key=os.environ["GANIVRA_API_KEY"])
client = Anthropic()
with ganivra.trace(customer_id="tenant_42", plan_key="growth",
                   workflow="document_review", agent_name="review_agent"):
    with client.messages.stream(
        model="claude-sonnet-4-6", max_tokens=1024,
        messages=[{"role": "user", "content": "Summarize the document."}],
    ) as stream:
        for text in stream.text_stream:
            print(text, end="")

Also supports messages.create, stream=True, and AsyncAnthropic. Consume streams or close them explicitly. Capture records usage once, including cache reads/writes and reported search/fetch counts, without collecting message content. Interrupted streams retain partial usage and are unpriced on the matching backend. Internal provider retries are not separately observed. Beta, batch and raw-response APIs, Bedrock/Vertex clients and Gemini are outside this adapter’s supported scope. Delivery remains best-effort.

Send a model event with an ingestion key
import os
from ganivra import Client

usage = Client(api_key=os.environ["GANIVRA_API_KEY"])
result = usage.events.create(payload={
    "event_id": "evt_saved_step_1",
    "execution_id": "exec_saved_action",
    "step_id": "step_saved_1",
    "timestamp": "2026-10-04T10:00:00Z",
    "provider": "anthropic",
    "model": "your-actual-provider-model-id",
    "operation": "messages.create",
    "input_tokens": 1000,
    "output_tokens": 200,
    "total_tokens": 1200,
    "latency_ms": 850,
    "status": "success",
    "environment": "development",
    "attributes": {
        "customer_id": "tenant_42",
        "workflow": "document_review",
        "agent_name": "review_agent",
        "plan_key": "growth"
    }
})

Replace the example with actual usage, identifiers and occurrence time. All REST event fields are accepted, including additional usage components and provider-reported costs. An unknown model still needs pricing configured. Batch up to 100 events with events.create_batch(payload={'events': events}).

Manage pricing and historical dates with a user session
admin = Client(
    access_token=lambda: session_manager.current_access_token(),
    workspace_id="receiving-workspace-id",
)
plan = admin.pricing_plans.create(payload={
    "name": "Growth", "plan_key": "growth",
    "pricing_model": "per_execution",
    "rate_config": {"rate": "2.00"},
    "effective_from": "2025-12-01T00:00:00Z"
})
assignment = admin.customer_plans.assign(payload={
    "customer_id": "tenant_42", "pricing_plan_id": plan["id"],
    "effective_from": "2026-01-01T00:00:00Z"
})
admin.customer_plans.update_dates(assignment["id"], payload={
    "effective_from": "2025-12-01T00:00:00Z"
})

Your application supplies and refreshes the signed-in user token; session_manager represents your own session code. Do not supply a service-role secret. Viewer, admin and owner permissions still apply. Plan dates must cover assignment dates, and assignments cannot overlap. Historical date edits recalculate live plan-based economics without rewriting usage, confirmed revenue or saved statements.

Available operations, reports and evidence
  • value.list / import_assessment: read and import customer value evidence.
  • revenue.create / import_entries and transactions.create (also economics.transaction): revenue entries, bulk import and confirmed financial transactions.
  • pricing_plans.create / update_dates, customer_plans.assign / update_dates: plans and historical coverage.
  • economics.create_contract_rate / reconcile / reprice: private rates, invoice comparison and repricing.
  • economics.gpu_pools / create_gpu_pool / end_gpu_pool / delete_gpu_pool: GPUs you run your own models on; each day's GPU cost is split across your self_hosted calls.
  • economics.gpu_tenants: customers renting dedicated GPU pools, with cost including idle hours, what they pay per GPU-hour and the margin.
  • economics.gpu_move_analysis: which features' API calls would cost less on a GPU pool, or on GPUs you are considering.
  • delivery_costs.create_batch / import_events / set_bill_policy: external costs, corrections, invoice matching and allocation.
  • delivery_costs.report / investigate / evidence / create_snapshot / get_snapshot: combined economics and supporting evidence.
  • reports.costs / export / allocation_rules / create_allocation_rule / retire_allocation_rule / reconcile: cost reporting, allocation rules and reconciliation.
  • events.correct / history: correct a complete event with a stable request UUID and reason, or inspect append-only correction and repricing evidence. Requires user-session authorization; original ingestion retries remain duplicates.
  • audit.list / export, executions.list / get / dashboard: administrative history and execution investigation.
  • bills.list / preview / import_rows / void: bill imports and corrections.
  • budgets.list / create / evaluate / review / set_owner / archive: budget management.
  • workspaces.list / create / get / rename / rotate_key / access / grant_access / revoke_access and account.consumption / bootstrap / rotate_key: workspace administration.
report = admin.delivery_costs.investigate(params={
    "start": "2026-10-01", "end": "2026-10-04"
})
archive = admin.reports.export(params={
    "start": "2026-10-01", "end": "2026-10-04", "format": "zip"
})  # Returns bytes; save to your chosen file.

Methods use REST-shaped payload for JSON bodies and params for query filters; resource IDs are positional arguments. Use the selected workspace consistently. Pagination uses the endpoint’s limit and offset. Exports return bytes; other responses retain the API’s JSON structure.

Explicit client calls require acknowledgement and raise APIError, with HTTP status_code, body, response headers and retry_after when available. They do not automatically retry writes. Persist identical IDs and payloads for safe ingestion retries; inspect state before retrying plan creation or key rotation after a timeout. This differs from the existing best-effort auto-capture queue.

From telemetry to customer economics

The fields that make your dashboard useful

API-required fields make an event valid. Business attribution and revenue make it useful. Missing revenue means Unknown contribution, not a confirmed loss.

For telephony, speech, OCR, compute, and other non-model charges, use the dedicated delivery-cost API or Python helper. Delivery-cost setup and invoice evidence →

FieldWhen to send itWhy it matters
Workspace API keyRequired · authenticationRoutes events to one workspace. Use that workspace’s key; a workspace_id in metadata does not change routing.
attributes.customer_idEssential for customer economicsYour stable tenant/account ID. Match the ID on revenue records and plan assignments. Missing IDs appear as Unattributed.
attributes.applicationRecommended · product attributionStable product or application name. Separate products within a workspace without changing the customer ID.
attributes.feature / workflowEssential for cost driversName the product capability and business process, such as lead_qualification / outbound_call. Use stable names, not URLs containing user IDs. Custom workflow names are supported.
attributes.plan_keyConditional · calculated revenueExact key of an effective pricing plan in this workspace, or configure a customer-plan assignment instead. This is your customer’s plan, not your Ganivra subscription. A key alone does not prove payment.
attributes.credits (or configured unit attribute)Required for credit/unit plansSend actual units consumed using the plan’s exact unit_attribute. Repeat the execution total across steps; Ganivra uses the maximum, not the sum. Other pricing models use their relevant captured usage.
event_id / execution_id / step_idRequired · identityOne execution ID per business action, a distinct event/step for each actual call. Preserve event IDs when redelivering telemetry; a real model retry gets a new event. Duplicates are scoped to a workspace.
timestampRecommended · occurrence timeSend the actual occurrence time with timezone. Omission defaults to receipt time. Economics uses occurrence time; Ganivra plan consumption uses server receipt time.
provider / model / operation + token countsRequired for model eventsSend actual input_tokens, output_tokens and total_tokens; include cached_tokens when applicable. Use the resolved model ID. Unknown prices are flagged, not treated as free.
event_type / mcp_server / mcp_toolRequired for MCP eventsUse event_type=mcp and identify server/tool. Omit event_type for model calls. Business labels like document_processed belong in workflow or custom attributes, not top-level event_type.
latency_ms / status / environmentRequired · execution contextReport duration, success or error, and environment. Add error_type on failures. Partial, refunded and corrected are not supported usage-event statuses.
usage_details / provider_reported_cost_usdWhen applicable · additional costSupply supported priced quantities or reported per-event cost for tools, retrieval and compute. Arbitrary quantities are stored but do not automatically gain a rate. Pricing version and calculated cost are assigned by Ganivra.
parent_step_id / attributes.user_id / attributes.agent_nameOptional · investigationLink child calls to the triggering step; identify the acting user separately from the customer. Send attributes.agent_name on each model or tool step to enable agent cost breakdowns and filters. Agent costs do not allocate execution revenue. Do not send prompts, responses or secrets.

Start with your use case

Connect a business action to its costs

Choose an example, replace the values, and attach it to your model or MCP events.

Outbound calling

Connect the model and MCP steps for one outbound call using the same execution_id. Track call direction and the agent without sending phone numbers or conversation content.

Customer
Your paying tenant or account
Workflow
outbound_call
Plan to configure
growth

Create this plan key in the receiving workspace, or use your existing key. A customer-plan assignment also works.

Configure customer pricing →
Event attributes · JSON

Partial payload — merge into a complete model or MCP event.

{
  "attributes": {
    "customer_id": "tenant_7f3c",
    "application": "voice-platform",
    "feature": "lead_qualification",
    "workflow": "outbound_call",
    "plan_key": "growth",
    "agent_name": "qualification_agent",
    "call_direction": "outbound"
  }
}
Implementation notes for outbound calling

For inbound calls, use workflow=inbound_call and call_direction=inbound. Call minutes and telephony costs are not inferred from these labels; instrument the relevant usage and costs separately.

Keep one execution ID for the business action and attribution consistent on every step. Explicit workflow names should take precedence over middleware route defaults.

Beyond model and tool costs

Send delivery costs through the API

Record telephony, speech, OCR, search, retrieval, compute, storage, network, external tools, and other service costs. Use the same customer and execution IDs as your model events.

POST /v1/delivery-costs/events on your Ganivra API base URL. Send Authorization: Bearer YOUR_WORKSPACE_API_KEY and Content-Type: application/json. The key selects the receiving workspace. Batch up to 500 events.

Complete request body · outbound call cost
{
  "events": [
    {
      "event_id": "telephony-call-123",
      "timestamp": "2026-10-02T12:00:00Z",
      "provider": "telephony-provider",
      "category": "telephony",
      "customer_id": "customer-123",
      "workflow": "outbound_call",
      "agent_name": "qualification_agent",
      "execution_id": "exec-123",
      "quantity": "4.5",
      "unit": "minute",
      "cost_usd": "0.09",
      "source_reference": "provider-call-123"
    }
  ]
}

Replace the example timestamp with the actual occurrence time. Unlike model-event attributes, delivery-event attribution fields are at the top level of each event.

Actual costs or quantity-based estimates

Send cost_usd for a reported USD amount. Alternatively, omit it and send quantity, unit, unit_rate_usd, and pricing_version. Ganivra multiplies quantity by your supplied rate and labels the result estimated. Rates are not looked up automatically. A customer plan key controls revenue, not the provider cost rate.

Retries, corrections, and duplicate charges

A successful ingestion response reports accepted and duplicate counts. Retry the identical event ID and payload, including timestamp, after an uncertain response. Changed content under the same ID returns 409. Persist unsent events in your application; the SDK is not a durable queue.

For corrections, send a new event ID, correction_of, a reason, and the signed difference in cost_usd. Changing $0.09 to $0.07 means a −$0.02 correction. Keep attribution, category, and provider consistent with the original. Do not send the same charge through both this API and model/MCP telemetry.

Python SDK and reporting scope

The SDK exposes ganivra.delivery_costs.create(...) and create_batch([...]) with the same event fields. These calls raise GanivraAPIError if acceptance cannot be confirmed. Use Ganivra Python SDK 0.6.3 as documented above.

Open Delivery costs for combined reporting, invoice matching, allocation and saved statements. Existing model dashboards and budgets retain their AI-telemetry scope. Delivery events do not increment the model/MCP call allowance. Agent views show costs without duplicating execution revenue.

Confirm your first execution

Send one real action, then check these four things before rolling out.

  1. AttributionFind the intended workspace, customer, feature and workflow.
  2. CostCheck actual model/tool quantities and priced costs. A plan key does not price an unknown model.
  3. RevenueCheck the plan or assignment, effective dates and required units. Report confirmed payments separately when needed.
  4. Safe retriesRedeliver the same event ID. The call count should stay unchanged.
Typical pathOne workflowStart narrow and expand after validation
Customer setupAutomaticNo Ganivra customer registration required
Prompt contentNever requiredSend IDs and versions, not content
Pricing telemetryPlan-specificCredits send units; other plans use captured trace facts

Why MCP visibility matters

A tool can cost almost nothing and still trigger an expensive execution.

Agents do not stop spending when tools/call returns. They may reason over the result, fetch more context, validate an action, retry, or call another paid service. Provider dashboards show the model spend; tool logs show the invocation. Ganivra joins the model and tool steps you instrument under the same execution.

  1. 01
    Model decidesPlanning cost
  2. 02
    MCP tool runsDirect tool or paid API cost
  3. 03
    Model continuesResult processing and generation
  4. 04
    Agent retriesHidden fan-out and margin risk

SDK & REST API

Integration instructions.

Return here whenever you need to connect another service, review the event contract, or update your instrumentation.

Captured automatically

Technical cost signals

Model and provider · input, output, and cached tokens · latency · status and errors · execution IDs

You provide as metadata

Business context

Your product or site · current tenant/customer ID · workflow · feature · prompt version · optional revenue

Dynamic B2B attribution

No customer registration required.

Read the signed-in tenant or account ID from your own request context and send it as customer_id. Every new value appears automatically in customer economics and cohorts.

Your Ganivra workspace
Your company
application
Product, site, or domain
customer_id
Current B2B tenant/account
plan_key
Current configured pricing plan
Granular unit economics

Configure price once. Send usage facts per execution.

Send the stable plan_key configured in Ganivra. Your application never sends the plan's rate or calculates revenue during a model call; manual customer assignment remains an optional fallback.

Pricing modelConfigure in GanivraSend with each executionGanivra calculates
Monthly subscriptionMonthly amount + plan keycustomer_id + plan_key; add feature for feature marginMonthly revenue allocated across that customer's executions
Per executionRevenue rate + plan keycustomer_id + plan_key; recommended featureExecution rate minus the execution's AI cost
Token basedRevenue rate per 1M tokens + plan keycustomer_id + plan_key; SDK captures tokens automaticallyCaptured tokens × the plan rate
Credits or unitsRevenue per unit + plan key + exact usage attributecustomer_id, plan_key, and attributes.creditsCredits consumed × the plan's unit rate
HybridMonthly amount plus execution and/or token ratescustomer_id; SDK captures executions and tokensAllocated base revenue plus captured usage revenue
Credit examplecustomer_id="account_northstar" · plan_key="growth" · credits=50

Your customer pays $100 for 500 credits, then spends 50 credits on one action. Record the purchase with the Transactions API as credit_purchase with units_purchased: 500, and send credits=50 on the action. Ganivra earns 50 × ($100 ÷ 500) = $10 on that action and subtracts its AI cost to give its margin. Unused credits count in the month they are spent.

  • Send the credits actually deducted by your system—not their monetary value. Use the exact attribute name configured on the plan.
  • Treat one billable customer action as one execution_id. If it makes several model calls, send the same credit total on every step; Ganivra counts it once. Do not split it across steps, because Ganivra uses the largest value, not the sum.
  • Subscriptions that include credits: record each payment as subscription_payment with the credits it includes in units_purchased. Each payment covers one month from its payment date, such as October 15 to November 14, and is shared equally across the customer's actions in that period. Included credits are used first and add no revenue; extra credits come from packs, oldest first.
  • If you do not record purchases, Ganivra prices credits at the plan's revenue per credit. It does the same for credits beyond what the customer bought.
  • Users who have not bought credits, such as guests or free users, should not send a credit plan_key. Their calls then show as cost with no revenue instead of a missing-plan issue.
01 · Install and initializepython -m pip install "ganivra>=0.6.3,<0.7"import ganivra
ganivra.init(api_key="your_key")

This alone captures cost telemetry. No prompt or response content is collected. Source, examples and changelog are on GitHub.

02 · Attribute the current customer dynamicallydef run_ai(account, credits_debited):
  with ganivra.trace(
    application="customer-operations-platform",
    customer_id=account.id,
    plan_key=account.plan_key,
    workflow="inbound_support_call",
    feature="voice_agent",
    metadata={"credits": credits_debited},
  ):
    client.responses.create(...)

Use a stable pseudonymous tenant ID—not a person’s name. For a credit plan, pass the units your system already deducted; do not send their price. Omit credits for subscription, execution, token, and hybrid plans because Ganivra derives their usage from the trace. Metadata accepts additional bounded JSON dimensions.

03 · Observe MCP tools through the same SDK queuewith ganivra.mcp_call(
  server="crm-server",
  tool="search_accounts",
  transport="streamable_http",
  auth_type="oauth",
  metadata={"result_count": 3, "cache_hit": False},
  provider_reported_cost_usd=0.002,
):
  result = mcp_client.call_tool(...)

This guide targets Ganivra Python SDK 0.6.3. The helper measures latency and outcome, inherits the active trace, and never receives arguments or results. For Node.js and other runtimes, send the equivalent MCP event to the same REST endpoint.

Usage and revenue · two different inputs

Which API does your app need?

Start with usage events. Add confirmed payment data when you want reported transactions alongside calculated customer economics. Both ingestion endpoints use the same server-side Ganivra workspace API key.

Events API: what your AI did

POST /v1/events

Send model or MCP usage after the call. Include stable event, execution, and step IDs plus the usage facts needed to calculate cost.

Add attributes.customer_id and a configured attributes.plan_key for plan-rated revenue. This is your customer's pricing plan, not your Ganivra subscription.

SDK and event examples →

Transactions API: what the customer paid

POST /v1/economics/transactions

Send a confirmed purchase, subscription payment, credit purchase, or refund from your backend or verified payment webhook. This reports a payment; it does not charge the customer.

Use the same customer_id. Include execution_id when the payment belongs to one particular result. Omit it for a general subscription or credit pack.

Payment integration example →

Why revenue can exist with zero transactions

Customer pricing plans and event-supplied revenue can populate the economics overview without creating a revenue ledger record. The Reported transactions table shows ledger records from the Transactions API, revenue imports, or manual revenue entry.

An empty table means no matching records in the selected period and customer filter. It does not mean your integration failed or your customers generated no revenue.

Keep the two flows connected

  1. Send usage with your own stable customer and execution IDs.
  2. Confirm payment in your system before reporting it.
  3. Send the same customer ID and, for a paid result, the original execution ID.
  4. Check Reported transactions, then inspect the linked execution.

Without an execution link, do not expect a subscription or credit-pack purchase to appear as revenue on one specific execution.

Retries, refunds, imports, and expected responses

Retry safely

Events return HTTP 202 with accepted and duplicate counts. Transactions return HTTP 201 with an ID and created: true; repeating the same external_id returns created: false. Reuse the ID for retries of the same transaction, not for different purchases. An existing record is not updated by resending its ID.

A 401 means the key is invalid or missing. A 422 means the payload failed validation. Retry temporary failures with backoff.

Report refunds separately

Use a new external ID, transaction_type: "refund", and a positive refund amount. Ganivra records it with refunded status and treats it as negative revenue in economics. Link the same customer and execution where applicable.

Import billing records

Use POST /v1/economics/revenue for one revenue entry, or POST /v1/economics/revenue/import with an entries array for a batch. Profile & setup also supports manual entry. These write to the same ledger.

Reuse the same external ID across ingestion paths for the same financial record. Do not send one payment with different IDs through multiple endpoints.

Understand revenue precedence

Recognized or refunded ledger records linked to an execution take precedence over its event-supplied or plan-rated revenue. Customer-month billing actuals can also replace estimated plan allocation. Import complete, correctly linked records and check the resulting coverage.

For non-USD transactions, optionally provide one reporting value to normalize: amount_usd or exchange_rate_to_usd. Amounts use major currency units. See currency options and examples →

API 02 · Optional · Confirmed purchases

Keep actual payment data current when you need it.

You do not need both APIs to start. API 01 already provides recorded AI cost and plan-rated execution contribution from customer_id, plan_key, and usage. Add this purchase API when you also want actual credit purchases, subscription payments, renewals, discounts, and refunds reflected in Ganivra.

  1. 1
    Verify paymentValidate the Stripe signature and handle a successful webhook such as payment_intent.succeeded.
  2. 2
    Fulfil the purchaseUpdate credits, subscription access, or the purchased result in your own database. Ganivra never changes entitlements.
  3. 3
    Post to GanivraSend the confirmed transaction with your server-side Ganivra workspace key.

Stripe example

Example: your customer buys 1,000 credits for $49.

Use Stripe's PaymentIntent ID as external_id. It stays stable when Stripe retries the webhook, so Ganivra records the purchase once.

Ganivra earns a credit pack as its credits are used, at the price paid: $49 ÷ 1,000 = $0.049 per credit. An action that sends attributes.credits: 50 earns $2.45 against its own AI cost. Credits not yet used are not revenue yet; they count in the month they are spent. Omit execution_id for a pack, because a purchase linked to one execution counts in full on that execution.

external_id
Payment provider's unique transaction ID
customer_id
Your internal customer or tenant ID
plan_key
Plan key configured in Ganivra
amount
Major currency units—Stripe's cents divided by 100
units_purchased
Credits granted by your application
POSThttps://api.ganivra.com/v1/economics/transactions
Authorization: Bearer gv_live_...
Content-Type: application/json

{
  "external_id": "pi_3QxExample",
  "customer_id": "customer_123",
  "plan_key": "growth",
  "transaction_type": "credit_purchase",
  "amount": 49,
  "currency": "USD",
  "units_purchased": 1000,
  "source": "stripe",
  "occurred_at": "2026-08-12T16:30:00Z"
}

Subscription payments

Example: a customer pays $99 a month for a plan that includes 2,500 credits.

After confirming payment and updating subscription access in your system, send subscription_payment. Send each successful renewal separately, using that payment or paid invoice's unique ID as external_id. Do not use the subscription ID: it stays the same across renewals.

Use the same customer_id as your usage events. The optional plan_key identifies a plan configured in Ganivra. Omit execution_id for a general subscription; the payment does not belong to one AI execution.

occurred_at is the actual payment timestamp, not the time you retry or import it. Each payment covers one month from its payment date, such as October 15 to November 14, or until the next subscription payment if the customer changes plan sooner. It is shared equally across the customer's executions in that period, so every action shows a margin.

If the plan includes credits each month, send them as units_purchased, such as 2,500. Actions use the included credits first; they add no revenue because the subscription already paid for them. Credits beyond the allowance come from credit packs, oldest first, at each pack's price. Features that use no credits still receive their share of the subscription.

POSThttps://api.ganivra.com/v1/economics/transactions
Authorization: Bearer gv_live_...
Content-Type: application/json

{
  "external_id": "subscription_payment_2026_10_123",
  "customer_id": "customer_123",
  "plan_key": "growth",
  "transaction_type": "subscription_payment",
  "amount": 99,
  "currency": "USD",
  "units_purchased": 2500,
  "source": "my_billing_app",
  "occurred_at": "2026-10-01T10:00:00Z"
}

One-time purchases

Example: a customer pays $19 for an AI report.

After confirming payment and fulfilling the purchase, send one_time_purchase with the payment's unique ID. Include the original execution_id from the AI report so Ganivra can connect its revenue and recorded costs. Omit it for purchases unrelated to a specific execution.

No units_purchased or plan_key is required for this example: the confirmed amount supplies the revenue. Keep the customer ID consistent with the original usage.

POSThttps://api.ganivra.com/v1/economics/transactions
Authorization: Bearer gv_live_...
Content-Type: application/json

{
  "external_id": "report_payment_456",
  "customer_id": "customer_123",
  "execution_id": "exec_report_456",
  "transaction_type": "one_time_purchase",
  "amount": 19,
  "currency": "USD",
  "source": "my_billing_app",
  "occurred_at": "2026-10-02T14:30:00Z"
}

Replace sample IDs, amounts, timestamps, and the server-side workspace key with your own values. For either example, check Reported transactions with the matching date and customer filters. A successful request returns HTTP 201 with created: true; retry the same payment with the same external_id to receive created: false. Keep failed submissions in your backend retry queue and do not report the same payment again through a revenue import with a different ID.

Payment currency

Choose how revenue and costs are compared.

Send amounts in major currency units: 299 with INR means ₹299, not 299 paise. These rules apply to the Transactions API for purchases, subscriptions, credits, and refunds.

1. Omit currency: default to USD

If you send amount: 299 without currency, Ganivra records $299. You can also send currency: "USD" explicitly. Revenue and provider costs are then in USD; omit both conversion fields.

2. Supply currency and an exchange rate

To compare revenue and costs in USD, send your original currency plus exchange_rate_to_usd. The rate means USD per one unit of the supplied currency: Ganivra multiplies the amount by this rate.

For example, ₹299 × 0.0117647059 = $3.51764706. This is an illustrative rate; use your payment or accounting rate. Alternatively, send the already converted total as amount_usd. Send only one conversion field, never both. Ganivra does not fetch exchange rates automatically.

3. Supply currency without conversion

Ganivra accepts the payment and displays its supplied currency. For contribution and margin calculations, it uses the raw revenue and cost amounts as if both shared the same currency. This assumption does not convert costs: provider costs remain recorded and labeled in USD.

For example, £299 revenue and $100 cost produces 199 unnormalized contribution and 66.56% margin. An alert explains the assumption and asks you to supply an exchange rate or USD amount for normalized comparisons. Mixed-currency totals are labeled as mixed currencies.

Example: report a ₹299 credit purchase with conversion.

Send this JSON to the Transactions API with your server-side workspace bearer key. To keep ₹299 without normalization, omit exchange_rate_to_usd and retain currency: "INR". If you omit both currency and conversion, the amount means $299.

Without conversion, the response includes amount_usd: null, currency_normalized: false, and a warning. With conversion, it returns the calculated USD amount. The original amount and currency remain available in either case.

POSThttps://api.ganivra.com/v1/economics/transactions
{
  "external_id": "razorpay_payment_123",
  "customer_id": "customer_123",
  "plan_key": "starter",
  "transaction_type": "credit_purchase",
  "amount": 299,
  "currency": "INR",
  "exchange_rate_to_usd": 0.0117647059,
  "units_purchased": 50,
  "source": "razorpay"
}

Decide the conversion basis before submission. Retries must keep the same payload and external ID. Adding an exchange rate to an already recorded payment returns HTTP 409; submitting the same payment under a new ID would duplicate revenue.

Best practices when using the currency fallback

  1. Send the actual currency explicitly. Use INR, GBP, or the payment's other three-letter currency code. Omit currency only when the amount is already USD. Never omit it merely because an exchange rate is unavailable.
  2. Use fallback to capture payments while FX is unavailable. It preserves the payment amount and lets you inspect usage alongside revenue. A successful request confirms acceptance, not a valid cross-currency profit comparison. Treat unnormalized contribution and margin as provisional; normalize before using them to assess profitability or change pricing.
  3. Normalize before submission when possible. Supply the payment or accounting conversion as either exchange_rate_to_usd or amount_usd. Do not send both, guess a rate, or send 1 just to remove the warning. If normalization is required and FX is temporarily unavailable, retain the payment in your own durable queue until the conversion is available.
  4. Keep units and rate direction consistent. Convert provider minor units to major units using that currency's denomination. Send USD per one native unit, not native units per dollar. The amount is the total purchase value; units_purchased records the credits granted separately. Keep full conversion precision until display.
  5. Check normalization before comparing totals. Inspect currency_normalized, the response warning, and report currency_basis. Do not read mixed-currency totals as USD or compare raw margins across customers or periods with different currencies or normalization states. Costs remain USD even when the calculation assumes a shared currency.
  6. Preserve the original submission on retries. Store the external ID, amount, currency, conversion value, and payment timestamp together. Retry unchanged. An existing payment cannot be updated by adding FX; HTTP 409 signals conflicting content. Do not submit another purchase ID to normalize the same payment, because that duplicates revenue. This API does not currently provide an FX correction endpoint.
  7. Report real refunds as separate transactions. Use a new refund ID, transaction_type: "refund", a positive refund amount, and the actual refund currency. Preserve the customer and relevant execution link. Use a consistent conversion policy backed by your payment or accounting records; a normalized purchase paired with an unconverted refund makes the combined comparison unnormalized.
  8. Verify the three paths before going live. Check that omitted currency produces USD, supplied currency without FX preserves the native amount and shows a warning, and supplied FX produces the expected USD total. Also retry a payment and report a partial refund to verify that revenue is counted once and reduced correctly.
Link a paid result

Include execution_id only when relevant.

For a paid resume, image, or report, send the original execution ID. Ganivra assigns the purchase revenue to that execution and recalculates contribution after recorded costs. Omit it for a general credit pack or subscription purchase.

Multiple currencies

Keep native currency or normalize to USD.

Currency defaults to USD when omitted. INR, GBP, EUR, and other currencies are accepted without conversion; the supplied currency is displayed. Costs stay in USD, and contribution and margin compare the raw numbers as if they shared a currency, with a warning. For example, £299 revenue and $100 cost gives 199 unnormalized contribution and 66.56% margin. Supply either amount_usd or exchange_rate_to_usd to normalize; never send both.

Plan changes

History remains effective-dated.

Send the new plan_key on future executions. Earlier events retain their supplied plan key. Configure effective dates carefully: a stored key is not an immutable snapshot of the plan rate. Refunds are appended as new transactions.

What your team owns

A small, explicit contract.

Ganivra needs operational facts from the call and stable business identifiers from your application. Everything else can be added incrementally.

Required for cost

Execution telemetry

Model calls send provider, model, operation, and tokens. MCP calls send event_type, server, and tool. Both send latency, status, and stable event/execution/step IDs.

Required for granular economics

Business attribution

application, dynamic customer_id, workflow, and feature connect spend and plan-derived revenue to the exact product action that caused them.

Depends on pricing model

Revenue usage facts

Subscription, execution, token, and hybrid plans use captured trace facts. Credit/unit plans additionally require the units actually deducted, such as attributes.credits. Ganivra applies the assigned rate.

Required for MCP privacy

Metadata, never tool content

Send server, tool, transport, auth category, counts, categories, and flags. Never send arguments, results, scopes, credentials, workspace IDs, prompts, or responses.

Agent retries

Same task. Another attempt.

Keep the same execution ID, workflow, and customer when an agent retries. Send a new event and step ID for each actual call, and add retry metadata to its attributes. Ganivra observes retries you report; internal provider SDK retries may not be separately visible.

REST API: report each actual call

POST to /v1/events with Authorization: Bearer YOUR_API_KEY and Content-Type: application/json. Replace the sample IDs and usage with values from your application.

{
  "event_id": "evt_document_123_attempt_2",
  "execution_id": "exec_document_123",
  "step_id": "step_document_123_attempt_2",
  "provider": "openai",
  "model": "gpt-5-mini",
  "operation": "responses.create",
  "input_tokens": 1000,
  "output_tokens": 200,
  "total_tokens": 1200,
  "latency_ms": 850,
  "status": "success",
  "environment": "production",
  "attributes": {
    "workflow": "document_extraction",
    "customer_id": "customer_123",
    "agent_name": "validation_agent",
    "is_retry": true,
    "attempt": 2,
    "retry_reason": "validation_failed"
  }
}

Python SDK: attach metadata to your existing retry loop

After SDK initialization, wrap each attempt as shown below. Your application supplies task_id, customer_id, and attempt (starting at 1). Reuse the task ID across attempts. The SDK creates event and step IDs for supported calls.

with ganivra.trace(
    execution_id=task_id,
    workflow="document_extraction",
    customer_id=customer_id,
    metadata={
        "agent_name": "validation_agent",
        "is_retry": attempt > 1,
        "attempt": attempt,
        "retry_reason": "validation_failed" if attempt > 1 else None,
    },
):
    response = client.responses.create(
        model="gpt-5-mini",
        input=agent_input,
    )