Skip to main content
Restricted Access: This documentation is only accessible to @tenzo.ai email addresses.

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:
TablePurpose
org_billing_settingsStores which billing model an org uses (one row per org)
billing_credit_additionsRecords every credit top-up (support adds X credits)
billing_credit_usageRecords every billable action (call events, SMS events)

billing_credit_usage Columns

ColumnTypeDescription
credit_usage_idUUID PKUnique row identifier
org_idVARCHAR FKOrganization being billed
usage_typebilling_usage_type_enumWhat kind of charge (see Usage Types below)
usage_keyVARCHAR(255) UNIQUEIdempotency key — prevents double-charging
credits_usedFloatCredit amount deducted
unitsFloat nullableRounded minutes or SMS segments
call_id_strVARCHAR nullableCall ID (string, for backward compat)
call_id_fkUUID FK nullableCall ID (typed FK to calls.call_id)
sms_message_idVARCHAR(255) nullableTwilio message SID for SMS events
sms_message_lengthInteger nullableCharacter length of the SMS
billing_modelVARCHARWhich model was active when this charge was created
created_atDateTimeWhen the charge was recorded

Usage Types (BillingUsageType enum)

ValueDescription
CALL_FLATSingle flat charge per call (PER_INTERVIEW, INTERVIEW_LENGTH models)
CALL_MINUTEPer-minute charge (PER_CREDIT, LUXUS models)
CALL_ATTEMPTAttempt charge — call was dialed (LUXUS only)
CALL_ANSWEREDAnswered bonus — candidate picked up (LUXUS only)
SMS_SENTOutbound SMS charge
SMS_RECEIVEDInbound SMS charge

Billing Models

Each organization is assigned one billing model via org_billing_settings. The model determines how credits are calculated for calls and SMS.

PER_INTERVIEW

Flat 1 credit per qualifying interview.
ConditionCredits
question_completion_rate > 01.0
No questions answered0 (no charge)
No SMS billing.

INTERVIEW_LENGTH

Length-based flat rate per qualifying interview.
ConditionCredits
Call < 10 minutes, completion rate > 01.0
Call >= 10 minutes, completion rate > 02.0
No questions answered0 (no charge)
No SMS billing.

PER_CREDIT

Per-minute call billing + per-segment SMS billing. Calls:
ConditionCredits
Any call with duration > 0ceil(minutes)
Zero duration0 (no charge)
No completion rate gate — voicemails and no-answers with duration still cost credits. SMS:
DirectionCredits
Outbound0.2 * ceil(chars / 160)
Inbound0.2 * ceil(chars / 160)

LUXUS

Multi-event billing with separate charges for attempt, minutes, and answered bonus. Calls:
EventConditionCredits
AttemptCallStatus.attempt_completed0.3
MinutesAnswered + duration > 00.5 * ceil(minutes)
Answered bonusAnswered0.3
A single answered call can produce up to 3 billing events. An unanswered call (voicemail, no-answer) produces only the attempt event. SMS:
DirectionCredits
Outbound0.1 * ceil(chars / 160)
Inbound0.2 flat (any length)

PER_PLACEMENT

No automatic billing. Credits are not deducted for calls or SMS.

Idempotency

Every billing event has a usage_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

PatternUsed byExample
call:{call_id}PER_INTERVIEW, INTERVIEW_LENGTHcall:abc-123
call:{call_id}:minutes:{n}PER_CREDIT, LUXUScall:abc-123:minutes:6
call:{call_id}:attemptLUXUScall:abc-123:attempt
call:{call_id}:answeredLUXUScall:abc-123:answered
sms:out:{twilio_sid}Outbound SMSsms:out:SM123abc
sms:in:{twilio_sid}Inbound SMSsms:in:SM456def

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:
  1. Loads the call with its campaign relationship to get org_id
  2. Fetches org_billing_settings for the org
  3. Calls generate_call_billing_events(call, billing_model) — pure function, no I/O
  4. Passes the resulting events to billing_dao.record_billing_events()
  5. 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:
  1. Fetches billing settings for the campaign’s org
  2. Calls generate_sms_billing_event(message_length, "outbound", sent_message.sid, billing_model)
  3. 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:
  1. Resolves the campaign and org from the most recent call for the sender
  2. Fetches billing settings for the org
  3. Calls generate_sms_billing_event(message_length, "inbound", message_sid, billing_model)
  4. 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 the CLIENT_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.
