Skip to content

Add-on Features & Unbundled Billing ​

Oktivo platform documentation for developers. Covers the unbundled pricing model where recording, transcription, and analysis are optional paid add-ons billed on top of the calling (preset) rate.


Overview ​

Historically, a preset's rate bundled everything: the call, the recording, the transcript, and the analysis were all included in one per-minute price.

The unbundled model splits these apart:

  • The preset rate covers the CALL only.
  • Recording, transcription, and analysis are separate, optional add-on features. A customer is billed for an add-on only when they have the corresponding feature enabled.
  • When a customer does not have an add-on, the artifact is never generated, stored, or billed the recording egress doesn't start, the transcript isn't synced, and analysis doesn't run.

The three add-on features ​

Defined in client_backend/src/types/index.ts → AVAILABLE_FEATURES, and mirrored on the frontend in app/src/lib/features.ts.

Feature keyWhat it gates
call_recordingWhether the call is recorded (LiveKit egress) and playable
call_transcriptionWhether the transcript is produced and stored
call_analysisWhether post-call analysis (sentiment, summary, custom schema) runs

Dependency: call_analysis requires call_transcription analysis needs a transcript to work on. This is enforced everywhere:

  • Backend billing (resolveAddons) forces analysis off when transcription is off.
  • Dispatch gates (resolveAddonGates) force analysis off when transcription is off.
  • Frontend feature assignment (toggleFeatureWithDependencies) auto-selects transcription when analysis is enabled and drops analysis when transcription is removed.

ADDON_FEATURES and ADDON_DEPENDENCIES live in client_backend/src/types/billing.types.ts (and app/src/lib/features.ts).


Pricing model (two tiers + floor) ​

Add-on rates follow the same superadmin → org tiering as preset rates.

SuperAdmin global rate (floor)  ──►  Org rate (>= floor, org margin)  ──►  Customer billed
  • SuperAdmin sets a global platform rate per add-on. Same for every preset. A rate of 0 means the add-on is free at the platform level.
  • Org may set its own rate ≥ the global floor to add margin. If the org doesn't set one, it uses the global rate.
  • Customer is billed at the org rate. The wholesale/superadmin cost stays at the global rate, so the org keeps (org_rate − global_rate) × units as margin exactly like preset margin.

Rates are expressed per billing unit (the same unit as the call), so they scale with call length. Example with billingUnit = 60s, recording rate 0.5: a 2-minute call adds 2 × 0.5 = 1 credit for recording on top of the call cost.

Storage ​

TierWhereField
Global (floor)SystemConfigaddon_rates: { call_recording, call_transcription, call_analysis }
OrgOrgConfigReplicaaddon_rates: { call_recording?, call_transcription?, call_analysis? } (null = use global)

Both default to 0 / unset. Values are non-negative with at most 2 decimal places.


Billing flow ​

Add-on resolution and charging happen inside the existing credit pipeline.

1. Rate snapshot (call end) ​

captureRateSnapshot(orgUID, presetId, customerUID) in client_backend/src/utils/rateResolver.ts resolves the calling rate as before, then calls resolveAddons(...) and attaches an addons map to the snapshot:

ts
addons: {
  call_recording:     { enabled: boolean, rate: number, superadmin_rate: number },
  call_transcription: { enabled: boolean, rate: number, superadmin_rate: number },
  call_analysis:      { enabled: boolean, rate: number, superadmin_rate: number },
}
  • enabled customer has the feature (org plan AND user assignment), via featureAccessService.checkCustomerFeature.
  • rate customer-facing rate (org rate if set and ≥ floor, else global).
  • superadmin_rate wholesale/global rate (the floor).

Performance short-circuit: if no add-on has a configured rate (neither global nor org), resolveAddons returns early with everything disabled and performs no feature-access DB lookups. This keeps the common "add-ons not in use" path cheap.

2. Consumption ​

CreditService.consumeCredits (client_backend/src/services/credit.service.ts) combines the calling rate with the enabled add-on rates:

