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 |
| Credits | The spend allowance for a billing period (derived from tier price) |
| 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 programmatically when users sign up for your service:
import { ModelRelay } from "@modelrelay/sdk";
const mr = ModelRelay.fromSecretKey(process.env.MODELRELAY_SECRET_KEY!);
// Create a customer
const customer = await mr.customers.create({
external_id: "user_123", // Your user ID
email: "user@example.com",
});
console.log(customer.customer.id); // ModelRelay customer ID
console.log(customer.subscription?.tier_code ?? ""); // Optional tier code
customer, err := client.Customers.Create(ctx, sdk.CustomerCreateRequest{
ExternalID: sdk.NewCustomerExternalID("user_123"),
Email: "user@example.com",
})
if err != nil {
return err
}
fmt.Println(customer.Customer.ID) // ModelRelay customer ID
tier := ""
if customer.Subscription != nil {
tier = customer.Subscription.TierCode.String()
}
fmt.Println(tier) // Optional tier code
use modelrelay::{Client, CustomerCreateRequest};
let customer = client.customers().create(CustomerCreateRequest {
external_id: "user_123".to_string(),
email: "user@example.com".to_string(),
metadata: None,
}).await?;
println!("{}", customer.customer.id); // ModelRelay customer ID
if let Some(subscription) = &customer.subscription {
println!("{}", subscription.tier_code); // Optional tier code
}
Upserting Customers
Use upsert to create or update customers by their external ID:
// Creates if not exists, updates if exists
const customer = await mr.customers.upsert({
external_id: "user_123",
email: "user@example.com",
});
customer, err := client.Customers.Upsert(ctx, sdk.CustomerUpsertRequest{
ExternalID: sdk.NewCustomerExternalID("user_123"),
Email: "user@example.com",
})
// Creates if not exists, updates if exists
let customer = client.customers().upsert(CustomerUpsertRequest {
external_id: "user_123".to_string(),
email: "user@example.com".to_string(),
metadata: None,
}).await?;
Listing Customers
const customers = await mr.customers.list();
for (const customer of customers) {
console.log(`${customer.external_id}: ${customer.tier_code}`);
}
customers, err := client.Customers.List(ctx)
if err != nil {
return err
}
for _, customer := range customers {
fmt.Printf("%s: %s\n", customer.ExternalID, customer.TierCode)
}
let customers = client.customers().list().await?;
for customer in customers {
println!("{}: {}", customer.external_id, customer.tier_code);
}
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’ve already created a customer and want to start a subscription:
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
// 1. Customer already exists
const customer = await mr.customers.get("cust_uuid");
// 2. Create checkout session for this customer
const session = await mr.customers.subscribe(customer.customer.id, {
tier_id: "tier_uuid",
success_url: "https://myapp.com/success?session={CHECKOUT_SESSION_ID}",
cancel_url: "https://myapp.com/cancel",
});
// 3. Redirect user to Stripe
window.location.href = session.url;
// 1. Customer already exists
customer, err := client.Customers.Get(ctx, customerUUID)
if err != nil {
return err
}
// 2. Create checkout session for this customer
session, err := client.Customers.Subscribe(ctx, customer.Customer.ID, sdk.CustomerSubscribeRequest{
TierID: uuid.MustParse("tier_uuid"),
SuccessURL: "https://myapp.com/success?session={CHECKOUT_SESSION_ID}",
CancelURL: "https://myapp.com/cancel",
})
if err != nil {
return err
}
// 3. Redirect user to Stripe
http.Redirect(w, r, session.URL, http.StatusSeeOther)
use modelrelay::CustomerSubscribeRequest;
// 1. Customer already exists
let customer = client.customers().get(&customer_uuid).await?;
// 2. Create checkout session for this customer
let session = client.customers().subscribe(
customer.customer.id,
CustomerSubscribeRequest {
tier_id: tier_uuid,
success_url: "https://myapp.com/success?session={CHECKOUT_SESSION_ID}".to_string(),
cancel_url: "https://myapp.com/cancel".to_string(),
},
).await?;
// 3. Redirect user to Stripe
// Return session.url to your frontend for redirect
Flow 2: Stripe-First Checkout
Use this when you want users to subscribe before creating an account. Stripe collects their email during checkout:
sequenceDiagram
participant User
participant App as Your App
participant MR as ModelRelay
participant Stripe
Note over App: No customer yet
App->>MR: Create tier checkout session
MR-->>App: session.url
App->>User: Redirect to Stripe
User->>Stripe: Enter email + payment
Stripe->>MR: Webhook: checkout.completed
MR->>MR: Auto-create customer with Stripe email
Stripe->>User: Redirect to success_url
Note over App: Claim customer during auth
// 1. Create checkout session for a tier (no customer yet)
const session = await mr.tiers.checkout("tier_uuid", {
success_url: "https://myapp.com/success",
cancel_url: "https://myapp.com/pricing",
});
// 2. Redirect to Stripe (collects email during checkout)
window.location.href = session.url;
// 3. After checkout, customer is auto-created with Stripe email
// 4. When user authenticates, link their identity to the customer:
const customer = await mr.customers.claim({
email: "user@example.com", // Email from Stripe checkout
provider: "github", // Your OAuth provider
subject: "github_user_id", // User's ID from that provider
});
// 1. Create checkout session for a tier (no customer yet)
session, err := client.Tiers.Checkout(ctx, tierUUID, sdk.TierCheckoutRequest{
SuccessURL: "https://myapp.com/success",
CancelURL: "https://myapp.com/pricing",
})
if err != nil {
return err
}
// 2. Redirect to Stripe (collects email during checkout)
http.Redirect(w, r, session.URL, http.StatusSeeOther)
// 3. After checkout, customer is auto-created with Stripe email
// 4. When user authenticates, link their identity to the customer:
customer, err := client.Customers.Claim(ctx, sdk.CustomerClaimRequest{
Email: "user@example.com", // Email from Stripe checkout
Provider: sdk.NewCustomerIdentityProvider("github"),
Subject: sdk.NewCustomerIdentitySubject("github_user_id"),
})
use modelrelay::{TierCheckoutRequest, CustomerClaimRequest};
// 1. Create checkout session for a tier (no customer yet)
let session = client.tiers().checkout(
&tier_uuid,
TierCheckoutRequest {
success_url: "https://myapp.com/success".to_string(),
cancel_url: "https://myapp.com/pricing".to_string(),
},
).await?;
// 2. Redirect to Stripe (collects email during checkout)
// Return session.url to your frontend for redirect
// 3. After checkout, customer is auto-created with Stripe email
// 4. When user authenticates, link their identity to the customer:
let customer = client.customers().claim(CustomerClaimRequest {
email: "user@example.com".to_string(), // Email from Stripe checkout
provider: "github".to_string(), // Your OAuth provider
subject: "github_user_id".to_string(), // User's ID from that provider
}).await?;
Checking Subscription Status
const status = await mr.customers.getSubscription("cust_uuid");
if (status.active) {
console.log("Subscription active until", status.current_period_end);
} else {
console.log("No active subscription");
}
status, err := client.Customers.GetSubscription(ctx, customerUUID)
if err != nil {
return err
}
if status.Active {
fmt.Println("Subscription active until", status.CurrentPeriodEnd)
} else {
fmt.Println("No active subscription")
}
let status = client.customers().get_subscription(&customer_uuid).await?;
if status.active {
println!("Subscription active until {:?}", status.current_period_end);
} else {
println!("No active subscription");
}
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:
// Frontend: using a customer bearer token
const mr = ModelRelay.fromToken(customerToken);
// Get customer's own profile
const me = await mr.customers.me();
console.log(`Tier: ${me.tier_code}`);
// Get usage for current billing period
const usage = await mr.customers.meUsage();
console.log(`Billing period: ${usage.window_start} - ${usage.window_end}`);
console.log(`Requests: ${usage.requests}`);
console.log(`Tokens: ${usage.tokens}`);
// For paid tiers with credit limits
if (usage.credits_granted_cents) {
const granted = usage.credits_granted_cents / 100;
const used = (usage.credits_used_cents ?? 0) / 100;
const remaining = (usage.credits_remaining_cents ?? 0) / 100;
console.log(`Credits: $${used.toFixed(2)} / $${granted.toFixed(2)} used`);
console.log(`Remaining: $${remaining.toFixed(2)}`);
if (usage.low) {
console.log("⚠️ Credit usage is above 80%");
}
}
// Daily breakdown
for (const day of usage.daily) {
console.log(`${day.day}: ${day.requests} requests, ${day.tokens} tokens`);
}
// Using a customer bearer token
client, err := sdk.NewClientWithToken(customerToken)
if err != nil {
return err
}
// Get customer's own profile
me, err := client.Customers.Me(ctx)
if err != nil {
return err
}
fmt.Printf("Tier: %s\n", me.TierCode)
// Get usage for current billing period
usage, err := client.Customers.MeUsage(ctx)
if err != nil {
return err
}
fmt.Printf("Billing period: %s - %s\n", usage.WindowStart, usage.WindowEnd)
fmt.Printf("Requests: %d\n", usage.Requests)
fmt.Printf("Tokens: %d\n", usage.Tokens)
// For paid tiers with credit limits
if usage.CreditsGrantedCents != nil {
granted := float64(*usage.CreditsGrantedCents) / 100
used := float64(*usage.CreditsUsedCents) / 100
remaining := float64(*usage.CreditsRemainingCents) / 100
fmt.Printf("Credits: $%.2f / $%.2f used\n", used, granted)
fmt.Printf("Remaining: $%.2f\n", remaining)
if usage.Low != nil && *usage.Low {
fmt.Println("⚠️ Credit usage is above 80%")
}
}
// Daily breakdown
for _, day := range usage.Daily {
fmt.Printf("%s: %d requests, %d tokens\n", day.Day, day.Requests, day.Tokens)
}
use modelrelay::{Client, Config, AccessToken};
// Using a customer bearer token
let client = Client::new(Config {
access_token: Some(AccessToken::parse(&customer_token)?),
..Default::default()
})?;
// Get customer's own profile
let me = client.customers().me().await?;
println!("Tier: {}", me.tier_code);
// Get usage for current billing period
let usage = client.customers().me_usage().await?;
println!("Billing period: {} - {}", usage.window_start, usage.window_end);
println!("Requests: {}", usage.requests);
println!("Tokens: {}", usage.tokens);
// For paid tiers with credit limits
if let Some(granted) = usage.credits_granted_cents {
let granted = granted as f64 / 100.0;
let used = usage.credits_used_cents.unwrap_or(0) as f64 / 100.0;
let remaining = usage.credits_remaining_cents.unwrap_or(0) as f64 / 100.0;
println!("Credits: ${:.2} / ${:.2} used", used, granted);
println!("Remaining: ${:.2}", remaining);
if usage.low.unwrap_or(false) {
println!("⚠️ Credit usage is above 80%");
}
}
// Daily breakdown
for day in &usage.daily {
println!("{}: {} requests, {} tokens", day.day, day.requests, day.tokens);
}
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 |
credits_granted_cents |
number? | Credit allowance (paid tiers only) |
credits_used_cents |
number? | Credits consumed |
credits_remaining_cents |
number? | Credits remaining |
percentage_used |
number? | Usage as percentage (0-100) |
low |
boolean? | True if usage ≥ 80% |
daily |
array | Daily usage breakdown |
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
import { ModelRelay, APIError } from "@modelrelay/sdk";
try {
const response = await mr.responses.text(
"claude-sonnet-4-20250514",
"You are helpful.",
"Hello!"
);
} catch (error) {
if (error instanceof APIError && error.status === 402) {
// Customer has exceeded their quota
console.log("Quota exceeded. Please upgrade your plan.");
// Show upgrade prompt to user
} else {
throw error;
}
}
response, err := client.Responses.Create(ctx, req)
if err != nil {
var apiErr sdk.APIError
if errors.As(err, &apiErr) && apiErr.Status == 402 {
// Customer has exceeded their quota
fmt.Println("Quota exceeded. Please upgrade your plan.")
// Show upgrade prompt to user
return
}
return err
}
use modelrelay::errors::{Error, APIError};
match ResponseBuilder::text_prompt("You are helpful.", "Hello!")
.model("claude-sonnet-4-20250514")
.send_text(&client.responses())
.await
{
Ok(response) => println!("{}", response),
Err(Error::API(APIError { status: 402, .. })) => {
// Customer has exceeded their quota
println!("Quota exceeded. Please upgrade your plan.");
// Show upgrade prompt to user
}
Err(e) => return Err(e.into()),
}
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:
// When user signs up
await mr.customers.upsert({
external_id: user.id, // Your user ID
email: user.email,
});
2. Use Upsert for Idempotency
Use upsert instead of create to handle retries gracefully:
// Safe to retry - won't create duplicates
const customer = await mr.customers.upsert({
external_id: userId,
email: userEmail,
});
3. Check Usage Proactively
Show usage information in your UI before customers hit their limits:
const usage = await mr.customers.meUsage();
if (usage.percentage_used && usage.percentage_used > 80) {
showUpgradePrompt();
}
4. Handle Trial Periods
Tiers can have trial periods. Check the subscription status to show appropriate UI:
const status = await mr.customers.getSubscription(customerId);
if (status.status === "trialing") {
const trialEnd = new Date(status.current_period_end!);
showTrialBanner(`Trial ends ${trialEnd.toLocaleDateString()}`);
}
Next Steps
- Customer Tokens — Issue tokens for customer access
- First Request — Make your first API call
- Streaming — Real-time response streaming