Overview
The billing system tracks credit usage across calls and SMS messages. Each organization has a billing model that defines how credits are consumed. Credits are added manually by support and deducted automatically when calls complete or SMS messages are sent/received. Credits remaining =SUM(billing_credit_additions.credits_added) - SUM(billing_credit_usage.credits_used)
Database Tables
Three tables power the billing system:billing_credit_usage Columns
Usage Types (BillingUsageType enum)
Billing Models
Each organization is assigned one billing model viaorg_billing_settings. The model determines how credits are calculated for calls and SMS.
PER_INTERVIEW
Flat 1 credit per qualifying interview.
No SMS billing.
INTERVIEW_LENGTH
Length-based flat rate per qualifying interview.
No SMS billing.
PER_CREDIT
Per-minute call billing + per-segment SMS billing. Calls:
No completion rate gate — voicemails and no-answers with duration still cost credits.
SMS:
LUXUS
Multi-event billing with separate charges for attempt, minutes, and answered bonus. Calls:
A single answered call can produce up to 3 billing events. An unanswered call (voicemail, no-answer) produces only the attempt event.
SMS:
PER_PLACEMENT
No automatic billing. Credits are not deducted for calls or SMS.Idempotency
Every billing event has ausage_key that is unique in the database. The DAO uses ON CONFLICT (usage_key) DO NOTHING when inserting, so retries and concurrent writes cannot double-charge.
Key Patterns
Integration Points
Billing events are generated at three points in the application:Call Billing
CallPostProcessor._record_billing_credit_usage() (call_post_processor.py:2163) fires after every completed call:
- Loads the call with its campaign relationship to get
org_id - Fetches
org_billing_settingsfor the org - Calls
generate_call_billing_events(call, billing_model)— pure function, no I/O - Passes the resulting events to
billing_dao.record_billing_events() - Errors are caught and logged — billing failures never crash the post-processing pipeline
Outbound SMS Billing
send_smses() (texting_helpers.py:417) fires after each Twilio message is sent:
- Fetches billing settings for the campaign’s org
- Calls
generate_sms_billing_event(message_length, "outbound", sent_message.sid, billing_model) - If the model charges for SMS, records the event via
billing_dao.record_sms_billing_event()
Inbound SMS Billing
failed_call_sms() (text_webhooks.py:660) fires on incoming SMS webhooks:
- Resolves the campaign and org from the most recent call for the sender
- Fetches billing settings for the org
- Calls
generate_sms_billing_event(message_length, "inbound", message_sid, billing_model) - If the model charges for SMS, records the event via
billing_dao.record_sms_billing_event()
Architecture
The billing system follows the project’s separation of concerns:Client Billing Visibility
The billing page is gated behind theCLIENT_BILLING_PAGE_VISIBLE feature flag (per-org, default off). When enabled, non-support users see a read-only billing page with credits remaining, usage history, and addition history. Support-only controls (billing model selector, add credits, backfill, placements tab) remain hidden.
Enabling for an Org
No admin UI — update directly in theorg_feature_flags table:
feature_flags_dao.set_flag(org_id, FeatureFlag.CLIENT_BILLING_PAGE_VISIBLE, True)
Backfill Script
server/scripts/backfill_billing_event_usage.py normalizes existing data for the event model migration. It is idempotent and safe to run multiple times.
Usage:
- Rename
PER_CREDITtoINTERVIEW_LENGTHinorg_billing_settings.billing_model - Rename
PER_CREDITtoINTERVIEW_LENGTHinbilling_credit_usage.billing_model - Backfill
usage_keyfromcall_id_strwhere missing (usage_key = 'call:' || call_id_str) - Normalize NULL
usage_typetoCALL_FLAT - Validation check — reports any rows still missing
usage_key
API Endpoints
Support Endpoints (/support/billing/{org_id}/...)
Client Endpoints (/billing/...)
All client endpoints require CLIENT_BILLING_PAGE_VISIBLE flag to be enabled. Auth context provides org_id.