Skip to content

Admin Billing Config

The billing admin config panel (/admin/tab/billing) lets super-admins manage all platform billing settings through the UI — no SQL or migrations required.

All endpoints require is_super_admin. Non-super-admin requests receive 403 Forbidden.

The tab renders five cards:

  1. Stripe & System Defaults — Stripe API keys and overdraft limit
  2. Credit Conversion Rates — cost in credits for every billable operation
  3. Subscription Tiers — edit the three seeded tiers (no create/delete)
  4. Credit Packs — full CRUD for purchasable credit packs
  5. Billing Config History — last 12 audit entries

Stores the 14 credit conversion rates. Seeded with one row on first migration — always queried with LIMIT 1.

ColumnTypeDescription
credit_price_centsi32Price per credit in USD cents
voice_call_creditsi32Credits per ceil-minute of voice call
voice_recording_creditsi32Credits per ceil-minute of recording
sms_outbound_creditsi32Credits per outbound SMS segment
sms_inbound_creditsi32Credits per inbound SMS segment
mms_outbound_creditsi32Credits per outbound MMS
ai_budget_1k_creditsi32Credits per 1K tokens (budget tier)
ai_mid_1k_creditsi32Credits per 1K tokens (mid tier)
ai_premium_1k_creditsi32Credits per 1K tokens (premium tier)
ai_ultra_1k_creditsi32Credits per 1K tokens (ultra tier)
transcription_creditsi32Credits per ceil-minute of transcription
realtime_openai_creditsi32Credits per minute of OpenAI realtime
realtime_gemini_creditsi32Credits per minute of Gemini realtime
email_outbound_creditsi32Credits per outbound email

Three seeded tiers: Starter, Pro, Enterprise. Ordered by sort_order.

Key fields: name, slug, stripe_price_id, price_per_seat_cents, credits_per_seat, 13 *_included allowance fields (Option<i32>), feature gates (autonomous_plans_enabled, knowledge_base_enabled, custom_analyzers_enabled), call_analysis_level, is_active.

The *_included fields use tri-state encoding:

  • None — operation not available on this tier
  • Some(0) — available but zero included (overage-only)
  • Some(n) — n credits included per billing cycle

Fields: name, credits, price_cents, stripe_price_id, is_active, sort_order. No seed data — operators create packs through the UI.

Two new Stripe fields (stripe_secret_key, stripe_webhook_secret) and overdraft_limit_credits (default 0 = no overdraft).

MethodPathDescription
GET/api/admin/billing-configLoad config with masked Stripe secrets
PUT/api/admin/billing-configUpdate Stripe keys, overdraft, and all rates atomically
MethodPathDescription
GET/api/admin/subscription-tiersList all tiers by sort order
PUT/api/admin/subscription-tiers/{id}Update a single tier
MethodPathDescription
GET/api/admin/credit-packsList all packs by sort order
POST/api/admin/credit-packsCreate a new pack
PUT/api/admin/credit-packs/{id}Update a pack
DELETE/api/admin/credit-packs/{id}/archiveSoft-archive a pack (is_active = false)

Stripe keys follow a round-trip masking protocol:

  1. GET returns "__stored__" when a secret exists, null when unset
  2. PUT interprets "__stored__" as “keep current value” — no change
  3. PUT interprets an empty string as “clear this field”
  4. PUT validates new values by prefix: sk_live_, sk_test_, or rk_ for the secret key; whsec_ for the webhook secret

Sensitive values never appear in API responses or audit logs.

The PUT /api/admin/billing-config handler wraps Stripe keys, overdraft, and all 14 rates in a single SeaORM transaction. If any update fails, everything rolls back.

db.transaction::<_, (), AppError>(|txn| {
Box::pin(async move {
core_active.update(txn).await?;
billing_active.update(txn).await?;
Ok(())
})
})
.await?;

Every mutation records an entry in admin_config_audit with scope "billing":

FieldExample
actionconfig.billing.update, config.tier.update, config.credit_pack.create
changed_fields["overdraft_limit_credits", "voice_call_credits"]
status"saved" or "failed"
title"Updated billing configuration"

Only field names are logged — never values. The history card displays the last 12 entries.

VariableRequiredDescription
STRIPE_SECRET_KEYNoSeeded into core_conf on first migration
STRIPE_WEBHOOK_SECRETNoSeeded into core_conf on first migration

Both are optional so dev environments without Stripe can run migrations. Once set through the admin UI, the database values take precedence.

LayerPath
Typessrc/mods/admin/types/admin_billing_config_types.rs
Handlerssrc/mods/admin/api/get_admin_billing_config_api.rs, update_admin_billing_config_api.rs
Servicessrc/mods/admin/services/admin_billing_config_service.rs
Tier servicesrc/mods/admin/services/admin_subscription_tier_service.rs
Pack servicesrc/mods/admin/services/admin_credit_pack_service.rs
UIsrc/mods/admin/components/admin_billing_config_tab_component.rs
Migrationsmigration/src/m20260415_*.rs, m20260421_*.rs