Margin Guard Quickstart
Send AI usage from your backend, connect Stripe revenue, and Margin Guard shows your gross margin per customer and plan. This page documents the one ingestion endpoint that exists today: one authenticated HTTPS call. Official Node.js and Python SDKs wrap it (see SDKs).
Prerequisites
- Sign in at
/app/en/sign-inwith your e-mail (a one-time code; no password) and create your workspace. - An active subscription, or an active free evaluation where it is offered (see limits).
- Open Margin Guard → Connections and create an ingestion key for the provider you use (OpenAI, Anthropic or Google Gemini). The key (
mgik_…) is shown once. There is one key per provider and workspace; creating it again replaces the old key immediately. - Optional, for revenue: connect Stripe on the same page. Margin Guard requests Stripe’s standard Connect permission (scope read_write; Stripe offers a read-only scope only to Stripe Apps), but its code only ever reads revenue and never creates charges, transfers, payouts or refunds.
Margin Guard never asks for your OpenAI, Anthropic or Google (Gemini) API key and never calls those providers for you. The ingestion key is PrinVox’s own credential for receiving usage events; it is not a provider credential. Your backend calls the provider, then reports the usage it received.
Official SDKs: Node.js and Python
Both SDKs are thin wrappers over the HTTP API below, which you can also call directly. They are MIT licensed and published as @prinvox/sdk on npm and prinvox on PyPI. Both send only usage metadata with your PrinVox ingestion key (one key per provider), never need your OpenAI, Anthropic or Google API key, contain no prices, and use an eventId / event_id as the idempotency key.
Node.js / TypeScript
@prinvox/sdk v0.1.0 is the recommended way to integrate on Node.js 18+ (ESM only — from CommonJS use import() — with TypeScript types included and no runtime dependencies).
npm install @prinvox/sdkOpenAI
import { PrinVox } from "@prinvox/sdk";
// One ingestion key per provider: this key was created for OpenAI (create one each for Anthropic and Gemini).
const prinvox = new PrinVox({ apiKey: process.env.PRINVOX_INGESTION_KEY_OPENAI! });
// After your normal OpenAI call, report the usage it returned. Telemetry must never break your product:
void prinvox
.track({
provider: "openai", // must equal the provider this key was created for
model: "gpt-4.1",
eventId: response.id, // idempotency key (sent as provider_request_id)
featureKey: "assistant",
customerRef: "user_8421", // your own id for the end customer
inputTokens: response.usage?.input_tokens,
outputTokens: response.usage?.output_tokens,
})
.catch(() => {});Anthropic
import Anthropic from "@anthropic-ai/sdk";
import { PrinVox } from "@prinvox/sdk";
const anthropic = new Anthropic(); // your own key; PrinVox never sees it
const prinvox = new PrinVox({ apiKey: process.env.PRINVOX_INGESTION_KEY_ANTHROPIC! }); // the key created for Anthropic
const message = await anthropic.messages.create({ model: "claude-haiku-4-5", max_tokens: 512, messages: [{ role: "user", content: question }] });
void prinvox
.track({
provider: "anthropic",
model: "claude-haiku-4-5",
eventId: message.id,
featureKey: "support-answer",
customerRef: appCustomerId,
inputTokens: message.usage.input_tokens,
outputTokens: message.usage.output_tokens,
})
.catch(() => {});Google Gemini
import { GoogleGenAI } from "@google/genai";
import { randomUUID } from "node:crypto";
import { PrinVox } from "@prinvox/sdk";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); // your own key; PrinVox never sees it
const prinvox = new PrinVox({ apiKey: process.env.PRINVOX_INGESTION_KEY_GEMINI! }); // the key created for Google Gemini
const response = await ai.models.generateContent({ model: "gemini-2.5-flash", contents: prompt });
const u = response.usageMetadata;
const audio = (u?.promptTokensDetails ?? []).filter((d) => d.modality === "AUDIO").reduce((n, d) => n + (d.tokenCount ?? 0), 0);
void prinvox
.track({
provider: "google_gemini",
model: "gemini-2.5-flash",
eventId: response.responseId ?? randomUUID(), // prefer the provider id: a random one cannot dedupe a re-run
featureKey: "ai-chat",
customerRef: appCustomerId,
inputTokens: u?.promptTokenCount, // includes the audio tokens; inputAudioTokens is the audio SUBSET of it
inputAudioTokens: audio, // required (0 if none) for the 2.5 Flash family, else the cost is UNKNOWN
outputTokens: (u?.candidatesTokenCount ?? 0) + (u?.thoughtsTokenCount ?? 0), // thinking tokens are billed as output
})
.catch(() => {});Next.js (server-side only)
PRINVOX_INGESTION_KEY is a secret: use it only in server code (Route Handlers, Server Actions, server components, Node.js backends; the after() helper in the example needs Next.js 15.1+, on older versions use void prinvox.track(…).catch(…)). Never in a Client Component, never in a NEXT_PUBLIC_ variable, never in browser JavaScript — the SDK refuses to start in a browser.
// app/api/chat/route.ts — a Route Handler runs on the server only.
// .env.local (never NEXT_PUBLIC_): PRINVOX_INGESTION_KEY_OPENAI=mgik_…
import { after } from "next/server"; // Next.js 15.1+
import OpenAI from "openai";
import { PrinVox } from "@prinvox/sdk";
const openai = new OpenAI(); // your own key; PrinVox never sees it
const prinvox = new PrinVox({ apiKey: process.env.PRINVOX_INGESTION_KEY_OPENAI! });
export async function POST(req: Request) {
const { prompt, customerId } = await req.json();
const response = await openai.responses.create({ model: "gpt-4.1-mini", input: prompt });
// after() keeps the report running once the response is sent (serverless-safe) and can never fail the request.
after(() =>
prinvox
.track({
provider: "openai",
model: "gpt-4.1-mini",
eventId: response.id,
featureKey: "chat",
customerRef: customerId,
inputTokens: response.usage?.input_tokens,
outputTokens: response.usage?.output_tokens,
})
.catch(() => {}),
);
return Response.json({ text: response.output_text });
}- It sends only usage metadata with your PrinVox ingestion key. It never needs your OpenAI, Anthropic or Google API key, and it contains no prices.
eventIdis the idempotency key. The SDK retries only network errors, timeouts and 5xx, always with the identical event, so a repeat can never double count; it never retries 4xx or 429.- Run it on your server only — it refuses to start in a browser, because the ingestion key is a secret.
- For Gemini use the provider
google_gemini, add thinking tokens tooutputTokens, and sendinputAudioTokens(0if none) for the 2.5 Flash family. - To test against a Preview environment, pass that environment’s URL as
baseUrltogether with its own ingestion key.
The package README (shipped with it) covers every field, the error codes and a Next.js example.
Python
prinvox v0.1.0 is the Python SDK (Python 3.10+, one dependency: httpx, synchronous API).track takes keyword arguments and returns processed or duplicate, or raises a PrinVoxError.
pip install prinvoxOpenAI
import os
from prinvox import PrinVox
# One ingestion key per provider: this one was created for OpenAI (create one each for Anthropic and Gemini).
prinvox = PrinVox(api_key=os.environ["PRINVOX_INGESTION_KEY_OPENAI"])
# After your normal OpenAI call, report the usage it returned. Telemetry must never break your product:
try:
prinvox.track(
provider="openai", # must equal the provider this key was created for
model="gpt-4.1",
event_id=response.id, # idempotency key (sent as provider_request_id)
feature_key="assistant",
customer_ref="user_8421", # your own id for the end customer
input_tokens=response.usage.input_tokens if response.usage else None,
output_tokens=response.usage.output_tokens if response.usage else None,
)
except Exception:
passAnthropic
import os
import anthropic
from prinvox import PrinVox
claude = anthropic.Anthropic() # your own key; PrinVox never sees it
prinvox = PrinVox(api_key=os.environ["PRINVOX_INGESTION_KEY_ANTHROPIC"]) # the key created for Anthropic
message = claude.messages.create(model="claude-haiku-4-5", max_tokens=512, messages=[{"role": "user", "content": question}])
try:
prinvox.track(
provider="anthropic",
model="claude-haiku-4-5",
event_id=message.id,
feature_key="support-answer",
customer_ref=app_customer_id,
input_tokens=message.usage.input_tokens,
output_tokens=message.usage.output_tokens,
)
except Exception:
passGoogle Gemini
import os
import uuid
from google import genai
from prinvox import PrinVox
gemini = genai.Client() # your own key; PrinVox never sees it
prinvox = PrinVox(api_key=os.environ["PRINVOX_INGESTION_KEY_GEMINI"]) # the key created for Google Gemini
response = gemini.models.generate_content(model="gemini-2.5-flash", contents=prompt)
try:
u = response.usage_metadata
if u is not None and u.prompt_token_count is not None: # no usage reported -> report nothing: the cost stays unknown, never zero
audio = sum((d.token_count or 0) for d in (u.prompt_tokens_details or []) if str(d.modality).endswith("AUDIO"))
prinvox.track(
provider="google_gemini",
model="gemini-2.5-flash",
event_id=response.response_id or str(uuid.uuid4()), # prefer the provider id: a random one cannot dedupe a re-run
feature_key="ai-chat",
customer_ref=app_customer_id,
input_tokens=u.prompt_token_count, # includes the audio tokens; input_audio_tokens is the audio SUBSET of it
input_audio_tokens=audio, # required (0 if none) for the 2.5 Flash family, else the cost is UNKNOWN
output_tokens=(u.candidates_token_count or 0) + (u.thoughts_token_count or 0), # thinking tokens are billed as output
)
except Exception:
passFastAPI (server-side only)
PRINVOX_INGESTION_KEY is a secret: read it from the server’s environment only, never in browser code or a frontend bundle. track is synchronous, so run it as a background task as below (a worker thread, after the response), not on the event loop.
# main.py — server-side only. The key comes from the server's environment, never from a client.
import os
from fastapi import BackgroundTasks, FastAPI
from openai import OpenAI
from prinvox import PrinVox
app = FastAPI()
openai_client = OpenAI() # your own key; PrinVox never sees it
prinvox = PrinVox(api_key=os.environ["PRINVOX_INGESTION_KEY_OPENAI"])
def report(response_id: str, customer_id: str, usage) -> None:
try:
prinvox.track(
provider="openai", model="gpt-4.1-mini", event_id=response_id, feature_key="chat", customer_ref=customer_id,
input_tokens=usage.input_tokens, output_tokens=usage.output_tokens,
)
except Exception:
pass # never fail the user's request over telemetry
@app.post("/chat")
def chat(prompt: str, customer_id: str, background: BackgroundTasks):
response = openai_client.responses.create(model="gpt-4.1-mini", input=prompt)
background.add_task(report, response.id, customer_id, response.usage) # runs after the response, in a worker thread
return {"text": response.output_text}- Retries only network errors, timeouts and 500/502/503/504, always with the identical event; never 4xx or 429 (
PrinVoxRateLimitError.retry_aftertells you when to resend, with the sameevent_id). - For Gemini use
google_gemini, add thinking tokens tooutput_tokens, and sendinput_audio_tokens(0if none) for the 2.5 Flash family; otherwise the cost is stored as unknown. - To test against a Preview environment, pass its URL as
base_urltogether with its own ingestion key. The client’srepr()redacts the key.
Send your first event
POST https://www.prinvox.com/api/margin-guard/usage with Authorization: Bearer <ingestion key> and a JSON body of at most 8,192 bytes. Call it from your server, never from a browser: the key is a secret. In a preview environment use that environment’s own host.
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ) # events outside your report window are stored but not shown
curl -sS -X POST "$PRINVOX_URL/api/margin-guard/usage" \
-H "Authorization: Bearer $PRINVOX_INGESTION_KEY" \
-H "Content-Type: application/json" \
-d '{
"feature_key": "summarize",
"provider": "openai",
"model": "gpt-4.1-mini",
"provider_request_id": "resp_SAMPLE_0001",
"external_customer_id": "cust_demo_001",
"input_tokens": 812,
"output_tokens": 164,
"occurred_at": "'"$NOW"'"
}'
# -> HTTP 201 {"status":"processed"} (send it again: HTTP 200 {"status":"duplicate"})Sample identifiers are marked. Events are append-only and cannot be deleted through the API, so try the examples in a test workspace. A sample event is a real stored event; it is not a real margin result (see below).
Node.js (bounded, non-blocking helper)
// prinvox.mjs — Node 18+ (global fetch). Telemetry must never break your product:
// every failure is swallowed, the wait is bounded, and nothing here throws.
const URL_ = process.env.PRINVOX_URL ?? "https://www.prinvox.com";
const KEY = process.env.PRINVOX_INGESTION_KEY;
export async function reportUsage(event) {
if (!KEY) return; // not configured: skip silently, your request already succeeded
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch(`${URL_}/api/margin-guard/usage`, {
method: "POST",
headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
body: JSON.stringify(event),
signal: AbortSignal.timeout(2000), // never wait longer than 2 s
});
// 201 processed, 200 duplicate: done. Any other 4xx will not succeed on a quick retry — including
// 429, whose Retry-After (60 s) is far longer than a telemetry call may wait: drop the event.
if (res.status < 500) return;
} catch {
/* network error or timeout: fall through and retry */
}
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt)); // 250 ms, 500 ms
}
// Give up quietly. Losing one event leaves a visible gap; it never fails the customer's request.
}Python (standard library)
# Python 3.9+, standard library only.
import json, os, threading, time, urllib.request, urllib.error
PRINVOX_URL = os.environ.get("PRINVOX_URL", "https://www.prinvox.com")
PRINVOX_KEY = os.environ.get("PRINVOX_INGESTION_KEY")
def _send(event: dict) -> None:
if not PRINVOX_KEY:
return # not configured: skip silently
body = json.dumps(event).encode()
for attempt in range(3):
req = urllib.request.Request(
f"{PRINVOX_URL}/api/margin-guard/usage",
data=body,
method="POST",
headers={"Authorization": f"Bearer {PRINVOX_KEY}", "Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=2): # bounded wait
return # 201 processed or 200 duplicate
except urllib.error.HTTPError as e:
if e.code < 500:
return # a 4xx (including 429, whose Retry-After is 60 s) will not succeed on a quick retry
except Exception:
pass # network error or timeout: retry
time.sleep(0.25 * 2 ** attempt)
def report_usage(event: dict) -> None:
"""Fire and forget on a daemon thread: it can never block or fail the caller."""
threading.Thread(target=_send, args=(event,), daemon=True).start()
# After an OpenAI Responses call:
# report_usage({
# "feature_key": "summarize", "provider": "openai", "model": "gpt-4.1-mini",
# "provider_request_id": response.id, "external_customer_id": app_customer_id,
# "input_tokens": response.usage.input_tokens, "output_tokens": response.usage.output_tokens,
# "occurred_at": datetime.now(timezone.utc).isoformat(),
# })
#
# After a Google Gemini call (google-genai SDK), thinking tokens are billed as output, so add them:
# u = response.usage_metadata
# report_usage({
# "feature_key": "ai-chat", "provider": "google_gemini", "model": "gemini-2.5-flash",
# "provider_request_id": response.response_id or str(uuid.uuid4()), "external_customer_id": app_customer_id,
# "input_tokens": u.prompt_token_count,
# "input_audio_tokens": sum(d.token_count or 0 for d in (u.prompt_tokens_details or []) if str(d.modality).endswith("AUDIO")),
# "output_tokens": (u.candidates_token_count or 0) + (u.thoughts_token_count or 0),
# "occurred_at": datetime.now(timezone.utc).isoformat(),
# })Event fields
| Field | Required | Rules |
|---|---|---|
feature_key | yes | Your name for the AI feature, 1–100 characters. |
provider | yes | openai, anthropic or google_gemini, and it must equal the provider the key was created for (otherwise provider_mismatch). For Gemini the spellings gemini, google-ai and google_ai are accepted and stored as google_gemini. |
model | yes | 1–100 characters. Exact alias — see Models. |
provider_request_id | yes | 1–200 characters. Unique per provider request; it is the idempotency key. |
occurred_at | yes | ISO 8601 timestamp, e.g. 2026-10-17T09:30:00Z. |
external_customer_id | no | 1–200 characters; your own id for the end customer. Must not start with stripe:. |
stripe_customer_id | no | The cus_… id of that customer in your connected Stripe account. |
input_tokens, output_tokens | no* | Non-negative integers. *Without both, the event is stored but its cost is unknown. |
input_audio_tokens | no | Required for gemini-2.5-flash, gemini-2.5-flash-lite and gemini-3.1-flash-lite (send 0 if none): how many of input_tokens were audio (a subset, never extra). Without it these models’ cost is unknown, because Google bills audio at a higher rate. Ignored for every other model. More than input_tokens leaves the cost unknown. |
Reports show the trailing 30 days by default (and Basic and the trial reach back no further), so send a current occurred_at: older events are stored but do not appear. Cost is computed from input and output tokens at list price. Cached-input and batch discounts are not modelled, so the cost shown is an upper bound. Amounts are in USD; there is no currency conversion.
Workspace, customer and Stripe identity
Three different identities are involved. Keeping them apart is what makes the margin correct.
| Identity | What it is | How it is set |
|---|---|---|
| Workspace | Your Margin Guard account (the tenant). | Implied by the ingestion key. Never sent in the request and never selectable by it — a key can only write to its own workspace. |
| Application customer | Your end customer in your own system (a user or organisation id). | external_customer_id, e.g. cust_demo_001 (sample). |
| Stripe customer | The same customer in your connected Stripe account, whose payments are the revenue. | stripe_customer_id, e.g. cus_SampleDoNotUse123 (sample). |
- Only
stripe_customer_idjoins usage to Stripe revenue. Margin Guard never guesses a link from names, e-mails or id shapes. - An event with only
external_customer_idbecomes a separate customer in your own namespace, shown without revenue (unlinked). - If both are sent, the Stripe id is used for attribution. With neither, the event is Unattributed (shared or overhead cost).
- Past your plan’s tracked-customer limit (25 on Basic) the cost is still recorded, but as Unattributed.
{
"feature_key": "summarize",
"provider": "openai",
"model": "gpt-4.1-mini",
"provider_request_id": "resp_SAMPLE_0002",
"stripe_customer_id": "cus_SampleDoNotUse123",
"input_tokens": 500,
"output_tokens": 90,
"occurred_at": "<current UTC time, ISO 8601>"
}OpenAI, Anthropic and Google Gemini examples
These read the usage object of the completed, non-streaming response. Streaming is not covered: with streaming, the usage arrives in the final event/chunk and you must capture it there yourself; do not report an event before you have the counts. Pass the model alias you requested, not the longer dated name some responses echo back.
OpenAI (Responses API)
import OpenAI from "openai";
import { reportUsage } from "./prinvox.mjs";
const openai = new OpenAI(); // your own key; Margin Guard never sees it
const MODEL = "gpt-4.1-mini"; // send THIS alias, not response.model (that is a dated snapshot name)
export async function summarize(text, appCustomerId) {
const response = await openai.responses.create({ model: MODEL, input: `Summarize: ${text}` });
// Fire and forget: do not await, do not let it throw into your handler.
void reportUsage({
feature_key: "summarize",
provider: "openai",
model: MODEL,
provider_request_id: response.id,
external_customer_id: appCustomerId,
// Only send token counts you actually received. If usage is missing, omit them:
// the event is stored with an UNKNOWN cost instead of an invented zero.
...(response.usage && {
input_tokens: response.usage.input_tokens,
output_tokens: response.usage.output_tokens,
}),
occurred_at: new Date().toISOString(),
});
return response.output_text;
}Anthropic (Messages API)
import Anthropic from "@anthropic-ai/sdk";
import { reportUsage } from "./prinvox.mjs";
const anthropic = new Anthropic(); // your own key; Margin Guard never sees it
const MODEL = "claude-haiku-4-5";
export async function answer(question, appCustomerId) {
const message = await anthropic.messages.create({
model: MODEL,
max_tokens: 512,
messages: [{ role: "user", content: question }],
});
void reportUsage({
feature_key: "support-answer",
provider: "anthropic",
model: MODEL,
provider_request_id: message.id,
external_customer_id: appCustomerId,
...(message.usage && {
input_tokens: message.usage.input_tokens,
output_tokens: message.usage.output_tokens,
}),
occurred_at: new Date().toISOString(),
});
return message.content;
}Google Gemini (generateContent)
Use the provider value google_gemini. Report output_tokens as the candidate tokens plus the thinking tokens (Google bills thinking as output). input_tokens is the prompt token count. The same request works as plain HTTP from any language:
{
"feature_key": "ai-chat",
"provider": "google_gemini",
"model": "gemini-2.5-flash",
"provider_request_id": "gemini_SAMPLE_0001",
"external_customer_id": "cust_demo_001",
"input_tokens": 1200,
"input_audio_tokens": 0,
"output_tokens": 400,
"occurred_at": "<current UTC time, ISO 8601>"
}import { GoogleGenAI } from "@google/genai";
import { randomUUID } from "node:crypto";
import { reportUsage } from "./prinvox.mjs";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); // your own key; Margin Guard never sees it
const MODEL = "gemini-2.5-flash"; // send THIS alias, not a dated or "-latest" name
export async function chat(prompt, appCustomerId) {
const response = await ai.models.generateContent({ model: MODEL, contents: prompt });
const u = response.usageMetadata;
// The 2.5 Flash family bills audio input higher, so say how much of the prompt was audio (0 for text-only calls).
const audio = (u?.promptTokensDetails ?? []).filter((d) => d.modality === "AUDIO").reduce((n, d) => n + (d.tokenCount ?? 0), 0);
void reportUsage({
feature_key: "ai-chat",
provider: "google_gemini",
model: MODEL,
// Unique per request; reused on every retry of THIS event, which is what makes a retry safe.
provider_request_id: response.responseId ?? randomUUID(),
external_customer_id: appCustomerId,
// Only send counts you actually received. Thinking tokens are billed as OUTPUT, so add them.
...(u?.promptTokenCount != null && {
input_tokens: u.promptTokenCount,
input_audio_tokens: audio,
output_tokens: (u.candidatesTokenCount ?? 0) + (u.thoughtsTokenCount ?? 0),
}),
occurred_at: new Date().toISOString(),
});
return response.text;
}PrinVox prices exactly the token counts you send; if you use tools such as grounding, check which usage fields Google reports for them. Gemini events use the same endpoint, ingestion key mechanism, external_customer_id / stripe_customer_id attribution and provider_request_id idempotency as the other providers. PrinVox does not need your Google API key to receive them.
Reporting is fire-and-forget with a 2-second bound and up to three attempts on network errors and 5xx. If Margin Guard is unreachable, your customer’s AI request still succeeds; the lost event is simply missing from your totals — Margin Guard cannot detect it, and it is never replaced by a made-up zero. In serverless runtimes, use the platform’s “wait until” facility so the background call is not cut off when the response returns.
Responses, errors and retries
| HTTP | Body | Meaning / what to do |
|---|---|---|
| 201 | {"status":"processed"} | Stored. |
| 200 | {"status":"duplicate"} | This provider_request_id was already stored; nothing changed. Safe to retry any time — it is never counted twice. |
| 400 | invalid_body / invalid_json / provider_mismatch | Fix the request; do not retry unchanged. |
| 401 | unauthorized | Missing, wrong or replaced key. |
| 402 | trial_expired | The free evaluation ended. Stop sending and subscribe; nothing was deleted. |
| 413 | payload_too_large | Body over the size limit. |
| 429 | rate_limited | More than 1,200 requests in a minute from one ingestion key, whatever their outcome (per web instance). Answers with Retry-After (60 s); resend later with the same provider_request_id. |
| 429 | trial_rate_limit / trial_event_limit | Evaluation limits. The rate limit answers with Retry-After (60 s); the event limit is final. The sample helpers do not wait that long — they drop the event; if you need it, queue it durably and resend after Retry-After with the same provider_request_id. |
| 503 | temporarily_unavailable | Retry with the same provider_request_id. |
Quick retries are for network errors and 5xx only, with the same provider_request_id — that is the supported idempotency mechanism. A 429 needs a longer wait than a telemetry call should block for.
Models and unknown cost
Cost is priced from a list-price table matched on the exact provider + model string. Currently priced:
openai: gpt-4.1openai: gpt-4.1-miniopenai: gpt-4.1-nanoopenai: gpt-4oopenai: gpt-4o-miniopenai: gpt-5openai: gpt-5-miniopenai: gpt-5-nanoopenai: gpt-5.1openai: gpt-5.2openai: gpt-5.4-miniopenai: gpt-5.4-nanoanthropic: claude-opus-5-5anthropic: claude-sonnet-5-5anthropic: claude-opus-4-5anthropic: claude-sonnet-4-5anthropic: claude-haiku-4-5google_gemini: gemini-2.5-flashgoogle_gemini: gemini-2.5-flash-litegoogle_gemini: gemini-2.5-progoogle_gemini: gemini-3.1-flash-litegoogle_gemini: gemini-3.5-flash-litegoogle_gemini: gemini-3.6-flashgoogle_gemini: gemini-3.8-flash
Any other model — a dated snapshot, a new release, a fine-tune — is stored with an unknown cost. Margin Guard then marks the figure incomplete instead of treating it as zero or guessing a price. Events before the price table’s effective date (2026-10-01; Google Gemini: 2026-10-19) are also unpriced.
Google Gemini pricing details
- Standard (synchronous) token prices from Google’s published Gemini API pricing page, checked 2026-10-20. Batch and cached-input discounts are not modelled, so a cost is an upper bound.
- The
models/prefix and letter case are ignored (models/Gemini-2.5-Flashisgemini-2.5-flash). Dated,-latest, preview and tuned model names are not guessed: they are stored with an unknown cost. gemini-2.5-pro: a prompt over 200,000 input tokens is billed at Google’s higher long-context input and output rates; PrinVox applies that tier frominput_tokens.gemini-2.5-flash,gemini-2.5-flash-lite,gemini-3.1-flash-lite: audio input has its own price, soinput_audio_tokensis required (0for text-only calls); without it the event is stored with an unknown cost.gemini-3.6-flashandgemini-3.8-flash: Google’s listed price rises on 2027-01-01; events are priced by theiroccurred_at.
Where the first result appears
After the first event, open Margin Guard → Overview (/app/en/margin-guard/overview): AI cost, by feature and customer. Reporting is on the trailing 30 days by default.
A synthetic example is not a margin result. A real margin needs revenue and cost for the same customers over the same period in the same currency (USD): Stripe connected, paid invoices in that period, and usage events carrying the matching stripe_customer_id. Until then the page shows cost, or shows the margin as incomplete. We publish no time-to-first-result claim.
Free evaluation limits
Where a free evaluation is offered it lasts 14 days, needs no card and never converts into a charge automatically. Server-enforced limits:
- Basic-plan features: up to 25 tracked customers and 30 days of reporting.
- At most 5,000 accepted events in total and 60 per 60 seconds. Duplicate replays and rejected requests do not count.
- When it ends, new events are refused with 402
trial_expired. Stored data is kept and becomes viewable again if you subscribe. Revenue that Stripe reports while ingestion is stopped is still recorded, so a report spanning that period shows revenue without its AI cost — treat it as incomplete.
Questions or problems: hello@prinvox.com.