customer_rate   = call_org_rate       + Σ enabled addon.rate            (customer-facing)
wholesale_rate  = call_superadmin_rate + Σ enabled addon.superadmin_rate (platform cost)

customer_charge = billing_units × customer_rate   → debited from User.creditBalance
org_charge      = billing_units × wholesale_rate  → recorded as wholesale cost (counter only)
margin          = customer_charge − org_charge    → recorded in totalProfitRetained

Helpers sumEnabledAddonRates and sumEnabledAddonSuperadminRates live in billing.types.ts. The checkBalance pre-campaign guard uses the same combined customer rate so it reserves enough credits.

Transaction metadata records the full breakdown: addons, addon_rate_total, addon_superadmin_total, and call_rate (the calling rate excluding add-ons).


Skip generation (when an add-on is disabled) ​

When a customer lacks an add-on, the artifact is never produced. Gates exist at multiple layers for defense-in-depth.

Recording ​

  • Dispatch: resolveAddonGates (client_backend/src/utils/dispatch.utils.ts) resolves enable_recording and passes it in the LiveKit dispatch metadata.
  • Agent: start_recording in Agent/src/handlers/call_handler.py posts skipRecording: true to /start-recording when recording is off (it still calls the endpoint so callAnsweredAt is set for accurate billing duration).
  • Backend safety net: the /start-recording handler (webhook.controller.ts → startRecording) independently re-checks call_recording via featureAccessService and skips egress if the customer lacks it. Test calls are exempt.

Transcription & analysis (standard AI calls) ​

The Python agent produces the transcript and triggers analysis. Both are gated in Agent/src/services/post_call.py:

  • enable_transcription off → transcript sync is skipped entirely.
  • enable_analysis off → analysis trigger is skipped (analysis also requires transcription).

These flags come from the dispatch metadata (enable_transcription, enable_analysis), computed by resolveAddonGates.

Transcription & analysis (CRM human / phone-to-phone / human_callback calls) ​

These calls have no AI agent in the room. The transcript is now produced live by a backend-only STT listener that runs inside the room while the call is active (Agent/src/services/room_transcriber.py), wired into the bridge handlers (phone_bridge_handler.py, cloud_telephony_handler.py, human_agent_inbound_handler.py). The STT provider/model/language are resolved from the selected human_agent's preset (BYOK supported; fallback deepgram/nova-3/multi). On call end, the agent syncs the transcript and triggers analysis via sync_and_analyse (Agent/src/services/bridge_post_call.py): POST /transcription-sync then POST /analyse/conversation the same transcription/analysis path used by AI calls.

  • The backend no longer triggers POST /analyse/crm-recording for these call types on egress_ended. The egress handlers only save the recording and fire recording_ready.
  • There is no recording fallback: if the live transcript is empty, no transcript is synced and no analysis runs.
  • Recording-based STT + analysis (/analyse/crm-recording, Sarvam) is still used for manual retry / reanalyze (/api/v1/crm/retry-analysis, HumanCallService.reprocessCall, PhoneToPhoneCallService.reprocessCall, reanalyze-contacts.ts).

Add-on gating note. The unbundled add-on gate resolvePostCallAddonGate (in webhook.controller.ts) is no longer wired into the automatic post-call flow for these calls (it was used to gate the removed crm-recording trigger). It is retained for the recording-based retry paths. The call_transcription / call_analysis feature checks for AI calls continue via resolveAddonGates.


Frontend ​

RoleWhereCapability
SuperAdminSystem Config panel → "Add-on Rates" cardSet global rates (the floor). 0 = free
Org AdminConfig Management → Pricing & Visibility → "Add-on Rates"View floor + set org rate (≥ floor)
CustomerCredits page → "Add-on Rates"View the effective org rate (read-only)
  • Component: app/src/components/features/AddonRatesEditor.tsx (editable + read-only modes).
  • SuperAdmin editor lives in app/src/components/features/SystemConfigPanel.tsx.
  • Feature assignment UIs (OrgManagementPage, CustomerManagementPage) show the call_transcription checkbox automatically and enforce the analysis→transcription dependency via toggleFeatureWithDependencies.
  • Recording player, transcript tab, and analysis tab are hidden when the customer lacks the corresponding feature (HistoryPage, CampaignDetailPage).

