Customer Billing
ModelRelay provides a complete billing system for multi-tenant AI applications. Assign customers to tiers with spend limits, track usage, and integrate with Stripe for subscription management.
Billing Model Overview
flowchart TB
subgraph project["Your Project"]
subgraph free["Free Tier"]
f1["$0/month"]
f2["$5 credits"]
end
subgraph pro["Pro Tier"]
p1["$29/month"]
p2["$29 credits"]
end
subgraph enterprise["Enterprise Tier"]
e1["$299/month"]
e2["unlimited"]
end
free --> A["Customer A"]
pro --> B["Customer B"]
enterprise --> C["Customer C"]
end
Key concepts:
| Concept | Description |
|---|---|
| Tier | A pricing plan with spend limits and model access |
| Customer | An customer in your system, assigned to a tier |
| Spend Limit | The API usage budget for a billing period |
| Overage | Optional subscription-tier entitlement to continue against a prepaid wallet after the spend limit; disabled by default |
| Usage | Tracked per-request based on token consumption |
Tiers
Tiers define pricing plans with spend limits and model access. Before managing customers, you need at least one tier configured in your project.
See Tiers & Margins for complete documentation on:
- Creating and configuring tiers
- Setting per-model pricing and margins
- Common tier patterns (free, pro, enterprise)
- Spend limit strategies
Managing Customers
Creating Customers
Create customers from your backend when users sign up for your service. These endpoints take a secret key in the X-ModelRelay-Api-Key header:
curl -X POST https://api.modelrelay.ai/api/v1/customers \
-H "X-ModelRelay-Api-Key: mr_sk_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "user_123",
"email": "user@example.com"
}'
external_id is your own user ID; the response carries the ModelRelay customer ID under customer.customer.id and, once the customer has one, the subscription under customer.subscription.
Upserting Customers
PUT /customers creates the customer if its external_id is new and updates it otherwise. It returns 201 on create and 200 on update:
curl -X PUT https://api.modelrelay.ai/api/v1/customers \
-H "X-ModelRelay-Api-Key: mr_sk_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "user_123",
"email": "user@example.com"
}'
Listing Customers
curl https://api.modelrelay.ai/api/v1/customers \
-H "X-ModelRelay-Api-Key: mr_sk_..."
The response is {"customers": [...]}. To look up one customer by your own ID, use GET /customers/by-external-id/{external_id}.
Stripe Integration
ModelRelay integrates with Stripe for subscription billing. There are two checkout flows depending on your use case.
Flow 1: Customer-First Checkout
Use this when you know the customer’s identity before checkout. There are two approaches:
1a. Self-Service Checkout (Recommended)
When customers authenticate in your app, your backend mints a customer token. The frontend can initiate checkout directly using that token. Ensure the customer exists (create it via POST /customers) before minting tokens.
sequenceDiagram
participant User
participant App as Your App
participant Backend as Your Backend
participant MR as ModelRelay
participant Stripe
User->>App: Login
App->>Backend: Request customer token
Backend->>MR: POST /auth/customer-token
MR-->>Backend: Customer token
Backend-->>App: Customer token
App->>MR: POST /customers/me/checkout
MR-->>App: session.url
App->>User: Redirect to Stripe
User->>Stripe: Complete payment
Stripe->>MR: Webhook: checkout.completed
MR->>MR: Link subscription to customer
Stripe->>User: Redirect to success_url
# Called with the customer token your backend minted
curl -X POST https://api.modelrelay.ai/api/v1/customers/me/checkout \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tier_code": "pro",
"success_url": "https://myapp.com/success",
"cancel_url": "https://myapp.com/pricing"
}'
The response contains session_id and url; redirect the user to url to complete checkout.
Key benefits:
- Frontend can initiate checkout with a customer token
- Backend keeps control over customer identity
- Works with any auth system
1b. Server-Side Checkout
Use this when your backend manages the customer lifecycle:
sequenceDiagram
participant User
participant App as Your App
participant MR as ModelRelay
participant Stripe
Note over App: Customer already exists
App->>MR: Create checkout session (customer_id)
MR-->>App: session.url
App->>User: Redirect to Stripe
User->>Stripe: Complete payment
Stripe->>MR: Webhook: checkout.completed
MR->>MR: Link subscription to customer
Stripe->>User: Redirect to success_url
curl -X POST https://api.modelrelay.ai/api/v1/customers/{customer_id}/subscribe \
-H "X-ModelRelay-Api-Key: mr_sk_..." \
-H "Content-Type: application/json" \
-d '{
"tier_id": "tier_uuid",
"success_url": "https://myapp.com/success?session={CHECKOUT_SESSION_ID}",
"cancel_url": "https://myapp.com/cancel"
}'
The response contains session_id and url. Redirect the user to url.
Checking Subscription Status
curl https://api.modelrelay.ai/api/v1/customers/{customer_id}/subscription \
-H "X-ModelRelay-Api-Key: mr_sk_..."
The response is {"subscription": {...}}. Its subscription_status is one of active, trialing, past_due, canceled, unpaid, incomplete, incomplete_expired, or paused, and current_period_end marks the end of the current period. A customer can read their own subscription with GET /customers/me/subscription and a customer token.
PAYGO Billing
Pay-as-you-go (PAYGO) billing lets customers prepay a balance and pay for actual usage rather than a fixed monthly subscription. This is ideal for:
- Usage-based pricing: Customers only pay for what they use
- Variable workloads: No monthly limits or resets
- Low-commitment plans: No recurring charges
How PAYGO Works
flowchart LR
A[Customer] -->|Top-up| B[Balance]
B -->|API Request| C[Usage Deducted]
C -->|Low Balance| D[Top-up Reminder]
C -->|Zero Balance| E[Access Blocked]
- Create a tier with
billing_mode: "paygo" - Customer tops up their balance via Stripe checkout
- Each API request deducts from their balance
- When balance reaches zero, API access is blocked until they top up
PAYGO vs Subscription
| Feature | Subscription | PAYGO |
|---|---|---|
| Monthly price | Fixed (e.g., $29/mo) | None |
| Spend limit | Resets monthly | No limit (balance-based) |
| Usage tracking | Against monthly limit | Against balance |
| Billing cycle | Monthly/Yearly | None (prepaid) |
| Access when exceeded | Blocked until reset | Blocked until top-up |
Customer Top-Up Flow
Customers can top up their balance using the self-service endpoint:
sequenceDiagram
participant User
participant App as Your App
participant MR as ModelRelay
participant Stripe
User->>App: Click "Add Funds"
App->>MR: POST /customers/me/topup
MR-->>App: session.url
App->>User: Redirect to Stripe
User->>Stripe: Complete payment
Stripe->>MR: Webhook: payment_intent.succeeded
MR->>MR: Credit balance
Stripe->>User: Redirect to success_url
curl -X POST https://api.modelrelay.ai/api/v1/customers/me/topup \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"credit_amount_cents": 2500,
"success_url": "https://myapp.com/billing?success=true",
"cancel_url": "https://myapp.com/billing"
}'
The response contains a Stripe checkout url to redirect the customer to.
Checking Balance
Customers can check their exact balance and in-flight reservations. Both values are returned in microcents (1 cent = 1,000,000 microcents); available funds are derived as balance_microcents - reserved_microcents rather than returned as a separate field.
curl https://api.modelrelay.ai/api/v1/customers/me/balance \
-H "Authorization: Bearer $CUSTOMER_TOKEN"
{
"customer_id": "…",
"billing_profile_id": "…",
"balance_microcents": 2500000000,
"reserved_microcents": 12000000,
"currency": "usd"
}
Balance Transaction History
View the ledger of all balance changes:
curl https://api.modelrelay.ai/api/v1/customers/me/balance/history \
-H "Authorization: Bearer $CUSTOMER_TOKEN"
The response is {"entries": [...]}. Each entry has a direction (credit or debit), an amount_microcents, a reason, and occurred_at; usage debits also carry model_id, input_tokens, and output_tokens.
Transaction reasons include:
- Top-up via Stripe: Customer added funds
- API usage: Tokens consumed by requests
- Refund: Stripe refund processed
PAYGO Error Handling
When a PAYGO customer’s balance is depleted, requests return HTTP 402. On a 402, read GET /customers/me/balance; if balance_microcents - reserved_microcents is zero or less, prompt the customer to top up.
Usage Tracking
ModelRelay automatically tracks usage for every API request. Customers can query their own usage with a customer bearer token.
Customer Self-Service Usage
Customers can check their own usage using their customer token:
# The customer's own profile
curl https://api.modelrelay.ai/api/v1/customers/me \
-H "Authorization: Bearer $CUSTOMER_TOKEN"
# Usage for the current billing window
curl https://api.modelrelay.ai/api/v1/customers/me/usage \
-H "Authorization: Bearer $CUSTOMER_TOKEN"
The usage response is {"usage": {...}}. Money fields ending in _microcents divide by 100,000,000 to give dollars; spend_limit_cents divides by 100. Spend-limit fields are present only when the tier has a spend limit, and wallet fields only when the customer has a PAYGO wallet.
Usage Response Fields
| Field | Type | Description |
|---|---|---|
window_start |
Date | Start of billing period |
window_end |
Date | End of billing period |
requests |
number | Total API requests |
tokens |
number | Total tokens consumed |
total_cost_microcents |
number | Exact total cost incurred in microcents (always populated) |
spend_limit_cents |
number? | API usage budget for this billing window |
spend_remaining_microcents |
number? | Exact remaining budget in microcents |
percentage_used |
number? | Usage as percentage (0-100) |
low |
boolean? | True if usage ≥ 80% |
wallet_balance_microcents |
number? | Exact PAYGO wallet balance in microcents |
wallet_reserved_microcents |
number? | Exact PAYGO wallet reserved amount in microcents |
overage_enabled |
boolean? | True if PAYGO wallet can cover subscription overages |
daily |
array | Daily usage breakdown |
Customer Self-Service Tier Changes
Customers can change their own subscription tier using their customer bearer token:
POST /api/v1/customers/me/change-tier
Request:
{
"tier_code": "pro"
}
Response:
{
"subscription": {
"tier_code": "pro",
"tier_display_name": "Pro Plan",
"price_amount_cents": 2999,
"price_currency": "usd",
"price_interval": "month",
"subscription_status": "active",
"current_period_start": "2024-01-01T00:00:00Z",
"current_period_end": "2024-02-01T00:00:00Z"
},
"action": "upgraded"
}
The action field indicates what happened:
| Action | Description |
|---|---|
upgraded |
Moved to a higher-priced tier |
downgraded |
Moved to a lower-priced tier |
scheduled_cancellation |
Paid → default Free: subscription cancels at period end |
changed |
Moved to a different tier without a price increase or decrease (for example, equal-price paid tiers or Free → Free) |
unchanged |
The requested tier was already applied; no price change was made |
Tier transitions:
| From | To | Behavior |
|---|---|---|
| Paid → Paid | Stripe updates subscription with proration | |
| Free → Paid | Creates subscription using payment method on file | |
| Paid → default Free | Schedules cancellation at period end; the cancellation webhook assigns the project’s default tier | |
| Free → Free | Updates tier locally |
Error responses:
| Status | Error | Description |
|---|---|---|
| 400 | paygo subscriptions do not support tier changes |
PAYGO billing mode not supported |
| 402 | no payment method on file |
Free → paid requires a saved payment method |
| 404 | tier not found |
The requested tier_code doesn’t exist |
| 409 | tier has no billing pricing configured |
Target tier missing Stripe price |
| 409 | subscription is scheduled for cancellation |
Provider cancellation is authoritative; changing tiers does not reactivate the subscription |
| 409 | paid subscriptions can only return to the project default tier |
Paid cancellation must target the project’s configured default Free tier |
Example: Building an upgrade UI
// Frontend: let customers upgrade their plan
async function upgradeToPro(customerToken: string) {
const response = await fetch("https://api.modelrelay.ai/api/v1/customers/me/change-tier", {
method: "POST",
headers: {
"Authorization": `Bearer ${customerToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ tier_code: "pro" }),
});
if (!response.ok) {
const error = await response.text();
if (response.status === 402) {
// No payment method - redirect to checkout
window.location.href = "/checkout?tier=pro";
return;
}
throw new Error(error);
}
const result = await response.json();
console.log(`${result.action}: now on ${result.subscription.tier_code}`);
}
Note: For free → paid upgrades, the customer must have a payment method on file from a previous subscription. If not, use the checkout flow to collect payment details.
Quota Enforcement
ModelRelay enforces spend limits automatically. When a customer exceeds their tier’s credit allowance, requests are rejected with HTTP 402 (Payment Required).
Handling Quota Errors
A 402 response means the customer has exceeded their quota. Show an upgrade prompt (see Customer Self-Service Tier Changes) or, for PAYGO customers, a top-up prompt. Other errors should propagate as usual.
Best Practices
1. Map External IDs Early
Always set external_id to your user’s ID when creating customers. This makes lookups and upserts reliable, and lets you mint customer tokens with customer_external_id instead of storing ModelRelay customer IDs.
2. Use Upsert for Idempotency
Use PUT /customers instead of POST /customers when your signup handler may retry: upserting the same external_id never creates a duplicate.
3. Check Usage Proactively
Show usage information in your UI before customers hit their limits. GET /customers/me/usage returns percentage_used and sets low to true once 80% of the spend limit is used.
4. Handle Trial Periods
Tiers can have trial periods. When the subscription’s subscription_status is trialing, current_period_end is when the trial ends; show it in your UI.
Next Steps
- Customer Tokens — Issue tokens for customer access
- First Request — Make your first API call
- Streaming — Real-time response streaming