CGRateS Actions and Topup Behaviors
This guide explains how CGRateS Actions work within OmniCRM, specifically focusing on balance management, topup behaviors, and how different action types affect addon products.
Overview
In OmniCRM's Online Charging System (CGRateS), Actions are the mechanism for adding, modifying, or removing balances on customer accounts.
When you provision an addon or topup product, you're actually executing a CGRateS Action that manipulates the account's balances. (You can totally take other approaches as well, such as manually adding balances to accounts from Playbooks - this is just a common pattern we use to keep things clean)
Key Concepts
Action - A set of operations to perform on an account (add balance, deduct balance, log CDR, etc.)
Balance - A quantity of a resource (data, voice, SMS, monetary) with an expiry time and weight
Balance ID - A unique identifier for a balance type (e.g., "Data Package", "Voice Minutes")
Weight - Priority for balance consumption (higher weight consumed first)
Expiry Time - When the balance expires (absolute date or relative like "+5 days")
Blocker - A special balance flag that blocks all usage when the balance reaches zero, even if other balances exist (see Balance Blockers)
Critical: Actions Must Be Defined First
Before you can execute an Action in a playbook using ExecuteAction, that Action must already be defined in CGRateS. This is a critical prerequisite that's often overlooked.
When Actions Are Defined
Actions are typically defined during initial system configuration or product setup, NOT during provisioning. They are usually created via Python scripts that configure both the CRM and OCS simultaneously.
How Actions Link to Products
Actions are linked to products via a naming convention:
- CGRateS Action:
ActionsId = "Action_50gb-data-pack" - CRM Product:
product_slug = "50gb-data-pack" - In Playbook:
cgr_action_name = "Action_" + product_slug
When the playbook runs, it constructs the Action name from the product_slug and calls ExecuteAction. If that Action doesn't exist in CGRateS, provisioning fails.
Where to Define Actions
Actions should be defined in your product configuration scripts, typically:
- During Initial Setup - When first configuring your system
- When Creating New Products - Define the Action before creating the product
- Via Configuration Scripts - Python scripts that configure both OCS and CRM
Example: Defining an Action before creating the product
import cgrateshttpapi
OCS_Obj = cgrateshttpapi.CGRateS("ocs.example.com", "2080")
tenant = "your_tenant"
# Step 1: Define the Action in CGRateS FIRST
Action_50GB_Data_Pack = {
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_50gb-data-pack",
"Tenant": tenant,
"Actions": [
{
"Identifier": "*topup",
"BalanceType": "*data",
"Units": 50 * 1024 * 1024 * 1024,
"ExpiryTime": "+720h",
"Weight": 10
}
]
}]
}
result = OCS_Obj.SendData(Action_50GB_Data_Pack)
assert result['error'] is None or result['error'] == "EXISTS"
# Step 2: Now create the product in CRM
# (product_slug = "50gb-data-pack" will link to Action_50gb-data-pack)
What Happens If Action Doesn't Exist:
# In playbook
- name: Execute Action
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body:
{
"method": "APIerSv1.ExecuteAction",
"params": [{
"ActionsId": "Action_50gb-data-pack"
}]
}
register: response
# Result if Action not defined:
# response.json.error = "SERVER_ERROR: Action not found"
# Provisioning fails, customer receives no balance
For complete details on defining Actions and linking them to products, see Defining Products.
Balance Independence
By default, addon products create independent balances that work separately from each other. This means:
- You can have multiple active addons simultaneously
- Each addon maintains its own balance and expiry
- Balances are consumed based on weight and expiry rules
Example: Multiple Independent Addons
# Customer has these active balances:
Balance 1:
ID: "Data_5GB_5days_uuid_abc123"
Type: *data
Value: 5368709120 # 5GB in bytes
Expiry: 2024-12-29
Weight: 10
Balance 2:
ID: "Data_10GB_30days_uuid_def456"
Type: *data
Value: 10737418240 # 10GB in bytes
Expiry: 2025-01-24
Weight: 10
# Both balances coexist independently
# System consumes based on weight; if equal weight, order is not guaranteed
When balances have the same weight and match the same destination, CGRateS does not guarantee consumption order based on expiry - the order depends on how balances are stored and retrieved. Use different weights to control consumption order.
Action Types: *topup vs *topup_reset
The action type determines how new balances interact with existing balances of the same ID.
*topup - Additive Behavior
The *topup action adds to existing balances with the same Balance ID and extends the expiry time.
Behavior:
- Finds existing balance with matching ID
- Adds new value to existing value
- Updates expiry to new expiry time
- Preserves existing balance (rollover)
Example:
# Initial state:
Balance:
ID: "Data_Package__5368709120"
Value: 1073741824 # 1GB remaining
Expiry: 2024-12-24 (1 day remaining)
# Run *topup action with:
Action:
Identifier: "*topup"
Balance:
ID: "Data_Package__5368709120" # Same ID - triggers rollover
Value: 5368709120 # 5GB
ExpiryTime: "+5d"
# Result after *topup:
Balance:
ID: "Data_Package__5368709120"
Value: 6442450944 # 6GB (1GB + 5GB rolled over)
OriginalValue: 5368709120 # Still shows original 5GB
Value_hr: "6 GB"
OriginalValue_hr: "5 GB"
Remaining_hr: "6 GB of 5 GB (1 GB rolled over)"
Expiry: 2024-12-29 (5 days from now)
Use Cases:
- Loyalty rewards (add extra data to existing package)
- Compensation credits (add to customer's balance)
- Rollover data packages
- Grace period extensions
*topup_reset - Reset Behavior
The *topup_reset action replaces existing balances with the same Balance ID.
Behavior:
- Finds existing balance with matching ID
- Discards old value (no rollover)
- Sets balance to new value only
- Updates expiry to new expiry time
Example:
# Initial state:
Balance:
ID: "Data_Package__5368709120"
Value: 1073741824 # 1GB remaining
Expiry: 2024-12-24 (1 day remaining)
# Run *topup_reset action with:
Action:
Identifier: "*topup_reset"
Balance:
ID: "Data_Package__5368709120" # Same ID - triggers reset
Value: 5368709120 # 5GB
ExpiryTime: "+5d"
# Result after *topup_reset:
Balance:
ID: "Data_Package__5368709120"
Value: 5368709120 # 5GB (old 1GB discarded)
OriginalValue: 5368709120
Value_hr: "5 GB"
Remaining_hr: "5 GB of 5 GB"
PercentUsed: 0
Expiry: 2024-12-29 (5 days from now)
Use Cases:
- Monthly recurring packages (reset to full amount each month)
- Fixed-size topups (always receive exact amount)
- Plan changes (replace old plan balance with new plan)
- Prevent abuse (can't stack unlimited addons)
Controlling Balance Behavior with Balance IDs
The Balance ID is crucial for determining whether balances are independent or interact with each other.
Balance ID Naming Convention and Human-Readable Views
OmniCRM uses a specific naming convention for Balance IDs that encodes both the descriptive name and the original size. This allows the API to automatically generate human-readable fields for the web UI.
Balance ID Pattern:
{DescriptiveName}__{OriginalSizeInBaseUnits}
Example Balance IDs:
# Data balance: 100GB
"AU_Data_Domestic__107374182400"
# Breaks down to:
# - Descriptive part: "AU_Data_Domestic" (WHAT it is - type/destination)
# - Separator: "__" (double underscore)
# - Original size: "107374182400" (100GB in bytes)
# - UI shows: "AU Data Domestic - 100 GB"
# Voice balance: 3000 minutes
"AU_Voice_Domestic__180000000000000"
# - Descriptive part: "AU_Voice_Domestic" (NOT "AU_Voice_Domestic_3000min")
# - Original size: "180000000000000" (3000 minutes in nanoseconds)
# - UI shows: "AU Voice Domestic - 3000 min"
# SMS balance: 3000 messages
"AU_SMS_Domestic__3000"
# - Descriptive part: "AU_SMS_Domestic"
# - Original size: "3000" (count)
# - UI shows: "AU SMS Domestic - 3000 msgs"
Important: Don't include size information in the descriptive part - it's redundant since the size is encoded after __ and the API converts it to human-readable format automatically.
How the API Creates Human-Readable Views:
When the OmniCRM API retrieves balance data from CGRateS, it automatically parses the Balance ID and generates _hr (human-readable) fields:
{
"BalanceMap": {
"*data": [
{
"ID": "AU_Data_Domestic__107374182400",
"Value": 53687091200,
"ExpiryTime": "2025-01-25T23:59:59Z",
"Weight": 1200,
// Auto-generated human-readable fields:
"ID_hr": "AU Data Domestic",
"OriginalValue": 107374182400,
"OriginalValue_hr": "100 GB",
"Value_hr": "50 GB",
"Remaining_hr": "50 GB of 100 GB",
"PercentUsed": 50,
"ExpiryTime_hr": "25 Jan 2025 (22 days)"
}
]
}
}
API Processing Logic:
-
Parse Balance ID:
balance_id = "AU_Data_Domestic__107374182400"
parts = balance_id.split("__")
descriptive_name = parts[0] # "AU_Data_Domestic"
original_size = int(parts[1]) if len(parts) > 1 else None # 107374182400 -
Generate Human-Readable Descriptive Name:
# Replace underscores with spaces
id_hr = descriptive_name.replace("_", " ") # "AU Data Domestic" -
Convert Original Size to Human Units:
# For data balances (bytes)
if balance_type == "*data":
original_value_hr = convert_bytes_to_gb(original_size) # "100 GB"
# For voice balances (nanoseconds)
elif balance_type == "*voice":
original_value_hr = convert_ns_to_minutes(original_size) # "3000 min"
# For SMS balances (count)
elif balance_type == "*sms":
original_value_hr = f"{original_size} msgs" # "3000 msgs" -
Calculate Usage Percentage:
if original_size and original_size > 0:
percent_used = ((original_size - current_value) / original_size) * 100 -
Format Remaining Display:
remaining_hr = f"{current_value_hr} of {original_value_hr}"
# "50 GB of 100 GB"
Web UI Display:
The frontend uses these _hr fields to display user-friendly balance information:
// Instead of showing raw values:
// ID: "AU_Data_Domestic__107374182400"
// Value: 53687091200
// Show human-readable:
<BalanceCard>
<Title>{balance.ID_hr}</Title> {/* "AU Data Domestic" */}
<Progress value={balance.PercentUsed}> {/* 50% */}
{balance.Remaining_hr} {/* "50 GB of 100 GB" */}
</Progress>
<Expiry>{balance.ExpiryTime_hr}</Expiry> {/* "25 Jan 2025 (22 days)" */}
</BalanceCard>
Why This Matters:
-
Original Size Tracking - Even when balances are partially consumed, the UI can show "50 GB of 100 GB" instead of just "50 GB remaining"
-
Progress Visualization - Percentage calculations enable accurate progress bars
-
Consistent Naming - Descriptive names extracted from Balance IDs ensure consistency between backend and frontend
-
Rollover Display - When using
*topup(rollover), if a customer has 70 GB remaining and tops up 100 GB:- Balance ID remains:
"AU_Data_Domestic__107374182400"(original 100 GB) - Current value becomes: 170 GB
- UI shows: "170 GB (70 GB rolled over + 100 GB new)"
- Balance ID remains:
Best Practice - Creating Balance IDs:
Always include the original size after __ for proper UI display. Don't duplicate size info in the descriptive name:
# Good - descriptive name + size in base units
Action_Data_100GB = {
"Actions": [
{
"BalanceId": f"AU_Data_Domestic__{100 * 1024 * 1024 * 1024}",
"Units": 100 * 1024 * 1024 * 1024
}
]
}
# Bad - redundant size in descriptive name
Action_Data_100GB = {
"Actions": [
{
"BalanceId": f"AU_Data_Domestic_100GB__{100 * 1024 * 1024 * 1024}", # Redundant!
"Units": 100 * 1024 * 1024 * 1024
}
]
}
# Bad - no size information (UI can't calculate percentage)
Action_Data_100GB = {
"Actions": [
{
"BalanceId": "AU_Data_Domestic", # Missing __size
"Units": 100 * 1024 * 1024 * 1024
}
]
}
Special Case - Monetary Balances:
Monetary balances don't typically include original size since they can be topped up to any amount:
# Monetary balance without size encoding
{
"BalanceId": "PAYG_Monetary_Balance",
"BalanceType": "*monetary",
"Units": 5000 # $50.00
}
# UI simply shows current balance without percentage
# "Balance: $50.00"
Strategy 1: Unique IDs (Independent Balances)
Use unique IDs (e.g., with UUIDs) to create completely independent balances that never interact.
Example Implementation:
- name: Generate unique balance identifier
set_fact:
uuid: "{{ 99999999 | random | to_uuid }}"
balance_id: "Data_5days__5368709120_{{ uuid[0:8] }}"
- name: Add independent balance with *topup
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"BalanceType": "*data",
"Balance": {
"ID": "{{ balance_id }}",
"Value": 5368709120,
"ExpiryTime": "+5d",
"Weight": 10
}
}]
}
Result: Each addon creates a new, separate balance even if customer purchases the same addon multiple times.
# Customer purchases "5 days data addon" three times:
Balance 1:
ID: "Data_5days__5368709120_a1b2c3d4"
Value: 5368709120
Value_hr: "5 GB"
OriginalValue_hr: "5 GB"
Remaining_hr: "5 GB of 5 GB"
Expiry: 2024-12-29
Balance 2:
ID: "Data_5days__5368709120_e5f6g7h8"
Value: 5368709120
Value_hr: "5 GB"
Remaining_hr: "5 GB of 5 GB"
Expiry: 2024-12-30
Balance 3:
ID: "Data_5days__5368709120_i9j0k1l2"
Value: 5368709120
Value_hr: "5 GB"
Remaining_hr: "5 GB of 5 GB"
Expiry: 2024-12-31
# Total available: 15GB across three separate balances
# Each shows individually in UI as "Data 5days - 5 GB of 5 GB"
Strategy 2: Shared IDs with *topup (Rollover)
Use the same Balance ID with *topup action to allow balance rollover and expiry extension.
Example Implementation:
- name: Set fixed balance ID
set_fact:
balance_id: "Data_5days__5368709120"
- name: Add balance with *topup (rollover)
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"BalanceType": "*data",
"Balance": {
"ID": "Data_5days__5368709120",
"Value": 5368709120,
"ExpiryTime": "+5d",
"Weight": 10
}
}]
}
Result: Subsequent purchases add to existing balance and extend expiry.
# Day 1: Customer purchases "5 days data addon":
Balance:
ID: "Data_5days__5368709120"
Value: 5368709120
Value_hr: "5 GB"
Remaining_hr: "5 GB of 5 GB"
PercentUsed: 0
Expiry: 2024-12-29
# Day 3: Customer uses 1GB, then purchases same addon again:
Balance:
ID: "Data_5days__5368709120"
Value: 9663676416 # 4GB remaining + 5GB new
Value_hr: "9 GB"
OriginalValue_hr: "5 GB"
Remaining_hr: "9 GB (4 GB rolled over + 5 GB new)"
PercentUsed: -80 # Negative indicates rollover
Expiry: 2024-12-27 (new +5 days from today)
Strategy 3: Shared IDs with *topup_reset (Fixed Amount)
Use the same Balance ID with *topup_reset action to always reset to a fixed amount.
Example Implementation:
- name: Set fixed balance ID
set_fact:
balance_id: "Monthly_Plan__32212254720"
- name: Add balance with *topup_reset (no rollover)
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_Reset_Monthly_Plan",
"Actions": [{
"Identifier": "*topup_reset",
"BalanceType": "*data",
"Units": 32212254720, # 30GB
"ExpiryTime": "*monthly",
"DestinationIds": "*any",
"BalanceId": "Monthly_Plan__32212254720",
"Weight": 10
}]
}]
}
Result: Each month, balance resets to exactly 30GB regardless of how much was used.
# Month 1, Day 1:
Balance:
ID: "Monthly_Plan__32212254720"
Value: 32212254720
Value_hr: "30 GB"
Remaining_hr: "30 GB of 30 GB"
PercentUsed: 0
Expiry: 2024-12-31
# Month 1, Day 25: Customer used 28GB
Balance:
ID: "Monthly_Plan__32212254720"
Value: 2147483648
Value_hr: "2 GB"
Remaining_hr: "2 GB of 30 GB"
PercentUsed: 93
Expiry: 2024-12-31
# Month 2, Day 1: ActionPlan runs *topup_reset
Balance:
ID: "Monthly_Plan__32212254720"
Value: 32212254720 # Reset to full, unused 2GB lost
Value_hr: "30 GB"
Remaining_hr: "30 GB of 30 GB"
PercentUsed: 0
Expiry: 2025-01-31
Balance Blockers
Balance Blockers are a powerful CGRateS feature that allow you to block or limit usage even when balances exist. A blocker balance stops consumption when it reaches zero, preventing further usage regardless of other available balances.
How Blockers Work
When a balance has Blocker: true:
- While blocker has value → Usage is allowed up to the blocker amount
- When blocker reaches zero → All usage stops, even if other balances exist
- Error returned →
INSUFFICIENT_CREDIT_BALANCE_BLOCKERprevents the session
Key Characteristics:
- Blocker balances are checked even when value is zero (unlike normal balances which are skipped)
- When a blocker is encountered with remaining usage requested, CGRateS stops processing and returns an error
- Blockers work with all balance types:
*voice,*data,*sms,*monetary
Use Cases for Blockers
1. Account Suspension
Block all usage when an account is suspended (e.g., payment failure):
Action_Suspend_Account = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_suspend-account",
"Overwrite": True,
"Tenant": tenant,
"Actions": [
# Add zero-value blocker to prevent all usage
{
"Identifier": "*topup",
"BalanceId": "Suspension_Blocker",
"BalanceType": "*monetary",
"DestinationIDs": "*any",
"Units": 0, # Zero value
"BalanceWeight": 9999, # Highest priority - checked first
"Blocker": True, # Block all usage
"Weight": 10
}
]
}]
}
result = OCS_Obj.SendData(Action_Suspend_Account)
Result: All calls/data/SMS blocked regardless of other balances.
2. Spending Limits
Limit maximum spending to prevent bill shock:
Action_Monthly_Plan_With_Cap = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_monthly-with-cap",
"Overwrite": True,
"Tenant": tenant,
"Actions": [
# 10GB included data
{
"Identifier": "*topup_reset",
"BalanceId": f"Included_Data__{10 * 1024 * 1024 * 1024}",
"BalanceType": "*data",
"DestinationIDs": "Dest_PLMN_OnNet",
"Units": 10 * 1024 * 1024 * 1024,
"ExpiryTime": "*month",
"BalanceWeight": 1200, # Consumed first
"Weight": 95
},
# $50 overage limit (blocker)
{
"Identifier": "*topup_reset",
"BalanceId": "Overage_Cap",
"BalanceType": "*monetary",
"DestinationIDs": "*any",
"Units": 5000, # $50.00 maximum overage
"ExpiryTime": "*month",
"BalanceWeight": 1000, # Consumed after included
"Blocker": True, # Stop when $50 spent
"Weight": 90
}
]
}]
}
Flow:
- Customer uses 10GB included → FREE (from Included_Data_10GB)
- Customer uses additional 5GB → Charged from Overage_Cap (PAYG rates)
- When Overage_Cap reaches $0 → All usage blocked (spending limit reached)
3. Time-Limited Free Trial
Provide limited free usage for trial accounts:
Action_Trial_Account = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_trial-100-minutes",
"Overwrite": True,
"Tenant": tenant,
"Actions": [
# 100 free minutes (blocker - stops when exhausted)
{
"Identifier": "*topup",
"BalanceId": f"Trial_Voice__{100 * 60 * 1000000000}",
"BalanceType": "*voice",
"DestinationIDs": "Dest_Domestic_All",
"Units": 100 * 60 * 1000000000, # 100 minutes
"ExpiryTime": "+720h", # 30 days
"BalanceWeight": 1200,
"Blocker": True, # No usage after 100 minutes
"Weight": 10
}
]
}]
}
Result: Customer gets exactly 100 minutes free. After that, all calls blocked (no automatic charges).
4. Destination-Specific Blocking
Block specific destinations while allowing others:
Action_Block_Premium_Numbers = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_block-premium",
"Overwrite": True,
"Tenant": tenant,
"Actions": [
# Regular usage allowed
{
"Identifier": "*topup_reset",
"BalanceId": "Regular_Usage",
"BalanceType": "*monetary",
"DestinationIDs": "Dest_Domestic_All",
"Units": 10000, # $100
"ExpiryTime": "*month",
"BalanceWeight": 1000,
"Weight": 20
},
# Block premium numbers (0900, etc.)
{
"Identifier": "*topup",
"BalanceId": "Premium_Blocker",
"BalanceType": "*monetary",
"DestinationIDs": "Dest_Domestic_Premium",
"Units": 0, # Zero value
"BalanceWeight": 2000, # Higher weight - checked first for premium
"Blocker": True,
"Weight": 10
}
]
}]
}
Flow:
- Domestic calls → Uses Regular_Usage balance
- Premium number calls → Matches Premium_Blocker (weight 2000 > 1000) → BLOCKED
Blocker vs Disabled Balances
Don't confuse Blocker with Disabled:
| Feature | Blocker | Disabled |
|---|---|---|
| Purpose | Stop usage when balance is exhausted | Temporarily pause a balance |
| When value > 0 | Balance is usable normally | Balance is skipped/ignored |
| When value = 0 | Blocks all further usage | Balance is skipped (next balance tried) |
| Use Case | Spending limits, caps, trial limits | Temporarily suspend a specific balance |
# Blocker: Allows 10GB, then blocks everything
{
"BalanceId": f"Data_Cap__{10 * 1024 * 1024 * 1024}",
"Units": 10 * 1024 * 1024 * 1024,
"Blocker": True # Stops usage at 10GB
}
# Disabled: Ignores this balance entirely (temporarily paused)
{
"BalanceId": f"Bonus_Data__{5 * 1024 * 1024 * 1024}",
"Units": 5 * 1024 * 1024 * 1024,
"Disabled": True # This balance won't be used at all
}
Practical Example: Hybrid Plan with Safety Cap
Combine unitary balances, monetary overflow, and a blocker cap:
Action_Safe_Hybrid_Plan = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_safe-hybrid-plan",
"Overwrite": True,
"Tenant": tenant,
"Actions": [
{
"Identifier": "*reset_account",
"Weight": 700
},
# 500 domestic minutes included
{
"Identifier": "*topup_reset",
"BalanceId": f"Domestic_Voice__{500 * 60 * 1000000000}",
"BalanceType": "*voice",
"DestinationIDs": "Dest_Domestic_All",
"Units": 500 * 60 * 1000000000,
"ExpiryTime": "*month",
"BalanceWeight": 1200,
"Weight": 95
},
# $20 overage allowance
{
"Identifier": "*topup_reset",
"BalanceId": "Overage_Allowance",
"BalanceType": "*monetary",
"DestinationIDs": "*any",
"Units": 2000, # $20.00
"ExpiryTime": "*month",
"BalanceWeight": 1000,
"Weight": 90
},
# $50 HARD LIMIT (blocker)
{
"Identifier": "*topup_reset",
"BalanceId": "Hard_Spending_Cap",
"BalanceType": "*monetary",
"DestinationIDs": "*any",
"Units": 5000, # $50.00 absolute maximum
"ExpiryTime": "*month",
"BalanceWeight": 500, # Lower than overage - used last
"Blocker": True, # STOP when cap reached
"Weight": 85
}
]
}]
}
Customer Journey:
- 0-500 minutes: Uses Domestic_Voice_500min (FREE)
- 500-700 minutes: Uses Overage_Allowance at $0.10/min = $20 (200 min)
- 700-1200 minutes: Uses Hard_Spending_Cap at $0.10/min = $50 (500 min)
- At 1200 minutes: Hard_Spending_Cap exhausted → ALL USAGE BLOCKED
Customer gets 1200 minutes total, with spending capped at $50 overage maximum.
Best Practices with Blockers
-
Use High Weight for Blockers
"BalanceWeight": 9999 # Ensure blocker is checked first -
Zero-Value Blockers for Immediate Blocking
"Units": 0, # Block immediately
"Blocker": True -
Notify Customers Before Blocker Exhaustion
- Use ActionTriggers to send notifications at 80%, 90%, 100% of blocker usage
- Give customers option to increase cap before blocking occurs
-
Remove Blockers When Unsuspending
# Use *remove_balance to delete the blocker
{
"Identifier": "*remove_balance",
"BalanceId": "Suspension_Blocker"
} -
Test Blocker Behavior
- Verify blocker returns
INSUFFICIENT_CREDIT_BALANCE_BLOCKERerror - Confirm CDRs show cost = -1.0 when blocked
- Test that other balances are NOT used after blocker exhaustion
- Verify blocker returns
Troubleshooting Blockers
Issue: Usage not blocked despite blocker at zero
Possible Causes:
- Blocker weight too low (other balances checked first)
- DestinationIDs don't match usage destination
- Blocker field not set to
True
Solution:
# Verify blocker configuration
OCS_Obj.SendData({
'method': 'ApierV2.GetAccount',
'params': [{"Tenant": tenant, "Account": "service_uuid"}]
})
# Check:
# - Blocker: true
# - BalanceWeight is highest (e.g., 9999)
# - DestinationIDs includes usage destination
# - Value = 0
Issue: Usage blocked unexpectedly
Possible Cause: Blocker balance created unintentionally with low value
Solution: Check all balances for Blocker: true and verify their values are appropriate for your use case.
Balance Consumption Rules
Rule 1: Destination Precision Priority
Balances with higher destination precision (more specific destination match) are consumed first. This is determined by prefix match length.
# Customer calls +44-20-1234-5678 (London, UK)
Balance 1:
DestinationIDs: "Dest_UK_London" # Prefix: "4420" (precision: 4)
Value: 100 minutes
Weight: 10
Balance 2:
DestinationIDs: "Dest_UK_All" # Prefix: "44" (precision: 2)
Value: 200 minutes
Weight: 10
# Balance 1 consumed first (precision 4 > precision 2)
# More specific destination match wins
Use Case: City-specific or region-specific balances take priority over country-wide balances.
Rule 2: Weight Priority (Same Precision)
When destination precision is equal, balances with higher weight are consumed first.
Balance 1:
ID: "Premium_Data"
Value: 5GB
Weight: 20
Balance 2:
ID: "Standard_Data"
Value: 10GB
Weight: 10
# Balance 1 consumed first (weight 20 > weight 10)
# Even though Balance 2 has more data
Use Case: Priority balances (bonus data consumed before regular data).
Rule 3: Oldest First
When weight matches the oldest balance is used first.
Balance 1:
ID: "Data_Package_A"
Value: 5GB
Expiry: 2024-12-25
Weight: 10
Balance 2:
ID: "Data_Package_B"
Value: 10GB
Expiry: 2025-01-15
Weight: 10
To control consumption order, use different weights:
# Correct approach: Use weight to prioritize soon-to-expire balances
Balance 1:
ID: "Data_Package_A"
Value: 5GB
Expiry: 2024-12-25
Weight: 11 # Higher weight = consumed first
Balance 2:
ID: "Data_Package_B"
Value: 10GB
Expiry: 2025-01-15
Weight: 10 # Lower weight = consumed second
# Balance 1 consumed first (weight 11 > weight 10)
Best Practice: If you want to ensure soon-to-expire balances are consumed first, assign them higher weights when creating them.
Practical Examples
Example 1: Simple Data Addon (Independent)
Scenario: Customer can purchase "5GB 5 days" addon multiple times, each creating a separate balance.
Implementation:
- name: Generate UUID for unique balance
set_fact:
uuid: "{{ 99999999 | random | to_uuid }}"
- name: Add independent 5GB balance
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"BalanceType": "*data",
"Categories": "*any",
"Balance": {
"ID": "Data_5GB_{{ uuid[0:8] }}",
"Value": 5368709120,
"ExpiryTime": "+120h", # 5 days
"Weight": 10
}
}]
}
Customer Experience:
- Purchases addon on Dec 24 → Gets 5GB expiring Dec 29
- Purchases addon on Dec 25 → Gets 5GB expiring Dec 30
- Both balances coexist; consumption order depends on balance weights (both have weight 10, so order not guaranteed)
Example 2: Rollover Data Package
Scenario: "Monthly 50GB Plan" where unused data rolls over when customer tops up early.
Implementation:
- name: Add rollover data balance
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"BalanceType": "*data",
"Balance": {
"ID": "Rollover_Monthly_50GB",
"Value": 53687091200, # 50GB
"ExpiryTime": "+720h", # 30 days
"Weight": 10
}
}]
}
Action Type: Uses default *topup behavior (rollover enabled)
Customer Experience:
- Day 1: Gets 50GB expiring in 30 days
- Day 20: Used 30GB, has 20GB remaining
- Day 20: Tops up again → Gets 70GB total (20GB + 50GB), expiry extends to +30 days from Day 20
Example 3: Fixed Monthly Plan (No Rollover)
Scenario: "Unlimited 100GB Monthly" plan that resets to exactly 100GB each month, no rollover.
Implementation:
- name: Create monthly reset action
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_Monthly_100GB_Reset",
"Overwrite": true,
"Actions": [{
"Identifier": "*topup_reset",
"BalanceType": "*data",
"Units": 107374182400, # 100GB
"ExpiryTime": "*monthly",
"BalanceId": "Monthly_Plan__107374182400",
"Weight": 10
}]
}]
}
- name: Create monthly ActionPlan
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.SetActionPlan",
"params": [{
"Id": "ActionPlan_Monthly_100GB",
"ActionPlan": [{
"ActionsId": "Action_Monthly_100GB_Reset",
"Time": "*monthly",
"Weight": 10
}],
"Overwrite": true,
"ReloadScheduler": true
}]
}
- name: Assign ActionPlan to account
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV2.SetAccount",
"params": [{
"Account": "{{ service_uuid }}",
"ActionPlanIds": ["ActionPlan_Monthly_100GB"],
"ReloadScheduler": true
}]
}
Customer Experience:
- Month 1: Gets 100GB, uses 95GB, has 5GB remaining
- Month 2: Balance resets to 100GB (5GB unused data lost)
- Month 2: Uses 20GB, has 80GB remaining
- Month 3: Balance resets to 100GB (80GB unused data lost)
Example 4: Multi-Tier Balances with Weight Priority
Scenario: Customer has "Bonus Data" (high priority) and "Regular Data" (low priority). Bonus data consumed first.
Implementation:
# Add bonus data with high weight
- name: Add bonus data balance
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Account": "{{ service_uuid }}",
"BalanceType": "*data",
"Balance": {
"ID": "Bonus_Data",
"Value": 5368709120, # 5GB
"ExpiryTime": "+240h", # 10 days
"Weight": 20 # Higher priority
}
}]
}
# Add regular data with normal weight
- name: Add regular data balance
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Account": "{{ service_uuid }}",
"BalanceType": "*data",
"Balance": {
"ID": "Regular_Data",
"Value": 53687091200, # 50GB
"ExpiryTime": "+720h", # 30 days
"Weight": 10 # Normal priority
}
}]
}
Customer Experience:
- Has 5GB bonus data (weight 20) + 50GB regular data (weight 10)
- Consumes all 5GB bonus data first
- Then consumes from 50GB regular data pool
- Regular data preserved longer
Common Action Identifiers
CGRateS supports multiple action identifiers for different operations:
Balance Manipulation
*topup - Add to existing balance (rollover) *topup_reset - Reset balance to new value (no rollover) *debit - Subtract from balance *debit_reset - Set balance to negative value *reset_account - Remove all balances
Designing Addon Products
When designing addon products in OmniCRM, consider these questions:
Question 1: Should balances stack?
Yes (Independent) → Use unique Balance IDs (with UUID)
No (Replace) → Use fixed Balance ID with *topup_reset
Yes (Rollover) → Use fixed Balance ID with *topup
Question 2: What happens to unused balance?
Rollover → Use *topup action
Lost → Use *topup_reset action
Separate pools → Use unique Balance IDs
Question 3: How should balances be consumed?
Oldest first → Use different weights (assign higher weight to older/soon-to-expire balances) Premium first → Different weights (higher weight = higher priority) Specific order → Use weights: 30 (premium), 20 (bonus), 10 (regular) Random/No preference → Use same weight (consumption order not guaranteed)
Question 4: What's the expiry strategy?
Fixed duration → Use relative expiry (+720h for 30 days)
End of month → Use *monthly in ActionPlan
Never expires → Use *unlimited or very long duration
CGRateS Action Structure in Playbooks
Here's the complete structure for creating an Action:
- name: Create CGRateS Action
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_Name_Here",
"Overwrite": true, # Replace if exists
"Actions": [
{
"Identifier": "*topup", # or *topup_reset
"BalanceType": "*data", # *data, *voice, *sms, *monetary
"Units": 5368709120, # Amount to add
"ExpiryTime": "+120h", # +Xh, *unlimited, *monthly
"DestinationIds": "*any", # Usually *any
"BalanceId": "Balance_Name", # Unique or shared
"Weight": 10, # Priority (higher = consumed first)
"Blocker": false, # If true, prevent negative balance
"Disabled": false, # If true, balance is disabled
"SharedGroups": "" # Shared balance group (optional)
},
{
"Identifier": "*cdrlog", # Log this action as CDR
"BalanceType": "*generic",
"ExtraParameters": "{\"Category\":\"^activation\",\"Destination\":\"Addon Name\"}"
}
]
}]
}
Field Descriptions
ActionsId - Unique identifier for this action set
Identifier - The operation type (*topup, *topup_reset, *cdrlog, etc.)
BalanceType - Type of balance:
*data- Data balances (bytes)*voice- Voice balances (seconds)*sms- SMS balances (count)*monetary- Monetary balances (currency units)*generic- Generic balances
Units - Amount to add/deduct (in base units: bytes for data, seconds for voice)
ExpiryTime - When balance expires:
+Xh- Relative (e.g.,+720h= 30 days)*unlimited- Never expires*monthly- End of month2024-12-31T23:59:59Z- Absolute timestamp
BalanceId - Identifier for this balance (shared ID = interacts, unique ID = independent)
Weight - Priority (higher number = higher priority, consumed first)
Blocker - If true, prevents account from going negative
Disabled - If true, balance exists but cannot be used
Defining Actions via Python (Initial Setup)
Actions are typically defined during initial system configuration using Python scripts with the cgrateshttpapi library. These examples show how to define Actions using OCS_Obj.SendData().
Prerequisites
import cgrateshttpapi
import time
OCS_Obj = cgrateshttpapi.CGRateS("ocs.example.com", "2080")
tenant = "your_tenant_name"
tpid = str(tenant) + "_" + str(int(time.time()))
Defining Destinations
Before creating Actions, you must define destinations that specify WHERE balances can be used.
Destinations come in two types:
- Geographic Destinations - Number prefixes for voice/SMS TO places (e.g.,
Dest_International_UK) - PLMN Destinations - Network codes for data FROM places (e.g.,
Dest_PLMN_OnNet,Dest_PLMN_US_Verizon)
Critical Rule:
- Voice/SMS balances → Use geographic destinations (the number being called TO)
- Data balances → Use PLMN destinations (the network customer is connected on FROM)
For complete destination configuration including:
- Geographic destinations (domestic, international, toll-free, premium)
- PLMN destinations (on-net, roaming networks, zones)
- PLMN format rules and best practices
- Troubleshooting destination issues
See: CGRateS Destinations Configuration
Unit Calculations
Understanding unit conversions is critical for defining balances correctly:
# Data balances (in bytes)
1_GB = 1 * 1024 * 1024 * 1024 # 1073741824 bytes
100_GB = 100 * 1024 * 1024 * 1024
# Voice balances (in nanoseconds)
1_minute = 60 * 1000000000 # 60 billion nanoseconds
3000_minutes = 3000 * 60 * 1000000000
# SMS balances (in count)
3000_sms = 3000
Example 1: Multi-Balance Monthly Plan (Python)
A comprehensive monthly plan with data, voice, SMS, and roaming balances that reset each month.
Action_AU_Premium_Plan_1 = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_au-premium-plan-1",
"Overwrite": True,
"Tenant": str(tenant),
"Actions": [
# First, reset the account to clear old balances
{
"Identifier": "*reset_account",
"Weight": 700
},
# Add 100GB data balance
# IMPORTANT: Data balances use PLMN destinations (network customer is connected to)
# NOT geographic destinations. Use YOUR on-net PLMN.
{
"Identifier": "*topup_reset",
"BalanceId": "AU_Data_Domestic__" + str(100 * 1024 * 1024 * 1024),
"BalanceType": "*data",
"DestinationIDs": "Dest_PLMN_OnNet", # Your on-net PLMN (mcc505.mnc057)
"Units": 100 * 1024 * 1024 * 1024,
"ExpiryTime": "*month",
"BalanceWeight": 1200,
"Weight": 90
},
# Add 3000 minutes voice balance
{
"Identifier": "*topup_reset",
"BalanceId": "AU_Voice_Domestic__" + str(3000 * 60 * 1000000000),
"BalanceType": "*voice",
"DestinationIDs": "Dest_AU_Mobile;Dest_AU_Fixed;Dest_AU_TollFree;",
"Units": 3000 * 60 * 1000000000,
"ExpiryTime": "*month",
"BalanceWeight": 1200,
"Weight": 89
},
# Add 3000 SMS balance
{
"Identifier": "*topup_reset",
"BalanceId": "AU_SMS_Domestic__" + str(3000),
"BalanceType": "*sms",
"DestinationIDs": "Dest_AU_Mobile;",
"Units": 3000,
"ExpiryTime": "*month",
"BalanceWeight": 1200,
"Weight": 88
},
# Add 6GB roaming data
{
"Identifier": "*topup_reset",
"BalanceId": "AU_Roaming_Data__" + str(6 * 1024 * 1024 * 1024),
"BalanceType": "*data",
"DestinationIDs": "Dest_Roaming_All",
"Units": 6 * 1024 * 1024 * 1024,
"ExpiryTime": "*month",
"BalanceWeight": 1100,
"Weight": 87
},
# Log this action as a CDR
{
"Identifier": "*cdrlog",
"BalanceId": "",
"BalanceUuid": "",
"BalanceType": "*generic",
"Directions": "*out",
"Units": 0,
"ExpiryTime": "",
"Filter": "",
"TimingTags": "",
"DestinationIds": "",
"RatingSubject": "",
"Categories": "",
"SharedGroups": "",
"BalanceWeight": 0,
"ExtraParameters": "{\"Category\":\"^activation\",\"Destination\":\"AU Premium Plan 1\"}",
"BalanceBlocker": "false",
"BalanceDisabled": "false",
"Weight": 80
}
]
}]
}
# Send the action definition to CGRateS
result = OCS_Obj.SendData(Action_AU_Premium_Plan_1)
assert result['error'] is None or result['error'] == "EXISTS"
print("Created Action: Action_au-premium-plan-1")
Key Points:
- Uses
*reset_accountto clear old balances first - Uses
*topup_resetfor fixed monthly allowances (no rollover) - BalanceWeight determines consumption order (domestic 1200 > roaming 1100)
- Weight determines execution order within the Action
- Includes
*cdrlogfor tracking activations
Example 2: Simple Data Addon (Python)
A simple 20GB data addon with rollover disabled.
Action_AU_Data_Addon_20GB = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_au-data-addon-20gb",
"Overwrite": True,
"Tenant": str(tenant),
"Actions": [
# Reset account first
{
"Identifier": "*reset_account",
"Weight": 700
},
# Add 20GB data
# Data balances use PLMN destinations (which network customer is on)
{
"Identifier": "*topup_reset",
"BalanceId": "AU_Data_Domestic__" + str(20 * 1024 * 1024 * 1024),
"BalanceType": "*data",
"DestinationIDs": "Dest_PLMN_OnNet", # Your on-net PLMN (mcc505.mnc057)
"Units": 20 * 1024 * 1024 * 1024,
"ExpiryTime": "*month",
"BalanceWeight": 1200,
"Weight": 90
}
]
}]
}
result = OCS_Obj.SendData(Action_AU_Data_Addon_20GB)
assert result['error'] is None or result['error'] == "EXISTS"
print("Created Action: Action_au-data-addon-20gb")
Example 3: International Voice Addon (Python)
An addon for international calling minutes.
Action_AU_International_Voice_100min = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_au-international-voice-100min",
"Overwrite": True,
"Tenant": str(tenant),
"Actions": [
{
"Identifier": "*reset_account",
"Weight": 700
},
# Add 100 minutes for international calls
{
"Identifier": "*topup_reset",
"BalanceId": "AU_Voice_International__" + str(100 * 60 * 1000000000),
"BalanceType": "*voice",
"DestinationIDs": "Dest_International_All",
"Units": 100 * 60 * 1000000000,
"ExpiryTime": "*month",
"BalanceWeight": 1000,
"Weight": 90
}
]
}]
}
result = OCS_Obj.SendData(Action_AU_International_Voice_100min)
assert result['error'] is None or result['error'] == "EXISTS"
print("Created Action: Action_au-international-voice-100min")
Note: Voice/SMS use geographic destinations (number being called), while data uses PLMN destinations (network customer is connected to). See Defining Products for destination configuration.
Python Action Field Reference
Action Definition Fields:
- ActionsId (required) - Unique identifier for this action set (must match product_slug convention in CRM)
- Overwrite - If True, replace existing action with same ID
- Tenant - CGRateS tenant name
- Actions - Array of individual actions to execute
Individual Action Fields:
- Identifier - Action type (
*topup,*topup_reset,*reset_account,*cdrlog, etc.) - BalanceId - Unique identifier for this balance (must match across topups for rollover to work)
- BalanceType - Type of balance (
*data,*voice,*sms,*monetary) - DestinationIDs - Controls WHERE the balance can be used:
- For voice/SMS: Use geographic destinations (e.g.,
"Dest_AU_Mobile","Dest_International_UK") - For data: Use PLMN destinations (e.g.,
"Dest_PLMN_OnNet","Dest_PLMN_US_Verizon")
- For voice/SMS: Use geographic destinations (e.g.,
- Units - Amount to add (bytes for data, nanoseconds for voice, count for SMS)
- ExpiryTime - When balance expires (
*month,+720h,2024-12-31, etc.) - BalanceWeight - Consumption priority (higher = consumed first)
- Weight - Execution order within the action set (higher = executes first)
- Blocker (optional) - Boolean flag; if
True, blocks all usage when this balance reaches zero (see Balance Blockers) - Disabled (optional) - Boolean flag; if
True, this balance is ignored/skipped during consumption
Executing Actions
Once an Action is created, execute it on an account:
- name: Execute Action on Account
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "APIerSv1.ExecuteAction",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ActionsId": "Action_Name_Here"
}]
}
This executes all operations defined in the Action on the specified account.
Balance Management Approaches
There are three primary approaches to managing balances in CGRateS, each with different trade-offs for customer experience, billing predictability, and operational complexity.
Comparison: Unitary vs Monetary vs Hybrid
This table summarizes the trade-offs between the three balance approaches:
| Feature | Unitary | Monetary (PAYG) | Hybrid |
|---|---|---|---|
| Customer Predictability | ✅ Fixed monthly cost | ❌ Variable costs | ⚠️ Mostly predictable |
| Destination Flexibility | ❌ Limited to included destinations | ✅ Call/use anywhere | ✅ Included + anywhere |
| Overage Handling | ❌ Hard cutoff | ✅ Automatic usage | ✅ Automatic overflow |
| Bill Shock Risk | ✅ Low (hard caps) | ❌ High (unlimited billing) | ⚠️ Moderate (capped overflow) |
| Configuration Complexity | ⚠️ Moderate (many balances) | ✅ Simple (one balance) | ❌ Complex (both) |
| Revenue Optimization | ⚠️ Lower ARPU | ✅ Higher ARPU from heavy users | ✅ Balanced ARPU |
| Customer Satisfaction | ✅ High (no surprises) | ❌ Low (bill shock) | ✅ High (best of both) |
| Best For | Predictable users | Occasional users | Most customers |
Approach 1: Unitary Balances
Concept: Provide specific quantities for specific destinations. Each balance has a fixed amount (minutes, GB, SMS count) tied to specific destinations. When exhausted, usage is blocked unless there's a monetary fallback.
Example: Domestic Plan with Multiple Balances
Action_Domestic_Plan = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_domestic-plan",
"Overwrite": True,
"Tenant": tenant,
"Actions": [
{
"Identifier": "*reset_account",
"Weight": 700
},
# 500 minutes domestic calls
{
"Identifier": "*topup_reset",
"BalanceId": f"Domestic_Voice__{500 * 60 * 1000000000}",
"BalanceType": "*voice",
"DestinationIDs": "Dest_Domestic_All", # ONLY domestic
"Units": 500 * 60 * 1000000000, # 500 minutes in nanoseconds
"ExpiryTime": "*month",
"BalanceWeight": 1200,
"Weight": 90
},
# 1000 SMS domestic
{
"Identifier": "*topup_reset",
"BalanceId": "Domestic_SMS__1000",
"BalanceType": "*sms",
"DestinationIDs": "Dest_Domestic_All",
"Units": 1000,
"ExpiryTime": "*month",
"BalanceWeight": 1200,
"Weight": 89
},
# 10GB domestic data
{
"Identifier": "*topup_reset",
"BalanceId": f"Domestic_Data__{10 * 1024 * 1024 * 1024}",
"BalanceType": "*data",
"DestinationIDs": "Dest_PLMN_OnNet", # Your on-net PLMN
"Units": 10 * 1024 * 1024 * 1024,
"ExpiryTime": "*month",
"BalanceWeight": 1200,
"Weight": 88
},
{
"Identifier": "*cdrlog",
"BalanceType": "*generic",
"ExtraParameters": "{\"Category\":\"^activation\",\"Destination\":\"Domestic Plan\"}",
"Weight": 80
}
]
}]
}
result = OCS_Obj.SendData(Action_Domestic_Plan)
assert result['error'] is None or result['error'] == "EXISTS"
How This Works:
- Domestic calls (1-555-1234) → Uses 500-minute balance
- International calls (44-20-xxx) → NO balance available, blocked OR uses monetary balance if available
- Domestic SMS → Uses 1000-SMS balance
- Home data usage → Uses 10GB balance
- Roaming data → NO balance available (would need separate roaming data balance)
Pros:
- Predictable costs for customers
- No bill shock
- Clear limits
Cons:
- Less flexible - can't use service outside included destinations
- Requires multiple balances for different use cases
- Customer may feel restricted
Approach 2: Monetary (PAYG)
Concept: Provide monetary credit charged at destination-specific rates. Single monetary balance used for ALL usage types. CGRateS looks up the rate for each destination and deducts cost from the credit.
Note: PAYG requires defining Rate Profiles for each destination to set the dollar amount per unit. See Rate Profiles for PAYG/Monetary Balances for complete configuration.
Example: $50 PAYG Credit
Action_PAYG_Credit = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_payg-50-credit",
"Overwrite": True,
"Tenant": tenant,
"Actions": [
# $50 monetary balance
{
"Identifier": "*topup",
"BalanceId": "PAYG_Monetary_Balance",
"BalanceType": "*monetary",
"DestinationIDs": "*any", # Works for ANY destination
"Units": 5000, # $50.00 (in cents)
"ExpiryTime": "+2160h", # 90 days
"BalanceWeight": 1000, # Lower than unitary balances
"Weight": 90
},
{
"Identifier": "*cdrlog",
"BalanceType": "*generic",
"ExtraParameters": "{\"Category\":\"^activation\",\"Destination\":\"$50 PAYG Credit\"}",
"Weight": 80
}
]
}]
}
result = OCS_Obj.SendData(Action_PAYG_Credit)
How PAYG Works:
Scenario 1: Domestic call, 10 minutes
- Rate: $0.10/min
- Charge: 10 × $0.10 = $1.00
- Remaining: $49.00
Scenario 2: Call to UK, 5 minutes
- Rate: $0.25/min + $0.05 connect fee
- Charge: (5 × $0.25) + $0.05 = $1.30
- Remaining: $47.70
Scenario 3: Roaming on Verizon, 100MB data
- Rate: $2.00/MB
- Charge: 100 × $2.00 = $200.00
- Result: Insufficient funds → Session blocked at ~$47 worth (~23MB)
Pros:
- One balance for everything - very flexible
- Customer can use service anywhere rates are defined
- Simple configuration
Cons:
- Unpredictable costs for customers
- Bill shock risk (especially roaming)
- Can be expensive for heavy users
Approach 3: Hybrid (Best of Both)
Concept: Combine unitary balances with monetary fallback. Use high-weight unitary balances for included usage, with low-weight monetary balance as overflow. Best customer experience with predictable base cost and flexible overages.
Example: Hybrid Flex Plan
Action_Hybrid_Plan = {
"id": "0",
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_hybrid-flex-plan",
"Overwrite": True,
"Tenant": tenant,
"Actions": [
{
"Identifier": "*reset_account",
"Weight": 700
},
# ============= UNITARY BALANCES (Included) =============
# 500 domestic voice minutes
{
"Identifier": "*topup_reset",
"BalanceId": f"Domestic_Voice__{500 * 60 * 1000000000}",
"BalanceType": "*voice",
"DestinationIDs": "Dest_Domestic_All",
"Units": 500 * 60 * 1000000000,
"ExpiryTime": "*month",
"BalanceWeight": 1200, # Consumed FIRST for domestic calls
"Weight": 95
},
# 100 international voice minutes
{
"Identifier": "*topup_reset",
"BalanceId": f"International_Voice__{100 * 60 * 1000000000}",
"BalanceType": "*voice",
"DestinationIDs": "Dest_International_All",
"Units": 100 * 60 * 1000000000,
"ExpiryTime": "*month",
"BalanceWeight": 1150, # Consumed FIRST for international
"Weight": 94
},
# 1000 domestic SMS
{
"Identifier": "*topup_reset",
"BalanceId": "Domestic_SMS__1000",
"BalanceType": "*sms",
"DestinationIDs": "Dest_Domestic_All",
"Units": 1000,
"ExpiryTime": "*month",
"BalanceWeight": 1200,
"Weight": 93
},
# 15GB domestic data
{
"Identifier": "*topup_reset",
"BalanceId": f"Domestic_Data__{15 * 1024 * 1024 * 1024}",
"BalanceType": "*data",
"DestinationIDs": "Dest_PLMN_OnNet",
"Units": 15 * 1024 * 1024 * 1024,
"ExpiryTime": "*month",
"BalanceWeight": 1200,
"Weight": 92
},
# 2GB roaming data (Zone 1)
{
"Identifier": "*topup_reset",
"BalanceId": f"Roaming_Zone1_Data__{2 * 1024 * 1024 * 1024}",
"BalanceType": "*data",
"DestinationIDs": "Dest_PLMN_Zone_NorthAmerica",
"Units": 2 * 1024 * 1024 * 1024,
"ExpiryTime": "*month",
"BalanceWeight": 1100,
"Weight": 91
},
# ============= MONETARY BALANCE (Overflow/PAYG) =============
# $20 for overages
{
"Identifier": "*topup",
"BalanceId": "PAYG_Overflow_Balance",
"BalanceType": "*monetary",
"DestinationIDs": "*any",
"Units": 2000, # $20.00
"ExpiryTime": "*month",
"BalanceWeight": 1000, # Consumed LAST (fallback)
"Weight": 90
},
{
"Identifier": "*cdrlog",
"BalanceType": "*generic",
"ExtraParameters": "{\"Category\":\"^activation\",\"Destination\":\"Hybrid Flex Plan\"}",
"Weight": 80
}
]
}]
}
result = OCS_Obj.SendData(Action_Hybrid_Plan)
Balance Consumption Flow:
Example Scenarios:
Scenario 1: 600 domestic minutes used
- First 500 minutes → Uses "Domestic_Voice_500min" (weight 1200) - FREE
- Next 100 minutes → "Domestic_Voice_500min" exhausted, falls back to "PAYG_Overflow_Balance" (weight 1000)
- Charged: 100 min × $0.10 = $10.00 from monetary balance
- Remaining: $10.00 monetary
Scenario 2: 150 international minutes (to UK)
- First 100 minutes → Uses "International_Voice_100min" (weight 1150) - FREE
- Next 50 minutes → Falls back to "PAYG_Overflow_Balance"
- Charged: (50 × $0.25) + $0.05 connect = $12.55
- Remaining: $7.45 monetary (if starting with $20)
Pros:
- Predictable base cost
- Flexibility for occasional overages
- Best customer experience
- No hard cutoff - service continues
Cons:
- More complex to configure
- More complex to explain to customers
- Requires careful balance weight management
Real-World Usage Scenarios
These scenarios demonstrate how the concepts come together in practice. Each shows the complete flow from usage event to balance deduction or blocking.
Scenario 1: Customer at Home Calls UK
Customer has: Domestic Plan (500min domestic, 1000 SMS, 10GB data)
Action: Calls UK number (44-20-7946-0958) for 10 minutes
Balance Check:
- CGRateS receives call to
44207946... - Matches destination:
Dest_International_UK - Checks for balance with
DestinationIDs: "Dest_International_UK" - NO matching balance found
- Checks for monetary balance
- NO monetary balance found
- Result: Call BLOCKED (insufficient balance)
Solution: Customer needs International Plan OR PAYG credit to make UK calls
Scenario 2: Customer Roaming in US Uses Data
Customer has: Domestic Plan + US Roaming 5GB addon
Action: Roaming on Verizon (PLMN mcc310.mnc004), uses 2GB data
Balance Check:
- CGRateS receives data session on PLMN
mcc310.mnc004 - Matches destination:
Dest_PLMN_US_Verizon - Checks for data balance with
DestinationIDs: "Dest_PLMN_US_All"(broader match) - Finds: "Roaming_US_Data_5GB" balance (BalanceWeight: 1100)
- Deducts 2GB
- Remaining: 3GB roaming data
What if they used 6GB?
- First 5GB → Uses roaming balance (exhausted)
- Next 1GB → Checks for monetary balance
- NO monetary balance → Session blocked at 5GB
Scenario 3: Customer with Hybrid Plan Exceeds Domestic Minutes
Customer has: Hybrid Plan (500 domestic min + $20 overflow)
Action: Makes 600 minutes of domestic calls
Balance Check:
- First 500 minutes:
- Uses "Domestic_Voice_500min" (BalanceWeight: 1200)
- No charge
- "Domestic_Voice_500min" exhausted
- Next 100 minutes:
- Falls back to "PAYG_Overflow_Balance" (BalanceWeight: 1000)
- Rate: $0.10/min
- Charge: 100 × $0.10 = $10.00
- Deducted from $20 monetary balance
- Remaining: $10.00
Billing:
- Customer sees: 500 included minutes + 100 overage minutes ($10.00)
Scenario 4: PAYG User Roams Unexpectedly
Customer has: $50 PAYG credit
Action: Travels to US, roams on Verizon, uses 100MB data (unknowingly)
Charge Calculation:
- 100MB on Verizon
- Rate: $2.00/MB (from PAYG rates)
- Total charge: 100 × $2.00 = $200.00
- Available: $50.00
- Result: Customer uses ~25MB before credit exhausted, session blocked
- Bill shock! Customer unexpectedly consumed entire $50
Better approach: Recommend roaming addon to avoid bill shock
Best Practices
1. Use Descriptive Balance IDs
Good Balance IDs are self-documenting:
# Good
"Data_5GB_5days_{{ uuid }}"
"Voice_100min_Monthly"
"Bonus_Data_Loyalty"
# Bad
"balance1"
"data"
"temp"
2. Document Your Weight Strategy
Define a consistent weight scheme across all products:
# Weight scheme:
# 30 = Premium/Promotional balances
# 20 = Bonus/Loyalty balances
# 10 = Regular/Purchased balances
# 5 = Backup/Fallback balances
3. Include CDR Logging
Always log balance additions for audit trails:
{
"Identifier": "*cdrlog",
"BalanceType": "*generic",
"ExtraParameters": "{\"Category\":\"^activation\",\"Destination\":\"{{ package_name }}\"}"
}
4. Use ActionPlans for Recurring Operations
For monthly resets, don't run the Action manually - use ActionPlans:
# Create ActionPlan that runs monthly
- name: Create monthly ActionPlan
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.SetActionPlan",
"params": [{
"Id": "ActionPlan_Monthly_Reset",
"ActionPlan": [{
"ActionsId": "Action_Monthly_Reset",
"Time": "*monthly",
"Weight": 10
}],
"ReloadScheduler": true
}]
}
5. Reset ActionTriggers After Topup
After executing an action, reset triggers to prevent duplicate notifications:
- name: Reset ActionTriggers
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "APIerSv1.ResetAccountActionTriggers",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"Executed": false
}]
}
6. Set Appropriate Balance Weights
Use a consistent weight strategy across all products:
# Consumption priority (highest to lowest)
BalanceWeight: 1200 # Domestic included (use first at home)
BalanceWeight: 1150 # International included (use first for intl)
BalanceWeight: 1100 # Roaming included (use first when roaming)
BalanceWeight: 1000 # Monetary fallback (use last)
This ensures predictable consumption order: domestic balances first, then international, then roaming, and finally monetary PAYG as fallback.
7. Control Balance Usage with DestinationIDs
Always use specific DestinationIDs to prevent unintended usage:
# Good - explicit destination control
{
"BalanceType": "*voice",
"DestinationIDs": "Dest_International_UK", # ONLY UK
"Units": 200 * 60 * 1000000000
}
# Bad - unintended usage
{
"BalanceType": "*voice",
"DestinationIDs": "*any", # Could be used for premium numbers!
"Units": 200 * 60 * 1000000000
}
8. Group Destinations Logically
Create destination groups that make sense for your product offering:
# Good - logical grouping
"Dest_International_Europe" = All EU countries (shared 500min pool)
# Bad - too granular
"Dest_France", "Dest_Germany", "Dest_Italy" (separate small pools)
See CGRateS Destinations for destination grouping examples.
9. Include Monetary Fallback in Plans
For the best customer experience, include a small monetary balance as overflow:
# Recommended: Always include small monetary balance
{
"BalanceType": "*monetary",
"Units": 1000, # $10 overflow protection
"BalanceWeight": 1000 # Lower than unitary balances
}
This prevents hard service cutoffs and provides flexibility for occasional overages.
10. Use Rate Increments Strategically
Choose appropriate rate increments for different service types:
# Domestic: Customer-friendly per-second
"RateIncrement": "1s"
# International: Standard 6-second blocks
"RateIncrement": "6s"
# Premium: 30-second blocks to discourage abuse
"RateIncrement": "30s"
# Data: Per KB for accuracy
"RateIncrement": "1024"
11. Set Reasonable Expiry Times
Match expiry times to product types:
# Monthly subscription plans
"ExpiryTime": "*month"
# One-time topups/addons
"ExpiryTime": "+720h" # 30 days
# PAYG credit (longer validity)
"ExpiryTime": "+2160h" # 90 days
# Roaming addons (trip-based)
"ExpiryTime": "+360h" # 15 days
Troubleshooting
Issue: Balance Not Added
Symptoms: Action executes successfully but balance doesn't appear
Possible Causes:
- Wrong Account UUID
- BalanceType mismatch
- Expiry already passed
Solution: Verify account exists and check balance expiry time
Issue: Wrong Balance Consumed
Symptoms: System consuming from unexpected balance
Possible Causes:
- Weight configuration incorrect
- Multiple balances with same ID
Solution: Review weight values and ensure Balance IDs are unique if independence is desired
Issue: Rollover Not Working
Symptoms: Using *topup but old balance not rolling over
Possible Causes:
- Using different Balance IDs (need same ID)
- Action using
*topup_resetinstead of*topup
Solution: Verify Balance ID consistency and action type
Issue: Balance Expires Immediately
Symptoms: Balance added but shows as expired
Possible Causes:
- ExpiryTime in past
- Using absolute timestamp instead of relative
Solution: Use relative expiry (+Xh) instead of absolute timestamps
Issue: Balance Not Being Consumed
Symptom: Customer has balance but still blocked or charged PAYG
Possible Causes:
- DestinationIDs mismatch - Balance destinations don't match usage destination
- Balance expired
- Wrong balance type for usage
Debug Steps:
# Check account balances
OCS_Obj.SendData({
'method': 'ApierV2.GetAccount',
'params': [{"Tenant": tenant, "Account": "service_uuid"}]
})
# Check what's returned:
# - Balance exists?
# - DestinationIDs correct?
# - ExpiryTime in future?
# - Units > 0?
Solution: Verify destination matches and balance is active. See Scenario 1: Customer at Home Calls UK for example.
Issue: Wrong Rate Applied
Symptom: Customer charged incorrect amount for PAYG usage
Possible Causes:
- Destination overlap (wrong precedence in RatingPlan)
- Rating plan binding weights incorrect
- Rate definition has wrong values
Solution:
- Ensure specific destinations have higher weight in RatingPlan bindings
- Check longest prefix match is working correctly
- Verify rate values and increments are correct
See Rate Profiles for PAYG/Monetary Balances for proper configuration.
Issue: Roaming Balance Not Used
Symptom: Customer roaming, has roaming balance, but charged PAYG or blocked
Possible Causes:
- PLMN destination mismatch - Balance doesn't include visited PLMN
- Customer connected to wrong/unexpected network
- DestinationIDs don't match the actual PLMN
Solution:
# Check which PLMN customer is on (from CDRs or usage events)
# Verify balance includes that PLMN:
"DestinationIDs": "Dest_PLMN_US_All" # Should include mcc310.mnc004
See CGRateS Destinations for PLMN destination configuration and Scenario 2: Customer Roaming in US Uses Data for working example.
Issue: Hybrid Plan Not Falling Back to Monetary
Symptom: Unitary balance exhausted but monetary balance not being used
Possible Causes:
- Monetary balance has DestinationIDs restriction (should be
*any) - No PAYG rates defined for that destination
- Balance weight issue - monetary balance weight not lower than unitary
Solution:
- Ensure monetary balance has
DestinationIDs: "*any" - Verify PAYG rates defined for all destinations customer might use
- Check balance weights: monetary should be lowest (e.g., 1000)
See Approach 3: Hybrid for proper hybrid configuration.
Issue: Unexpected International Charges
Symptom: Customer called domestic number, charged international rates
Possible Causes:
- Number actually international (e.g., Canada +1 number treated as international)
- Prefix overlap in destination definitions
- Customer misdialed (added extra digits)
Solution:
- Check CDRs for actual dialed number
- Verify destination prefix definitions don't overlap incorrectly
- Consider separate Canada destination if NANP countries need different rates
See CGRateS Destinations for geographic destination configuration.
Related Documentation
Core Concepts
- Defining Products - Complete workflow for creating products, defining CGRateS Actions, and linking them together
- CGRateS Destinations - How to define geographic and PLMN destinations for voice, SMS, and data services
Implementation & Operations
- Charging and Payments from Playbooks - Two-phase commit payment flow, pro-rata calculations, and charging customers
- Ansible Playbooks Guide - Playbook structure, provisioning flows, and deprovisioning with rescue blocks
- Provisioning System Overview - How provisioning works in OmniCRM