End-User Billing
Bill your end-users for AI usage with rate plans, quotas, and Stripe Connect payouts.
End-user billing lets you charge your customers for AI usage. Set per-user rate plans with token/request limits, apply markup, and collect payments through Stripe Connect — all from the dashboard.
[!NOTE] New to end-user billing? Start with the Setup Guide — a step-by-step walkthrough for non-technical users. This page is the API reference for your engineering team.
How It Works
- Your end-user makes a request to your app.
- Your backend checks their quota with Cencori.
- If allowed, process the request.
- Report the usage back to Cencori.
- Cencori tracks everything — limits, costs, revenue.
- You view it all in the End-User Billing dashboard.
There are two ways to connect:
Option 1: AI Gateway (Recommended)
If you route AI requests through the Cencori gateway, end-user billing works automatically. Just pass the user ID in your request:
const response = await fetch("https://api.cencori.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": "Bearer csk_your_secret_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello!" }],
user: "user_123", // your end-user's ID
}),
});The gateway handles quota checks, usage recording, markup calculation, and limit enforcement. No additional API calls needed.
Option 2: Usage Events API
If you call AI providers directly and don't use the Cencori gateway, use the Usage Events API to report usage and check quotas.
Check Quota (Before Processing)
Call this before serving an AI request to verify the user is within their limits:
curl https://api.cencori.com/v1/billing/check-quota?end_user_id=user_123 \
-H "Authorization: Bearer csk_your_secret_key"Response:
{
"allowed": true,
"reason": "within_limits",
"end_user_id": "user_123",
"is_new_user": false,
"rate_plan": "pro",
"overage_action": "block",
"retry_after_seconds": null,
"usage": {
"daily_tokens": { "used": 45200, "limit": 1000000 },
"monthly_tokens": { "used": 1250000, "limit": 30000000 },
"daily_requests": { "used": 23, "limit": 500 },
"monthly_requests": { "used": 412, "limit": 10000 },
"requests_per_minute": { "used": 12, "limit": 60 }
},
"billing": {
"markup_percentage": 20,
"flat_rate_per_request": null,
"allowed_models": ["gpt-4o", "claude-sonnet-4.5"]
}
}When allowed is false, the reason field tells you which limit was hit (e.g. daily_token_limit_exceeded, monthly_request_limit_exceeded, requests_per_minute_exceeded, user_blocked). If the limit is time-based, retry_after_seconds tells your app how long to wait before retrying.
check-quota always returns 200 OK — inspect the allowed field to decide what to do. If you route traffic through the Cencori gateway instead, blocked requests return 429 Too Many Requests (see Gateway 429 Responses below).
You can also POST the same request with a JSON body:
curl -X POST https://api.cencori.com/v1/billing/check-quota \
-H "Authorization: Bearer csk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{ "end_user_id": "user_123", "model": "gpt-4o" }'Report Usage (After Processing)
After your AI request completes, report the usage:
curl -X POST https://api.cencori.com/v1/billing/usage-events \
-H "Authorization: Bearer csk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"end_user_id": "user_123",
"model": "gpt-4o",
"provider": "openai",
"prompt_tokens": 500,
"completion_tokens": 1200,
"cost_usd": 0.0085,
"latency_ms": 1340,
"metadata": {
"conversation_id": "conv_abc123"
}
}'Response:
{
"recorded": 1,
"failed": 0,
"total": 1
}Batch Reporting
Send up to 1,000 events in a single request:
curl -X POST https://api.cencori.com/v1/billing/usage-events \
-H "Authorization: Bearer csk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"end_user_id": "user_123",
"model": "gpt-4o",
"prompt_tokens": 500,
"completion_tokens": 1200,
"cost_usd": 0.0085
},
{
"end_user_id": "user_456",
"model": "claude-sonnet-4.5",
"prompt_tokens": 800,
"completion_tokens": 2000,
"cost_usd": 0.012
}
]
}'Full Integration Example
Here's how a typical backend integration looks:
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const CENCORI_API = "https://api.cencori.com/v1/billing";
const CENCORI_KEY = process.env.CENCORI_API_KEY;
async function handleUserMessage(userId: string, message: string) {
// 1. Check quota
const quotaRes = await fetch(
`${CENCORI_API}/check-quota?end_user_id=${userId}`,
{ headers: { Authorization: `Bearer ${CENCORI_KEY}` } }
);
const quota = await quotaRes.json();
if (!quota.allowed) {
throw new Error(`Rate limit: ${quota.reason}`);
}
// 2. Call your AI provider directly
const response = await anthropic.messages.create({
model: "claude-sonnet-4.5",
max_tokens: 1024,
messages: [{ role: "user", content: message }],
});
// 3. Report usage to Cencori
await fetch(`${CENCORI_API}/usage-events`, {
method: "POST",
headers: {
Authorization: `Bearer ${CENCORI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
end_user_id: userId,
model: "claude-sonnet-4.5",
provider: "anthropic",
prompt_tokens: response.usage.input_tokens,
completion_tokens: response.usage.output_tokens,
cost_usd: calculateCost(response.usage), // your cost calculation
}),
});
return response.content[0].text;
}Usage Event Fields
Rate Plans
Rate plans define per-user limits and pricing. Create them in the dashboard under End-User Billing > Rate Plans.
Each plan can set:
[!NOTE] If a user has no assigned plan, they inherit the project's default plan. If no default plan exists, they have no limits.
Rate Plan API
Rate plans are managed via the dashboard or the management API (session-cookie auth).
Create a plan:
POST /api/projects/{projectId}/rate-plans
{
"name": "Pro",
"slug": "pro",
"is_default": false,
"daily_token_limit": 2000000,
"monthly_token_limit": 50000000,
"daily_request_limit": 500,
"monthly_request_limit": 10000,
"requests_per_minute": 60,
"daily_cost_limit_usd": 5.00,
"monthly_cost_limit_usd": 50.00,
"markup_percentage": 20,
"flat_rate_per_request": null,
"allowed_models": ["gpt-4o", "claude-sonnet-4.5"],
"overage_action": "block",
"priority": 0
}Required fields: name, slug. All other fields are optional.
Update a plan:
PATCH /api/projects/{projectId}/rate-plans/{planId}
Send only the fields you want to change. Same field names as create.
Delete a plan:
DELETE /api/projects/{projectId}/rate-plans/{planId}
If end-users are assigned to the deleted plan, they are automatically reassigned to the project's default plan. If no default plan exists and users are still assigned, the delete returns 409 Conflict.
List all plans:
GET /api/projects/{projectId}/rate-plans
Returns all plans for the project, sorted by priority ascending.
Priority
When multiple plans could apply to a user, the plan explicitly assigned to the user takes precedence. If no plan is assigned, the project's default plan (is_default: true) is used. The priority field is for your own ordering in the dashboard — it does not affect which plan is selected.
End-User Management
Auto-Creation
Users are created automatically when usage is recorded for the first time, either by the AI Gateway after a successful request or by the Usage Events API. A quota check by itself does not create an end_users row.
You can also manage users explicitly through the dashboard.
[!NOTE] Project management routes such as
/api/projects/{projectId}/end-usersare currently dashboard management endpoints backed by the authenticated Supabase session cookie. They do not yet support bearer-token auth for external automation.
End-User API
Create or update a user (upsert by external_id):
POST /api/projects/{projectId}/end-users
{
"external_id": "user_123",
"display_name": "Jane Doe",
"email": "jane@example.com",
"rate_plan_id": "uuid-of-rate-plan",
"is_blocked": false,
"metadata": { "plan_tier": "enterprise", "signup_source": "api" }
}Required: external_id. All other fields are optional. If a user with that external_id already exists in the project, it updates instead of creating.
The metadata field accepts arbitrary JSON. Use it to store customer-specific tags (plan tier, signup source, internal notes, etc.) that your team can reference in the dashboard.
Update a user:
PATCH /api/projects/{projectId}/end-users/{endUserId}
Accepts: display_name, email, rate_plan_id, is_blocked, metadata, status (alias — "blocked" sets is_blocked: true, "active" sets is_blocked: false).
Delete a user:
DELETE /api/projects/{projectId}/end-users/{endUserId}
Permanently removes the user and their usage history. This cannot be undone.
Get user details:
GET /api/projects/{projectId}/end-users/{endUserId}
Returns the user with their rate plan details and usage history (last 30 daily/monthly rows).
List users (paginated, filterable):
GET /api/projects/{projectId}/end-users?page=1&per_page=25&search=jane&plan_id=uuid&status=active
Invoices
Generate invoices for your end-users based on their usage over a billing period. Invoices can optionally be sent through Stripe Connect.
List Invoices
GET /api/projects/{projectId}/end-user-billing/invoices?page=1&per_page=25&status=draft&end_user_id=uuid
All query parameters are optional. Filter by status (draft, sent, paid, void, overdue) or end_user_id.
Generate Invoices
POST /api/projects/{projectId}/end-user-billing/invoices
{
"period_start": "2026-03-01",
"period_end": "2026-04-01",
"end_user_ids": ["uuid-1", "uuid-2"],
"send_via_stripe": true
}Response:
{
"generated": 12,
"skipped": 3,
"errors": 0,
"invoices": [...],
"skipped_details": [...]
}Invoice Lifecycle
Stripe Connect
Collect payments from your end-users using Stripe Connect. Cencori acts as the platform — your users pay you directly.
Setup
Stripe Connect is organization-scoped, not project-scoped. You connect one Stripe account per organization, and every project in that organization reuses it.
- Open any project's End-User Billing > Configuration page.
- Click Connect Stripe Account.
- Complete the Stripe onboarding flow.
- Once connected, every project in that organization can generate invoices and collect payments.
How Charges Work
Provider cost (e.g. OpenAI) $0.01
+ Your markup (e.g. 20%) $0.002
+ Flat fee (if set) $0.00
= Customer charge $0.012The markup and flat fee come from the user's rate plan (or the project default).
Stripe Connect Webhook
The webhook endpoint at /api/billing/stripe-connect-webhook processes the following Stripe events:
Set the webhook URL in your Stripe dashboard to https://your-domain.com/api/billing/stripe-connect-webhook and configure the STRIPE_CONNECT_WEBHOOK_SECRET environment variable with the signing secret.
Dashboard
The End-User Billing page has four tabs:
- Configuration: Enable/disable billing, set default markup, connect Stripe.
- End Users: View all users, their plans, usage, and status. Block/unblock users.
- Rate Plans: Create and manage rate plans with limits and pricing.
- Revenue: Usage stats, cost breakdown, daily trends, and top users.
Authentication
Both billing endpoints (https://api.cencori.com/v1/billing/*) use the same API key as the gateway:
- Header:
Authorization: Bearer csk_your_secret_key - Or:
CENCORI_API_KEY: csk_your_secret_key
[!CAUTION] Only secret keys (
csk_*) can report usage and check quotas. Publishable keys (cpk_*) are rejected.
Dashboard management endpoints (/api/projects/*) use the authenticated Supabase session cookie and are not accessible via API key.
Environment Scoping
Quota checks and usage aggregation are scoped by API key environment. Test-key traffic (environment = test) does not consume production quota, and production-key traffic does not consume test quota.
Gateway 429 Responses
When a gateway request is blocked because the end-user exceeded a rate plan limit, the response looks like:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 3600
{
"error": "Rate limit exceeded",
"reason": "daily_token_limit_exceeded",
"retry_after_seconds": 3600
}The Retry-After header is included when the limit is time-based (daily/monthly resets, or requests-per-minute cooldown). Your client should respect this header for automatic retry logic.
Possible reason values:
Error Responses
SDK Support
The TypeScript SDK (cencori on npm) currently covers the AI Gateway, which handles end-user billing automatically when you pass user: "user_123" in your requests. Dedicated cencori.billing.checkQuota() and cencori.billing.recordUsage() methods for the Usage Events API path are not yet available — use the REST endpoints directly for now.
Limits
- Batch size: max 1,000 events per request.
- CSV export: max 50,000 rows per export.
- Rate plan limits are enforced at the daily and monthly level.
Related
- Billing & Usage — plan tiers and credits
- Rate Limiting — project-level rate limits
- API Keys — key types and management
- Authentication — how to authenticate requests

