Call Fee Model Platform Fee (BYOK) & P2P Fee
Oktivo platform documentation for developers. Covers how each type of call is priced: normal preset rate, the platform fee (for BYOK / custom-preset and browser calls), and the P2P fee (for phone-to-phone calls).
Overview
Not every call is billed against a voice preset. Some call types have no AI agent (or no platform-provided STT/TTS/LLM), so charging a preset rate would be wrong. For those, we bill a flat fee per billing unit instead.
There are two such fees, and they behave identically except for what triggers them:
| Fee | Triggers when… | Why |
|---|---|---|
| Platform fee | The call uses a custom preset (BYOK) the customer supplies their own STT/TTS/LLM keys OR the call is a browser-to-phone (human) call. | The platform pays no provider costs, so only a flat platform/orchestration fee applies. |
| P2P fee | The call is phone-to-phone bridging two phone numbers, no AI agent. | No preset applies to a pure phone bridge. |
Both fees follow the same tiered floor model as preset rates (global → per-org → org → per-customer), and add-ons (recording / transcription / analysis) are still charged on top of either fee.
Which call type is billed how
| Call type | callType | Billed at | Snapshot flag |
|---|---|---|---|
| AI agent, normal preset | ai_agent | Preset rate (cost_per_min / interval pricing → org rate → customer rate) | |
| AI agent, custom preset (BYOK) | ai_agent + byokConfig.customPresetEnabled (or preset_id === "custom") | Platform fee | is_platform_fee: true |
| Browser-to-phone (human) | human | Platform fee | is_platform_fee: true |
| Phone-to-phone (P2P) | phone_to_phone | P2P fee | is_p2p_fee: true |
A call is never billed at more than one fee. The resolver checks P2P first, then custom-preset/platform, then falls back to the normal preset rate.
The 4-tier floor model (same for both fees)
SuperAdmin global fee (floor) ──► Per-org superadmin fee (optional) ──► Org fee (margin) ──► Per-customer override (optional)- SuperAdmin sets a global fee the floor.
0= free at the platform level. - SuperAdmin (optional) can set a preferential per-org fee for a specific org. When set, it becomes both the wholesale cost charged to that org and the floor the org must stay at or above.
- Org may set its own fee ≥ the effective floor to add margin. If unset, the org uses the floor.
- Org (optional) may set a per-customer override ≥ the org fee to charge a specific customer a different rate. If unset, the customer uses the org fee.
- Customer is billed at the most specific rate set (customer override if present, else org fee). The wholesale cost stays at the effective superadmin fee, so the org keeps
(customer_rate − superadmin_fee) × unitsas margin — exactly like preset margin.
This mirrors the add-on rate tiers (CustomerAddonPricing). The per-customer fee override is stored in the CustomerFeePricing collection (one doc per customer per org, with platform_fee_per_min/sec and p2p_fee_per_min/sec).
Both fees support per-minute and per-second rates. For sub-minute billing units, the per-second fee is scaled by the interval (and if per_sec is unset it is derived from per_min / 60).
Floor enforcement & cascade
- An org fee below the effective floor is rejected (
400) when set via the API, and defensively raised to the floor at billing time. - When a superadmin sets a per-org fee above the org's current fee, the org's fee is auto-raised to the new floor.
- A per-customer override below the org fee is rejected (
400) when set, and defensively ignored (floored to the org fee) at billing time a customer is never billed below what the org itself pays.
Data model
Global fees SystemConfig
interface IPlatformFee { per_min: number; per_sec?: number } // BYOK / browser
interface IP2pFee { per_min: number; per_sec?: number } // phone-to-phone
interface ISystemConfig {
// ...
platform_fee: IPlatformFee; // default { per_min: 0 }
p2p_fee: IP2pFee; // default { per_min: 0 }
}per_min allows 2 decimal places; per_sec allows up to 6.
Per-org fees OrgConfigReplica
interface IOrgPlatformFee {
org_rate?: number | null; // org margin (per minute)
org_rate_per_sec?: number | null; // org margin (per second)
superadmin_org_rate?: number | null; // per-org superadmin floor (per minute)
superadmin_org_rate_per_sec?: number | null; // per-org superadmin floor (per second)
}
interface IOrgP2pFee { /* identical shape */ }
interface IOrgConfigReplica {
// ...
platform_fee?: IOrgPlatformFee;
p2p_fee?: IOrgP2pFee;
}Per-customer fees CustomerFeePricing
One document per customer per org. Each value must be ≥ the org fee; null means "use the org fee". Preset-independent (fees don't vary by preset).
interface ICustomerFeePricing {
orgUID: string;
customerUID: string;
platform_fee_per_min?: number | null;
platform_fee_per_sec?: number | null;
p2p_fee_per_min?: number | null;
p2p_fee_per_sec?: number | null;
}
// unique index: { orgUID, customerUID }Billing flow
1. Rate snapshot (call end)
captureRateSnapshot(orgUID, presetId, customerUID, transcriptionMode, isCustomPreset, isP2p) in utils/rateResolver.ts decides the path:
isP2p === true→captureP2pFeeSnapshot(...)→ snapshot markedis_p2p_fee: true.- else
isCustomPreset === true || presetId === "custom"→capturePlatformFeeSnapshot(...)→is_platform_fee: true. - else → normal preset resolution (interval pricing / per-minute / per-second).
On a fee path with a customerUID, the resolver loads the customer's CustomerFeePricing doc and passes the relevant override into the snapshot function, which applies it (floored at the org fee) via applyCustomerFeeOverride. When applied, rate_source becomes "customer_override".
Add-ons are resolved and attached to every snapshot regardless of path.
2. Consumption (call end)
CreditService.consumeCredits(...) (services/credit.service.ts) uses the snapshot's org_rate (customer-facing) and superadmin_rate (wholesale), adds enabled add-on rates on top, debits only the customer wallet, and records the org's wholesale cost + margin as reporting counters (org wallet untouched).
The transaction metadata carries is_platform_fee and is_p2p_fee so the UI can label the charge.
Where each fee is triggered in code
| Call type | File | Call |
|---|---|---|
| AI custom preset (BYOK) | controllers/webhook.controller.ts | captureRateSnapshot(..., isCustomPreset=true) when byokConfig.customPresetEnabled / preset_id === "custom" |
| Browser (human) | services/humanCall.service.ts | captureRateSnapshot(..., "recording", true) |
| Phone-to-phone (P2P) | services/phoneToPhoneCall.service.ts | captureRateSnapshot(..., "recording", false, true) |
For browser and P2P calls the transcript comes from Sarvam STT on the recording, so transcription (if the customer has it) is billed at the recording rate (call_transcription_stt).
API endpoints
SuperAdmin global fees (the floor)
| Method | Path | Body |
|---|---|---|
PUT | /superadmin/system-config/platform-fee | { per_min?, per_sec? } (per_sec: null clears) |
PUT | /superadmin/system-config/p2p-fee | { per_min?, per_sec? } |
SuperAdmin per-org fees
| Method | Path | Body |
|---|---|---|
GET | /api/org-config/replica/:orgUID/platform-fee | |
PUT | /api/org-config/replica/:orgUID/platform-fee | { superadmin_org_rate?, superadmin_org_rate_per_sec? } |
GET | /api/org-config/replica/:orgUID/p2p-fee | |
PUT | /api/org-config/replica/:orgUID/p2p-fee | { superadmin_org_rate?, superadmin_org_rate_per_sec? } |
Org admin (edit) + Customer (read-only)
| Method | Path | Role | What it does |
|---|---|---|---|
GET | /api/org-config/replica/platform-fee | admin, customer | Effective platform fee (floor + org rate) |
PUT | /api/org-config/replica/platform-fee | admin | Set org platform fee (≥ floor) |
GET | /api/org-config/replica/p2p-fee | admin, customer | Effective P2P fee |
PUT | /api/org-config/replica/p2p-fee | admin | Set org P2P fee (≥ floor) |
Org admin per-customer fee overrides
| Method | Path | Role | What it does |
|---|---|---|---|
GET | /api/org-config/replica/fees/customers | admin | List all per-customer fee overrides in the org |
GET | /api/org-config/replica/fees/customers/:customerUID | admin, customer | Effective per-customer fee view (org floor + override) |
PUT | /api/org-config/replica/fees/customers/:customerUID | admin | Upsert customer fee overrides (each ≥ org fee; null clears) |
DELETE | /api/org-config/replica/fees/customers/:customerUID | admin | Remove customer overrides (revert to org fees) |
Frontend
| Role | Where | Component |
|---|---|---|
| SuperAdmin | System Config panel | Global "Platform Fee" and "P2P Fee" editors in SystemConfigPanel.tsx |
| SuperAdmin | Per-Org Rate Management (/superadmin/org-rates) | SuperadminOrgFeeEditor.tsx (per-org platform fee + P2P fee) |
| Org Admin | Config Management → Pricing | PlatformFeeEditor.tsx, P2pFeeEditor.tsx (editable) |
| Org Admin | Config Management → Customer Overrides | CustomerFeeOverrides.tsx (per-customer platform + P2P fees) |
| Customer | Add Credits / Credits page | PlatformFeeEditor readOnly, P2pFeeEditor readOnly |
| Customer (CRM app) | Credits page | Transaction badges: Platform fee (BYOK) and P2P fee |
Backend code map
client_backend/src/
├── models/
│ ├── SystemConfig.ts # IPlatformFee, IP2pFee (global floors)
│ ├── OrgConfigReplica.ts # IOrgPlatformFee, IOrgP2pFee (per-org)
│ └── CustomerFeePricing.ts # per-customer platform/P2P fee overrides
├── repositories/
│ └── orgConfigReplica.repository.ts # setPlatformFee, setP2pFee
├── services/
│ ├── config.service.ts # updatePlatformFee, updateP2pFee (global)
│ ├── orgConfigReplica.service.ts # get/update + superadmin get/set + per-customer get/list/upsert/delete (floor-enforced)
│ ├── credit.service.ts # consumeCredits persists is_platform_fee / is_p2p_fee
│ ├── humanCall.service.ts # browser calls → platform fee
│ └── phoneToPhoneCall.service.ts # P2P calls → P2P fee
├── utils/
│ └── rateResolver.ts # capturePlatformFeeSnapshot, captureP2pFeeSnapshot, applyCustomerFeeOverride, routing
├── controllers/
│ ├── systemConfig.controller.ts # updateDefaultPlatformFee, updateDefaultP2pFee
│ ├── orgConfigReplica.controller.ts # get/update/superadmin + per-customer fee handlers
│ └── webhook.controller.ts # AI custom-preset → platform fee
└── routes/
├── superadmin.routes.ts # global fee routes
└── orgConfigReplica.routes.ts # org + per-org + per-customer fee routesSetup & rollout
- Both fees default to
0. Until a superadmin sets them, BYOK / browser / P2P calls are billed free (only add-ons, if any, apply). Set them on the superadmin System Config page. - Backward compatible. Existing config documents without
platform_fee/p2p_feeare treated as0. Existing preset-based billing is unchanged. CRM_HUMAN_CALL_PRESET_IDnow only affects audit metadata on human/P2P transactions (it no longer drives the rate).
Tests
platform-fee-rate-resolution.test.ts,platform-fee-floor-enforcement.test.ts,platform-fee-billing.integration.test.tsp2p-fee-rate-resolution.test.ts,p2p-fee-billing.integration.test.tshuman-call-platform-fee.test.tscustomer-fee-override.test.ts(per-customer override tier)
Run all fee tests:
cd backend/client_backend
npx vitest --run src/__tests__/platform-fee-*.test.ts src/__tests__/p2p-fee-*.test.ts src/__tests__/human-call-platform-fee.test.ts src/__tests__/customer-fee-override.test.ts