API endpoints ​

SuperAdmin (global rates) ​

MethodPathBody
PUT/superadmin/system-config/addon-rates{ call_recording?, call_transcription?, call_analysis? }

Global config (including addon_rates) is returned by GET /superadmin/system-config.

Org / Customer (org rates + floor view) ​

MethodPathRoleWhat it does
GET/api/org-config/replica/addon-ratesadmin, customerReturns { superadmin_rate, org_rate, org_rate_set } per add-on
PUT/api/org-config/replica/addon-ratesadminSet org rates (each must be ≥ the global floor)

Validation: org rate below the global floor is rejected with 400.


Backend code map ​

client_backend/src/
├── types/
│   ├── billing.types.ts        # ADDON_FEATURES, ADDON_DEPENDENCIES, IAddonRate,
│   │                           # sumEnabledAddonRates, sumEnabledAddonSuperadminRates,
│   │                           # validateAddonRate
│   └── index.ts                # call_transcription added to AVAILABLE_FEATURES + FEATURE_MAP
├── models/
│   ├── SystemConfig.ts         # addon_rates (global floor)
│   └── OrgConfigReplica.ts     # addon_rates (org rate, IOrgAddonRates)
├── utils/
│   ├── rateResolver.ts         # resolveAddons(); snapshot carries addons map
│   └── dispatch.utils.ts       # resolveAddonGates() → enable_recording/transcription/analysis
├── services/
│   ├── credit.service.ts       # consumeCredits/checkBalance add addon rates + margin
│   ├── config.service.ts       # updateAddonRates() (global)
│   ├── orgConfigReplica.service.ts # getAddonRates()/updateAddonRates() (org, floor-enforced)
│   └── calling.service.ts      # passes enable_* gates in dispatch metadata
├── repositories/
│   └── orgConfigReplica.repository.ts # setAddonRates()
├── controllers/
│   ├── systemConfig.controller.ts     # updateDefaultAddonRates
│   ├── orgConfigReplica.controller.ts # getAddonRates / updateAddonRates
│   └── webhook.controller.ts          # recording gate + resolvePostCallAddonGate
└── routes/
    ├── superadmin.routes.ts           # PUT /system-config/addon-rates
    └── orgConfigReplica.routes.ts     # GET/PUT /replica/addon-rates

Python:

Agent/src/schemas/metadata_schema.py   # enable_recording/transcription/analysis (default true)
Agent/src/handlers/call_handler.py     # start_recording passes skipRecording
Agent/src/services/post_call.py        # gates transcript sync + analysis trigger
Analysis/src/schemas/analysis_schema.py    # enable_analysis on RecordingAnalysisRequest
Analysis/src/routes/analysis_routes.py     # threads enable_analysis
Analysis/src/orchestration/analysis_orchestrator.py # skips analysis step, keeps transcript

Backward compatibility & rollout ​

  • All add-on rates default to 0 / unset, and the dispatch/metadata gates default to true. Until a superadmin sets rates and features are granted, behaviour is unchanged nothing new is billed and no artifacts are skipped.
  • Existing config documents without addon_rates are handled by fallbacks that treat missing rates as 0.
  • If the superadmin later raises a global rate above an org's set rate, billing defensively falls back to the global rate for that add-on, so an org can never be billed below the floor even before any cascade.

Migration (deferred) ​

Flipping existing customers to the unbundled model deciding whether to rebalance preset prices (since they no longer bundle add-ons) and how to grant add-on features so current bills don't jump is a deliberate, business-sensitive step handled separately. The mechanism above ships first; the migration is applied last.


Testing notes ​

  • consumeCredits / checkBalance billing math includes add-on rate sums.
  • resolveAddons short-circuits (no DB calls) when no add-on rate is configured — important for property-test performance and production efficiency.
  • Tests that mock utils/dispatch.utils.js must include a resolveAddonGates mock returning { enable_recording, enable_transcription, enable_analysis }.

Last updated: