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