Skip to content

Billing

Programmatic access to your tenant's credit balance, transactions, invoices, per-call cost breakdowns, and spend alerts. The dashboard's Billing & Usage screen is built entirely on these endpoints.

Rymi bills per minute of connected call audio against a prepaid credit balance, charged in 30-second increments rounded up. A 0–30s call bills 30 seconds, 31–60s bills one minute, and so on. Failed calls (busy, no-answer) don't bill. Only calls that connect and exchange audio do.

Pricing model

Every agent is billed on one of two tracks, set by its agent_kind:

  • Custom (agent_kind: "custom", the default): you choose the STT, LLM, and TTS models yourself. Billed at component cost (the sum of the per-minute rate of each model in the stack) plus a flat $0.02/min platform fee. Channels where you bring your own API key (BYOK) contribute $0 to the component cost, but the $0.02/min platform fee always applies. Use Estimate to price a stack before dialing.
  • Managed (agent_kind: "managed"): a ready-made agent on a tuned, locked stack (speech, model, and voice chosen for you). Billed at the SKU's fixed published price_per_min. List the available SKUs and their prices with List Managed SKUs.

The same rates apply across Free and Enterprise.


List Managed SKUs

GET/v1/managed-skus

Returns the active Rymi Managed agent SKUs, each with its effective per-minute price and locked STT/LLM/TTS stack. Create a managed agent by passing the chosen managed_sku_id (with agent_kind: "managed") to Create Agent.

Response 200

json
{
  "skus": [
    {
      "id": "mgd-...",
      "label": "...",
      "bestFor": "...",
      "description": "...",
      "features": ["..."],
      "pricePerMin": 0.05,
      "effectivePricePerMin": 0.05,
      "stt": { "provider": "...", "model": "..." },
      "llm": { "provider": "...", "model": "..." },
      "tts": { "provider": "...", "model": "..." },
      "voice": "...",
      "supportedLanguages": ["en-US"],
      "isActive": true,
      "sortOrder": 10
    }
  ]
}

Every value above is illustrative. The label, bestFor, description, features, the locked stt/llm/tts/voice stack, the price, and the ordering are all read from the SKU catalog and change without notice — don't hard-code any of them. Read what List Managed SKUs returns.

supportedLanguages is the list of BCP-47 language tags the SKU's stack supports (e.g. en-US, hi-IN) — not bare language codes. pricePerMin may be null for a SKU whose price is not yet set; effectivePricePerMin is the display-safe equivalent (the explicit pricePerMin, or 0 when it is null).


Get Balance

GET/v1/billing/balance

Returns the tenant's current credit balance plus the composition signals the dashboard uses to render the right headline format (runway estimate, low-balance tone, onboarding state, etc.).

Response 200

json
{
  "tenant_id": "550e8400-...",
  "balance_seconds": 18000,
  "balance_usd": 15.0,
  "balance_cents": 1500,
  "balance_credits": 1500,
  "estimated_minutes_remaining": 300,
  "topup_minutes_remaining": 60,
  "is_active": true,
  "tone": "ok",
  "tenant_state": "has_calls",
  "recent_daily_spend_usd": 1.20,
  "runway_days_estimate": 12
}
FieldDescription
balance_secondsRemaining seconds in the active credit bucket, converted at the platform reference rate of $0.05/min. The active bucket is the included plan quota while any remains, then top-up credits
balance_usdThe active bucket in USD
balance_centsThe active bucket in cents (1 credit = 1 cent = $0.01)
balance_creditsTotal remaining credits across both buckets (included quota + top-up)
purchased_creditsTop-up bucket in cents (purchased credit that survives the included quota reset)
free_creditsIncluded monthly-allowance bucket in cents
estimated_minutes_remainingThe active bucket in whole minutes
topup_minutes_remainingTop-up bucket only, in minutes. Purchased credit that survives the included quota reset
is_activeThe tenant's account-active DB flag. It is independent of balance and stays true at a zero or negative balance; tone is the balance-derived signal in this response
toneUI hint: ok, low (under 30 min), or critical (under 5 min or inactive). Reflects total balance (included quota + top-up), so a draining included bucket doesn't false-alarm when top-up credit remains
tenant_stateno_agents, agents_no_calls, or has_calls
recent_daily_spend_usdAverage daily spend over the last 7 days. Only set when every one of those 7 days had a billable call and total spend is above zero; otherwise null. A long-established tenant with one idle day in the window gets null
runway_days_estimatebalance_usd / recent_daily_spend_usd, only set when both are populated

List Transactions

GET/v1/billing/transactions

Per-tenant credit ledger: top-ups, monthly grants, refunds, and per-call deductions.

Query Parameters

ParameterTypeDefaultDescription
limitinteger50Max records (cap 200)
fromISO dateInclusive lower bound on created_at
toISO dateInclusive upper bound on created_at (extended to end of day)

Response 200

json
{
  "transactions": [
    {
      "id": "tx_...",
      "call_id": null,
      "amount_seconds": 36000,
      "amount_usd": 30.0,
      "transaction_type": "stripe_topup",
      "description": "Top-up via Stripe",
      "metadata": {},
      "created_at": "2026-04-01T10:00:00Z"
    }
  ]
}

Per-call deductions carry the call_id they were billed against; top-ups and grants have call_id: null.


List Invoices

GET/v1/billing/invoices

Cursor-paginated list of invoices for the tenant.

Query Parameters

ParameterTypeDefaultDescription
limitinteger25Max records (cap 100)
cursorstringResume token returned in next_cursor

Response 200

json
{
  "invoices": [
    {
      "id": "161bcec1-1108-4e9a-b8b0-0449af48c7d7",
      "invoice_number": "RYMI-2026-00000042",
      "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
      "kind": "topup",
      "status": "paid",
      "currency": "USD",
      "subtotal_minor": 3000,
      "tax_minor": 0,
      "discount_minor": 0,
      "total_minor": 3000,
      "gateway": "stripe",
      "gateway_invoice_id": null,
      "payment_event_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "period_start": null,
      "period_end": null,
      "issued_at": "2026-04-01T10:00:00Z",
      "paid_at": "2026-04-01T10:00:05Z",
      "voided_at": null
    }
  ],
  "next_cursor": null
}

All monetary amounts (subtotal_minor, tax_minor, discount_minor, total_minor) are integer minor units in the invoice's own currency — cents for USD, paise for INR. The currency is copied from the originating payment event and never converted.

Ids are UUIDs, not prefixed strings. gateway_invoice_id is populated for subscription cycles only; top-up invoices leave it null, as do period_start and period_end.

Get Invoice

GET/v1/billing/invoices/:id

Returns the invoice plus its line items.

Response 200

json
{
  "invoice": { "id": "161bcec1-1108-4e9a-b8b0-0449af48c7d7", "invoice_number": "RYMI-2026-00000042", "status": "paid", "currency": "USD", "total_minor": 3000 },
  "items": [
    {
      "id": "9f8c2b41-5d3e-4a17-9c6b-2e8d1a4f7b30",
      "invoice_id": "161bcec1-1108-4e9a-b8b0-0449af48c7d7",
      "kind": "topup",
      "description": "Top-up",
      "quantity": 1,
      "unit_amount_minor": 3000,
      "amount_minor": 3000,
      "plan_id": null,
      "credit_transaction_id": "3d594650-52c4-4f59-a9b1-c8f6d2e7a913"
    }
  ]
}

Invoice and line-item amounts are integer minor units (cents for USD).

StatusMeaning
404Invoice does not exist for this tenant

Per-Call Cost Breakdown

GET/v1/billing/calls

Lists recent calls with per-call cost metadata. Use it to reconcile against your own records.

Query Parameters

ParameterTypeDefaultDescription
limitinteger50Max records (cap 200)
fromISO dateInclusive lower bound on started_at
toISO dateInclusive upper bound on started_at
agent_idstringFilter to a single agent

Response 200

json
{
  "calls": [
    {
      "id": "call_...",
      "started_at": "2026-04-01T10:00:00Z",
      "agent_id": "agent_...",
      "agent_name": "Support Bot",
      "duration_seconds": 145,
      "customer_revenue": 0.29,
      "provider_cost": 0.07,
      "gross_margin": 0.22
    }
  ]
}

Export CSV

GET/v1/billing/export.csv

