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 publishedprice_per_min. List the available SKUs and their prices with List Managed SKUs.
The same rates apply across Free and Enterprise.
List 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
{
"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
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
{
"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
}| Field | Description |
|---|---|
balance_seconds | Remaining 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_usd | The active bucket in USD |
balance_cents | The active bucket in cents (1 credit = 1 cent = $0.01) |
balance_credits | Total remaining credits across both buckets (included quota + top-up) |
purchased_credits | Top-up bucket in cents (purchased credit that survives the included quota reset) |
free_credits | Included monthly-allowance bucket in cents |
estimated_minutes_remaining | The active bucket in whole minutes |
topup_minutes_remaining | Top-up bucket only, in minutes. Purchased credit that survives the included quota reset |
is_active | The 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 |
tone | UI 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_state | no_agents, agents_no_calls, or has_calls |
recent_daily_spend_usd | Average 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_estimate | balance_usd / recent_daily_spend_usd, only set when both are populated |
List Transactions
Per-tenant credit ledger: top-ups, monthly grants, refunds, and per-call deductions.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Max records (cap 200) |
from | ISO date | — | Inclusive lower bound on created_at |
to | ISO date | — | Inclusive upper bound on created_at (extended to end of day) |
Response 200
{
"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
Cursor-paginated list of invoices for the tenant.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 25 | Max records (cap 100) |
cursor | string | — | Resume token returned in next_cursor |
Response 200
{
"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
Returns the invoice plus its line items.
Response 200
{
"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).
| Status | Meaning |
|---|---|
404 | Invoice does not exist for this tenant |
Per-Call Cost Breakdown
Lists recent calls with per-call cost metadata. Use it to reconcile against your own records.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Max records (cap 200) |
from | ISO date | — | Inclusive lower bound on started_at |
to | ISO date | — | Inclusive upper bound on started_at |
agent_id | string | — | Filter to a single agent |
Response 200
{
"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
Downloads up to 5,000 calls as CSV. Filterable by date range, agent, and call status.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
from | ISO date | Inclusive lower bound on started_at |
to | ISO date | Inclusive upper bound on started_at |
agent_id | string | Filter to a single agent |
status | string | Filter 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_usdSpend Alerts
Configure threshold-based spend alerts.
PUT Request Body
| Field | Type | Description |
|---|---|---|
thresholds_usd | number[] | Spend thresholds in USD (max 10) |
low_balance_pct | integer (0–100) | Alert when remaining balance ≤ this percent of last top-up |
email_enabled | boolean | Whether to email the alert |
GET Response 200
{ "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
{ "ok": true }Spend-Velocity Alerts
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
{
"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
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
| Field | Type | Description |
|---|---|---|
stt_model | string | Speech-to-text model ID |
llm_model | string | Language model ID (a realtime model covers STT/TTS too; leave those empty) |
tts_model | string | Text-to-speech model ID |
duration_seconds | number | Optional; 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
{
"agent_kind": "custom",
"rate_per_min": 0.0569,
"est_for_duration_usd": 0.1708
}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
{
"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
The dashboard uses these to read the current plan state and start prepaid top-up checkout.
GET /billing/plan Response 200
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
pack_usd | number | yes | Dollar value to add to the prepaid wallet |
billing_country | string | no | ISO country code used for gateway selection |
tax_id | string | no | Tax identifier used for gateway selection |
save_card | boolean | no | Register 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
| Status | Meaning |
|---|---|
503 | Gateway not configured (missing Stripe / Razorpay env) |
Top-Up Packs
Lists the active top-up packs (with any volume bonus) the dashboard offers on the checkout screen.
Response 200
{
"packs": [
{
"id": "pack_...",
"display_name": "$30 pack",
"price_cents": 3000,
"bonus_cents": 0,
"sort_order": 0
}
]
}Payment Methods
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
{
"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
{ "ok": true }| Status | Meaning |
|---|---|
404 | Payment method not found for this tenant |
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
{
"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.
| Field | Type | Description |
|---|---|---|
enabled | boolean | Turn auto-recharge on or off |
pack_usd | number (1–1000) | Dollar value to recharge each time |
threshold_usd | number (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
{ "ok": true }| Status | Meaning |
|---|---|
400 | pack_usd is not greater than threshold_usd |
Register Mandate
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
| Field | Type | Required | Description |
|---|---|---|---|
first_charge_usd | number (1–10000) | yes | Amount charged immediately when the mandate is set up |
max_amount_usd | number (1–10000) | yes | Maximum per-debit cap for future recurring charges. Must be ≥ first_charge_usd |
contact | string | yes | Billing phone (7–20 chars), required by Razorpay for recurring debits |
method | string | no | card or upi |
Response 200
{
"gateway": "razorpay",
"razorpay": {
"order_id": "order_...",
"key_id": "rzp_...",
"amount": 250000,
"currency": "INR",
"customer_id": "cust_..."
}
}| Status | Meaning |
|---|---|
400 | max_amount_usd is less than first_charge_usd |
404 | Tenant not found |
503 | Razorpay gateway not configured |
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
{ "enabled": true }PUT Request Body
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | yes | Whether top-up balance covers usage beyond the included quota |
PUT Response 200
{ "ok": true }