User typeFlag OFFFlag ON
SupportFull billing + placementsFull billing + placements
ClientNo billing nav or pageRead-only billing page

Enabling for an Org

No admin UI — update directly in the org_feature_flags table:
INSERT INTO org_feature_flags (id, org_id, flag_name, enabled)
VALUES (gen_random_uuid(), '<org_id>', 'client_billing_page_visible', true)
ON CONFLICT (org_id, flag_name) DO UPDATE SET enabled = true;
Or via DAO: 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:
python -m scripts.backfill_billing_event_usage           # Dry run (no changes)
python -m scripts.backfill_billing_event_usage --write-changes  # Persist changes
Steps:
  1. Rename PER_CREDIT to INTERVIEW_LENGTH in org_billing_settings.billing_model
  2. Rename PER_CREDIT to INTERVIEW_LENGTH in billing_credit_usage.billing_model
  3. Backfill usage_key from call_id_str where missing (usage_key = 'call:' || call_id_str)
  4. Normalize NULL usage_type to CALL_FLAT
  5. Validation check — reports any rows still missing usage_key

API Endpoints

Support Endpoints (/support/billing/{org_id}/...)

MethodPathDescription
GET/billing/{org_id}Full billing info (model, credits remaining, totals)
GET/billing/{org_id}/credits-usedTotal credits used
GET/billing/{org_id}/credits-addedCredit addition history
GET/billing/{org_id}/credits-usageCredit usage history
POST/billing/{org_id}/creditsAdd credits to org
PUT/billing/{org_id}/modelUpdate billing model
POST/billing/{org_id}/backfillBackfill credit usage from calls
GET/billing/{org_id}/client-visibilityGet client billing visibility flag
PUT/billing/{org_id}/client-visibilityToggle client billing visibility flag

Client Endpoints (/billing/...)

All client endpoints require CLIENT_BILLING_PAGE_VISIBLE flag to be enabled. Auth context provides org_id.
MethodPathDescription
GET/billing/infoBilling info for authenticated org
GET/billing/credits-addedCredit addition history for authenticated org
GET/billing/credits-usageCredit usage history for authenticated org

File Index

FilePurpose
server/billing/billing_helpers.pyPure functions — generate_call_billing_events(), generate_sms_billing_event(), calculate_credits_for_call()
server/billing/billing_models.pyPydantic models — BillingEvent, API request/response schemas
server/billing/billing_api.pyFastAPI endpoints — support + client routes, feature flag gating
server/dao/billing_dao.pyDAO — record_billing_events(), record_sms_billing_event(), backfill_credit_usage_for_org()
server/models.pySQLAlchemy models — OrgBillingSettings, BillingCreditUsage, BillingCreditAddition, BillingUsageType enum
server/caller/call_post_processor_service/call_post_processor.pyCall billing trigger — _record_billing_credit_usage()
server/texter/texting_helpers.pyOutbound SMS billing trigger — inside send_smses()
server/texter/text_webhooks.pyInbound SMS billing trigger — inside failed_call_sms()
server/scripts/backfill_billing_event_usage.pyData migration script for event model rollout
server/feature_flags/feature_flag_enum.pyCLIENT_BILLING_PAGE_VISIBLE flag definition
ui/src/pages/billing/BillingPage.tsxFrontend billing page with feature flag gating
ui/src/components/side-nav-bar/SideNavBar.tsxConditional billing nav item
server/tests/unit/test_billing.pyUnit tests for billing calculations (48 tests)
server/tests/unit/test_billing_dao.pyDAO tests for event recording and idempotency
server/tests/unit/test_billing_wiring.pyWiring tests for integration trigger points (3 tests)