Downloads up to 5,000 calls as CSV. Filterable by date range, agent, and call status.

Query Parameters

ParameterTypeDescription
fromISO dateInclusive lower bound on started_at
toISO dateInclusive upper bound on started_at
agent_idstringFilter to a single agent
statusstringFilter by call status (completed, failed, …)

Returns text/csv with header row:

call_id,started_at,agent_id,duration_seconds,customer_revenue_usd,provider_cost_usd,gross_margin_usd

Spend Alerts

GET/v1/billing/alerts
PUT/v1/billing/alerts

Configure threshold-based spend alerts.

PUT Request Body

FieldTypeDescription
thresholds_usdnumber[]Spend thresholds in USD (max 10)
low_balance_pctinteger (0–100)Alert when remaining balance ≤ this percent of last top-up
email_enabledbooleanWhether to email the alert

GET Response 200

json
{ "thresholds_usd": [50, 100], "low_balance_pct": 20, "email_enabled": true }

Tenants that have never saved preferences get the defaults shown above.

PUT Response 200

json
{ "ok": true }

Spend-Velocity Alerts

GET/v1/billing/spend-alerts
POST/v1/billing/spend-alerts/:id/acknowledge

Returns un-acknowledged spend-velocity alerts (Phase 2B safety net) and the most recent ten acknowledged ones, plus an endpoint to dismiss a banner.

GET Response 200

json
{
  "unacknowledged": [
    { "id": "alert_...", "alert_type": "spend_velocity", "severity": "critical", "amount_usd": 12.5, "threshold_usd": 10.0 }
  ],
  "recent": []
}

Acknowledging returns { "id": "...", "acknowledged_at": "..." }, or 404 if the alert doesn't exist, was already acknowledged, or belongs to another tenant.


Estimate

POST/v1/billing/estimate

Prices a custom model stack: the sum of each channel's per-minute component cost plus the flat $0.02/min platform fee. The studio's call-planner uses it. Managed agents don't need an estimate; their price is the SKU's fixed price_per_min, returned by List Managed SKUs.

Request Body

FieldTypeDescription
stt_modelstringSpeech-to-text model ID
llm_modelstringLanguage model ID (a realtime model covers STT/TTS too; leave those empty)
tts_modelstringText-to-speech model ID
duration_secondsnumberOptional; when provided, est_for_duration_usd is included in the response

Omitting a channel (or bringing your own key for it) drops that component to $0, but the $0.02/min platform fee always applies.

Response 200

json
{
  "agent_kind": "custom",
  "rate_per_min": 0.0569,
  "est_for_duration_usd": 0.1708
}

Usage Summary

GET/v1/billing/usage-summary

Lane-aware usage summary across the three metered surfaces: voice runtime (in minutes, the customer-facing unit), Studio AI units, and post-call intelligence units. The dashboard's usage cards render this, and the SDKs' billing.usage_summary() returns it.

Response 200

json
{
  "voice_runtime": { "remaining_minutes": 300, "status": "ok" },
  "studio_ai": {
    "used_units": 12,
    "included_units": 100,
    "quota_percent": 12,
    "status": "ok",
    "overage_enabled": false
  },
  "post_call_intelligence": {
    "status": "quota",
    "used_units": 4,
    "included_units": 50,
    "quota_percent": 8
  }
}

Plan & Billing State

GET/v1/billing/plan
POST/v1/billing/checkout/start

The dashboard uses these to read the current plan state and start prepaid top-up checkout.

GET /billing/plan Response 200

json
{
  "plan_id": "free",
  "plan": { "id": "free", "display_name": "Free", "monthly_credit_cents": 100, "byok_allowed": false },
  "monthly_credit_remaining_cents": 80,
  "monthly_credit_resets_at": "2026-05-01T00:00:00Z",
  "topup_balance_cents": 1500,
  "extra_usage_enabled": true,
  "plan_active_since": "2026-04-01T00:00:00Z",
  "preferred_gateway": "stripe",
  "preferred_currency": "USD",
  "usd_to_inr_rate": 84.0,
  "billing_mode": "prepaid_only"
}

billing_mode is currently prepaid_only in the MVP: included plan quota runs first, then prepaid wallet balance covers extra usage when enabled.

