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:

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]
  1. Create a tier with billing_mode: "paygo"
  2. Customer tops up their balance via Stripe checkout
  3. Each API request deducts from their balance
  4. 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