Skip to main content

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.

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:

  1. During Initial Setup - When first configuring your system
  2. When Creating New Products - Define the Action before creating the product
  3. 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:

  1. 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
  2. Generate Human-Readable Descriptive Name:

    # Replace underscores with spaces
    id_hr = descriptive_name.replace("_", " ") # "AU Data Domestic"
  3. 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"
  4. Calculate Usage Percentage:

    if original_size and original_size > 0:
    percent_used = ((original_size - current_value) / original_size) * 100
  5. 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:

  1. Original Size Tracking - Even when balances are partially consumed, the UI can show "50 GB of 100 GB" instead of just "50 GB remaining"

  2. Progress Visualization - Percentage calculations enable accurate progress bars

  3. Consistent Naming - Descriptive names extracted from Balance IDs ensure consistency between backend and frontend

  4. 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)"

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:

  1. While blocker has value → Usage is allowed up to the blocker amount
  2. When blocker reaches zero → All usage stops, even if other balances exist
  3. Error returnedINSUFFICIENT_CREDIT_BALANCE_BLOCKER prevents 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:

  1. Customer uses 10GB included → FREE (from Included_Data_10GB)
  2. Customer uses additional 5GB → Charged from Overage_Cap (PAYG rates)
  3. 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:

FeatureBlockerDisabled
PurposeStop usage when balance is exhaustedTemporarily pause a balance
When value > 0Balance is usable normallyBalance is skipped/ignored
When value = 0Blocks all further usageBalance is skipped (next balance tried)
Use CaseSpending limits, caps, trial limitsTemporarily 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:

  1. 0-500 minutes: Uses Domestic_Voice_500min (FREE)
  2. 500-700 minutes: Uses Overage_Allowance at $0.10/min = $20 (200 min)
  3. 700-1200 minutes: Uses Hard_Spending_Cap at $0.10/min = $50 (500 min)
  4. 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

  1. Use High Weight for Blockers

    "BalanceWeight": 9999  # Ensure blocker is checked first
  2. Zero-Value Blockers for Immediate Blocking

    "Units": 0,  # Block immediately
    "Blocker": True
  3. 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
  4. Remove Blockers When Unsuspending

    # Use *remove_balance to delete the blocker
    {
    "Identifier": "*remove_balance",
    "BalanceId": "Suspension_Blocker"
    }
  5. Test Blocker Behavior

    • Verify blocker returns INSUFFICIENT_CREDIT_BALANCE_BLOCKER error
    • Confirm CDRs show cost = -1.0 when blocked
    • Test that other balances are NOT used after blocker exhaustion

Troubleshooting Blockers

Issue: Usage not blocked despite blocker at zero

Possible Causes:

  1. Blocker weight too low (other balances checked first)
  2. DestinationIDs don't match usage destination
  3. 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 month
  • 2024-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_account to clear old balances first
  • Uses *topup_reset for fixed monthly allowances (no rollover)
  • BalanceWeight determines consumption order (domestic 1200 > roaming 1100)
  • Weight determines execution order within the Action
  • Includes *cdrlog for 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")
  • 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:

FeatureUnitaryMonetary (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 ForPredictable usersOccasional usersMost 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

  1. First 500 minutes → Uses "Domestic_Voice_500min" (weight 1200) - FREE
  2. Next 100 minutes → "Domestic_Voice_500min" exhausted, falls back to "PAYG_Overflow_Balance" (weight 1000)
  3. Charged: 100 min × $0.10 = $10.00 from monetary balance
  4. Remaining: $10.00 monetary

Scenario 2: 150 international minutes (to UK)

  1. First 100 minutes → Uses "International_Voice_100min" (weight 1150) - FREE
  2. Next 50 minutes → Falls back to "PAYG_Overflow_Balance"
  3. Charged: (50 × $0.25) + $0.05 connect = $12.55
  4. 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:

  1. CGRateS receives call to 44207946...
  2. Matches destination: Dest_International_UK
  3. Checks for balance with DestinationIDs: "Dest_International_UK"
  4. NO matching balance found
  5. Checks for monetary balance
  6. NO monetary balance found
  7. 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:

  1. CGRateS receives data session on PLMN mcc310.mnc004
  2. Matches destination: Dest_PLMN_US_Verizon
  3. Checks for data balance with DestinationIDs: "Dest_PLMN_US_All" (broader match)
  4. Finds: "Roaming_US_Data_5GB" balance (BalanceWeight: 1100)
  5. Deducts 2GB
  6. Remaining: 3GB roaming data

What if they used 6GB?

  1. First 5GB → Uses roaming balance (exhausted)
  2. Next 1GB → Checks for monetary balance
  3. 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:

  1. First 500 minutes:
    • Uses "Domestic_Voice_500min" (BalanceWeight: 1200)
    • No charge
  2. "Domestic_Voice_500min" exhausted
  3. 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:

  1. 100MB on Verizon
  2. Rate: $2.00/MB (from PAYG rates)
  3. Total charge: 100 × $2.00 = $200.00
  4. Available: $50.00
  5. Result: Customer uses ~25MB before credit exhausted, session blocked
  6. 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_reset instead 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:

  1. DestinationIDs mismatch - Balance destinations don't match usage destination
  2. Balance expired
  3. 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:

  1. Destination overlap (wrong precedence in RatingPlan)
  2. Rating plan binding weights incorrect
  3. 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:

  1. PLMN destination mismatch - Balance doesn't include visited PLMN
  2. Customer connected to wrong/unexpected network
  3. 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:

  1. Monetary balance has DestinationIDs restriction (should be *any)
  2. No PAYG rates defined for that destination
  3. 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:

  1. Number actually international (e.g., Canada +1 number treated as international)
  2. Prefix overlap in destination definitions
  3. 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.

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