POST /billing/checkout/start Body

FieldTypeRequiredDescription
pack_usdnumberyesDollar value to add to the prepaid wallet
billing_countrystringnoISO country code used for gateway selection
tax_idstringnoTax identifier used for gateway selection
save_cardbooleannoRegister the card for auto-recharge when supported

Returns a Razorpay order payload for India-region checkout or a Stripe Checkout URL when Stripe checkout is enabled for the tenant region.

Errors

StatusMeaning
503Gateway not configured (missing Stripe / Razorpay env)

Top-Up Packs

GET/v1/billing/topup-packs

Lists the active top-up packs (with any volume bonus) the dashboard offers on the checkout screen.

Response 200

json
{
  "packs": [
    {
      "id": "pack_...",
      "display_name": "$30 pack",
      "price_cents": 3000,
      "bonus_cents": 0,
      "sort_order": 0
    }
  ]
}

Payment Methods

GET/v1/billing/payment-methods
DELETE/v1/billing/payment-methods/:id

List the tenant's saved payment methods, or detach one. Detaching soft-deletes the method and best-effort detaches it on the gateway; if it was the only remaining method, auto-recharge is switched off.

GET Response 200

json
{
  "payment_methods": [
    {
      "id": "pm_...",
      "gateway": "razorpay",
      "method_type": "card",
      "upi_vpa": null,
      "card_brand": "visa",
      "card_last4": "4242",
      "card_country": "IN",
      "is_default": true,
      "created_at": "2026-04-01T10:00:00Z"
    }
  ]
}

DELETE Response 200

json
{ "ok": true }
StatusMeaning
404Payment method not found for this tenant

Auto-Recharge

GET/v1/billing/auto-recharge
PUT/v1/billing/auto-recharge

Read or update automatic top-up: when the balance falls below threshold_usd, Rymi charges the saved method for pack_usd. For Razorpay tenants, auto-recharge requires a confirmed e-mandate (see Register Mandate).

GET Response 200

json
{
  "enabled": true,
  "pack_usd": 30,
  "threshold_usd": 5,
  "gateway": "razorpay",
  "mandate_required": true,
  "has_mandate": true,
  "billing_phone": "+15551234567"
}

PUT Request Body

All fields optional; only provided fields are changed.

FieldTypeDescription
enabledbooleanTurn auto-recharge on or off
pack_usdnumber (1–1000)Dollar value to recharge each time
threshold_usdnumber (0–100)Balance level that triggers a recharge

pack_usd must be greater than threshold_usd, otherwise a 400 is returned (a smaller pack would immediately re-trigger a recharge loop).

PUT Response 200

json
{ "ok": true }
StatusMeaning
400pack_usd is not greater than threshold_usd

Register Mandate

POST/v1/billing/mandate/register

Creates a Razorpay order that registers a recurring e-mandate alongside an immediate first charge. The frontend opens Razorpay Checkout with the returned order_id; bank-side OTP confirms the mandate, and a webhook persists the token.

Request Body

FieldTypeRequiredDescription
first_charge_usdnumber (1–10000)yesAmount charged immediately when the mandate is set up
max_amount_usdnumber (1–10000)yesMaximum per-debit cap for future recurring charges. Must be ≥ first_charge_usd
contactstringyesBilling phone (7–20 chars), required by Razorpay for recurring debits
methodstringnocard or upi

Response 200

json
{
  "gateway": "razorpay",
  "razorpay": {
    "order_id": "order_...",
    "key_id": "rzp_...",
    "amount": 250000,
    "currency": "INR",
    "customer_id": "cust_..."
  }
}
StatusMeaning
400max_amount_usd is less than first_charge_usd
404Tenant not found
503Razorpay gateway not configured

Extra Usage

GET/v1/billing/extra-usage
PUT/v1/billing/extra-usage

Controls whether the tenant's purchased top-up balance is spent once the included plan quota is exhausted. When off, usage stops at the included quota.

GET Response 200

json
{ "enabled": true }

PUT Request Body

FieldTypeRequiredDescription
enabledbooleanyesWhether top-up balance covers usage beyond the included quota

PUT Response 200

json
{ "ok": true }