Skip to main content

Charging and Payments from Playbooks

This guide explains how to implement charging and payment processing within Ansible playbooks for OmniCRM provisioning workflows.

πŸ“˜ Complete API Reference: For full details on payment APIs, wallet routing, refunds, and vendor-agnostic architecture, see Payment System API Guide

Overview​

OmniCRM Ansible Playbooks can handle payment processing in a number of different ways, but all of them are just calling API calls on the CRM to charge.

  1. Two-Phase Commit Payment Flow - For paid services requiring immediate payment authorization and capture (ie Prepaid Services)
  2. Direct Transaction Creation - For adding charges/credits without immediate payment processing (e.g., setup fees, manual credits) which can later be processed (ie Postpad services)

These approaches ensure atomic transactions where customers are only charged if provisioning succeeds, with automatic rollback if any step fails.

Pricing Flexibility: Playbooks as a Scripting Engine​

Important: The dollar values defined in products and services (e.g., retail_cost, wholesale_cost, retail_setup_cost) are simply default values stored in the database. They do not dictate what you must chargeβ€”the playbook has complete control over the final pricing.

The Ansible playbook is essentially a scripting engine that you can tailor to meet any business logic requirements. You can:

  • Use the stored prices as-is - Simply reference api_response_product.json.retail_cost in your authorization
  • Override prices completely - Charge a different amount regardless of what's in the product definition
  • Apply dynamic discounts - Calculate percentages off based on customer tier, promotions, or loyalty
  • Implement tiered pricing - Charge different rates based on quantity, usage levels, or contract terms
  • Bundle pricing - Combine multiple products with custom bundle discounts
  • Time-based pricing - Adjust prices based on time of day, season, or promotional periods
  • Customer-specific pricing - Look up negotiated rates from customer contracts

Example: Overriding Product Price​

# Product retail_cost is $99.00, but we want to charge $79.00 for a promotion
- name: Set promotional price (ignoring product's retail_cost)
set_fact:
charge_amount: 79.00

- name: Authorize promotional payment
uri:
url: "http://localhost:5000/crm/payments/authorize/hold"
method: POST
body_format: json
body:
customer_id: "{{ customer_id | int }}"
amount: "{{ charge_amount | float }}" # Using our override, not retail_cost
# ...

Example: Customer Tier Discount​

# Apply discount based on customer tier
- name: Get customer tier
uri:
url: "http://localhost:5000/crm/customer/{{ customer_id }}"
# ...
register: customer_info

- name: Calculate tier discount
set_fact:
base_price: "{{ api_response_product.json.retail_cost | float }}"
discount_percent: >-
{% if customer_info.json.tier == 'gold' %}20
{% elif customer_info.json.tier == 'silver' %}10
{% else %}0{% endif %}

- name: Apply discount to final price
set_fact:
final_price: "{{ (base_price | float) * (1 - (discount_percent | float / 100)) | round(2) }}"

Example: Wholesale Partner Pricing​

# Wholesale partners pay wholesale_cost instead of retail_cost
- name: Determine price based on customer type
set_fact:
charge_amount: >-
{% if customer_info.json.is_wholesale_partner %}
{{ api_response_product.json.wholesale_cost | float }}
{% else %}
{{ api_response_product.json.retail_cost | float }}
{% endif %}

The key takeaway is that you are not locked into any pricing model. The playbook gives you full programmatic control to implement whatever business rules your organization requires. The product/service prices in the CRM are just convenient defaults that playbooks can use or ignore as needed.

Two-Phase Commit Payment Flow​

The two-phase commit pattern is used when you need to charge a customer's payment method during provisioning. This ensures the customer is only charged if provisioning completes successfully.

Flow Overview​

Implementation Pattern​

The pattern follows these phases:

Phase 1: Authorization - Hold funds on the payment method Phase 2: Provisioning - Execute OCS balance/action updates Phase 3: Capture - Finalize payment upon successful provisioning Rollback - Release authorization if provisioning fails

Complete Example​

Here's a complete example from play_topup_charge_then_action.yaml:

- name: Play Topup - Charge card then action the Topup
hosts: localhost
gather_facts: no
become: False

tasks:
- name: Include vars of crm_config
ansible.builtin.include_vars:
file: "../../crm_config.yaml"
name: crm_config

# Get product and service information
- name: Get Product information from CRM API
uri:
url: "http://localhost:5000/crm/product/product_id//{{ product_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
validate_certs: no
register: api_response_product

- name: Get Service information from CRM API
uri:
url: "http://localhost:5000/crm/service/service_id/{{ service_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
validate_certs: no
register: api_response_service

- name: Set service and package facts
set_fact:
service_uuid: "{{ api_response_service.json.service_uuid }}"
customer_id: "{{ api_response_service.json.customer_id }}"
package_name: "{{ api_response_product.json.product_name }}"
monthly_cost: "{{ api_response_product.json.retail_cost }}"
wholesale_cost: "{{ api_response_product.json.wholesale_cost }}"

# Get customer's default payment method
- name: Get the Customer Payment Methods from Payment Controller
uri:
url: "http://localhost:5000/crm/payments/methods?customer_id={{ customer_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
validate_certs: no
register: api_response_payment_methods

- name: Get the default payment_method_id from the response
set_fact:
payment_method_id: "{{ api_response_payment_methods.json | json_query(query) }}"
vars:
query: "data[?is_default==`true`].payment_method_id | [0]"

# ============================================================
# TWO-PHASE COMMIT PAYMENT FLOW
# ============================================================

- name: "Phase 1: Authorize payment (hold funds)"
uri:
url: "http://localhost:5000/crm/payments/authorize/hold"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": "{{ customer_id | int }}",
"amount": "{{ monthly_cost | float }}",
"currency": "{{ crm_config.currency | default('AUD') }}",
"payment_method_id": "{{ payment_method_id | int }}",
"metadata": {
"description": "{{ package_name }} on {{ api_response_service.json.service_name }}",
"service_id": "{{ api_response_service.json.service_id | int }}",
"site_id": "{{ api_response_service.json.site_id | int }}",
"product_id": "{{ product_id }}",
"user_id": "{{ (initiating_user | int) if (initiating_user is defined and initiating_user is not none) else omit }}",
"title": "{{ package_name }}",
"wholesale_cost": "{{ wholesale_cost | float }}",
"invoice": true,
"contract_days": "{{ days | int }}",
"send_email": true
}
}
return_content: yes
register: api_response_authorization

- name: Assert authorization was successful
assert:
that:
- api_response_authorization.status == 200
- api_response_authorization.json.success == true

- name: Store authorization_id for capture/release
set_fact:
authorization_id: "{{ api_response_authorization.json.data.authorization_id }}"

# Phase 2: OCS Provisioning (wrapped in block for transactional rollback)
- block:
- name: "Phase 2: Execute CGRateS action"
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body:
{
"id": "{{ 999999999 | random }}",
"method": "APIerSv1.ExecuteAction",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ActionsId": "{{ cgr_action_name }}"
}]
}
status_code: 200
register: action_execute_response

- name: Assert that the response from the action is OK
assert:
that:
- action_execute_response.status == 200
- action_execute_response.json.result == "OK"

# Phase 3: Payment Capture - Finalize transaction after successful provisioning
- name: "Phase 3: Capture authorized payment"
uri:
url: "http://localhost:5000/crm/payments/capture/{{ authorization_id }}"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"metadata": {
"provisioning_status": "success",
"cgr_action": "{{ cgr_action_name }}"
}
}
return_content: yes
register: api_response_capture

- name: Assert capture was successful
assert:
that:
- api_response_capture.status == 200
- api_response_capture.json.success == true

rescue:
# Transaction Rollback: Void authorization to release held funds
- name: "Rollback: Release payment authorization"
uri:
url: "http://localhost:5000/crm/payments/release/{{ authorization_id }}"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"metadata": {
"release_reason": "provisioning_failed",
"cgr_action": "{{ cgr_action_name }}"
}
}
return_content: yes
register: api_response_release

- name: Terminate playbook execution after rollback
fail:
msg: "OCS provisioning failed. Payment authorization {{ authorization_id }} voided. Customer not charged."

Payment API Endpoints​

πŸ’‘ Wallet-First Routing: The payment system automatically optimizes card charges by using wallet balance first. If a customer has $1 in their wallet and purchases a $10 addon, only $9 is charged to their card. See Wallet-First Routing section for details.

1. Authorize/Hold Payment​

Endpoint: POST /crm/payments/authorize/hold

This endpoint places a hold on the customer's payment method for the specified amount. Funds are reserved but not yet captured.

Wallet-First Behavior: The authorization automatically calculates the shortfall after checking wallet balance. If wallet balance is $150 and authorization is for $500, only $350 is authorized on the card. The wallet is debited at capture time.

Request Body:

{
"customer_id": 123,
"amount": 49.99,
"currency": "AUD",
"payment_method_id": 456,
"metadata": {
"description": "Package name and description",
"service_id": 789,
"site_id": 12,
"product_id": "34",
"user_id": 5,
"title": "Package Name",
"wholesale_cost": 25.00,
"invoice": true,
"contract_days": 30,
"send_email": true
}
}

Field Descriptions:

  • customer_id (required) - Customer ID being charged
  • amount (required) - Amount to authorize in decimal format
  • currency (optional) - Currency code (defaults to system default)
  • payment_method_id (required) - Payment method to charge
  • metadata (optional) - Additional data for transaction creation:
    • description - Transaction description
    • service_id - Service being charged for
    • site_id - Site associated with service
    • product_id - Product being provisioned
    • user_id - User initiating the charge (optional)
    • title - Transaction title
    • wholesale_cost - Your cost (for margin tracking)
    • invoice - If true, automatically create transaction when captured
    • contract_days - Contract duration
    • send_email - If true, send email notification

Response:

{
"success": true,
"message": "Payment authorized (hold created)",
"data": {
"authorization_id": 301,
"vendor_authorization_id": "auth_xxxxx",
"amount": 500.00,
"currency": "USD",
"status": "authorized",
"wallet_balance": 150.00,
"wallet_to_use": 150.00,
"card_amount": 350.00,
"message": "Card authorized for $350 (wallet top-up). Wallet debit of $500 will occur at capture."
}
}

Important:

  • Save the authorization_id for use in capture or release calls
  • Note the card_amount shows only the shortfall was authorized
  • Wallet balance is checked but NOT debited until capture

2. Capture Payment​

Endpoint: POST /crm/payments/capture/{authorization_id}

This endpoint finalizes the payment authorization and charges the customer. If invoice: true was set in the authorization metadata, a transaction is automatically created.

Request Body:

{
"metadata": {
"provisioning_status": "success",
"cgr_action": "Action_Topup_Standard",
"additional_info": "Any other relevant data"
}
}

Response:

{
"success": true,
"data": {
"payment_id": "pay_xyz789",
"transaction_id": 1234
}
}

What Happens:

  1. Card captured for shortfall amount (if card was authorized)
  2. Wallet credited with captured card amount
  3. Wallet debited for full service amount
  4. If invoice: true was in authorization metadata:
    • Debit transaction created (positive amount = charge)
    • Invoice created and linked to debit transaction
    • Credit transaction created (negative amount = payment received)
    • Invoice marked as PAID (debits and credits net to zero)
    • Transaction appears in customer's billing
  5. Payment record is created in the database
  6. If send_email: true, customer receives invoice email

Example: $500 authorization with $150 wallet balance:

  • Card captured: $350
  • Wallet credited: +$350 (wallet now $500)
  • Wallet debited: -$500 (service charge)
  • Final wallet: $0

See Wallet-First Routing Examples for detailed flow.

3. Release Payment Authorization​

Endpoint: POST /crm/payments/release/{authorization_id}

This endpoint cancels a payment authorization and releases the held funds. Use this in rescue/rollback scenarios when provisioning fails.

Request Body:

{
"metadata": {
"release_reason": "provisioning_failed",
"error_details": "OCS account creation failed"
}
}

Response:

{
"success": true,
"message": "Authorization released"
}

What Happens:

  1. Card authorization released (if card was authorized)
  2. Held funds returned to customer's available credit
  3. Wallet NOT debited (since debit only happens at capture)
  4. Customer is NOT charged
  5. No transaction is created

Note: With wallet-first routing, no wallet refund is needed since wallet is not debited until capture time.

Direct Transaction Creation​

For charges that don't require payment processing (setup fees, manual credits, adjustments), you can create transactions directly via the API.

Adding a Transaction via API​

Endpoint: PUT /crm/transaction/

Example from play_simple_service.yaml:

- name: Add Setup Cost Transaction via API
uri:
url: "http://localhost:5000/crm/transaction/"
method: PUT
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": {{ customer_id | int }},
"service_id": {{ service_creation_response.json.service_id | int }},
"title": "{{ package_name }} - Setup Costs",
"description": "Setup Costs for {{ package_comment }}",
"invoice_id": null,
"wholesale_cost": {{ api_response_product.json.wholesale_setup_cost | float }},
"retail_cost": "{{ api_response_product.json.retail_setup_cost | float }}"
}
return_content: yes
register: api_response_transaction

- name: Assert that the response from the transaction is OK
assert:
that:
- api_response_transaction.status == 200

Request Body Fields:

  • customer_id (required) - Customer ID
  • service_id (optional) - Service ID to link transaction to
  • title (required) - Short transaction name
  • description (optional) - Detailed description
  • invoice_id (optional) - Invoice ID if already invoiced (usually null)
  • wholesale_cost (optional) - Your cost
  • retail_cost (required) - Customer-facing cost
  • site_id (optional) - Site ID to link transaction to
  • tax_percentage (optional) - Tax rate percentage

Use Cases:

  • Setup fees during service provisioning
  • Installation charges
  • Manual credits or adjustments
  • Equipment charges
  • Administrative fees

Note: Direct transaction creation does NOT process payment - it only creates a billing record. The transaction will appear as uninvoiced and can be included in future invoices.

Calculating Pro-Rata Charges​

Pro-rata charging allows you to charge customers proportionally based on partial billing periods. This is common when:

  • Customer signs up mid-month
  • Service is upgraded/downgraded mid-cycle
  • Billing is calculated based on days used

Pro-Rata Calculation Formula​

pro_rata_charge = (monthly_cost Γ— days_remaining) / days_in_month

Implementation Example​

Here's how to calculate a pro-rata charge in a playbook:

# Calculate pro-rata charge for partial month
# If customer signs up on the 15th and billing is on 1st,
# charge proportionally for remaining days

- name: Get current day of month
command: "date +%d"
register: current_day

- name: Get total days in current month
command: "date -d 'last day of this month' +%d"
register: days_in_month

- name: Get last day of month
command: "date -d 'last day of this month' +%Y-%m-%d"
register: last_day_of_month

- name: Calculate days remaining in month
set_fact:
days_remaining: "{{ (days_in_month.stdout | int) - (current_day.stdout | int) + 1 }}"

- name: Calculate pro-rata cost
set_fact:
pro_rata_cost: "{{ ((monthly_cost | float) * (days_remaining | float) / (days_in_month.stdout | float)) | round(2) }}"

- name: Display calculation details
debug:
msg:
- "Monthly cost: ${{ monthly_cost }}"
- "Days in month: {{ days_in_month.stdout }}"
- "Days remaining: {{ days_remaining }}"
- "Pro-rata charge: ${{ pro_rata_cost }}"

# Use pro_rata_cost in authorization or transaction creation
- name: "Authorize pro-rata payment"
uri:
url: "http://localhost:5000/crm/payments/authorize/hold"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": "{{ customer_id | int }}",
"amount": "{{ pro_rata_cost | float }}",
"currency": "{{ crm_config.currency | default('AUD') }}",
"payment_method_id": "{{ payment_method_id | int }}",
"metadata": {
"title": "{{ package_name }} (Pro-rata {{ days_remaining }} days)",
"description": "Pro-rated charge for {{ days_remaining }}/{{ days_in_month.stdout }} days of {{ package_name }}",
"service_id": "{{ service_id | int }}",
"invoice": true
}
}

Pro-Rata from Custom Start Date​

If you need to calculate pro-rata from a specific start date to a billing date:

- name: Set custom start date and billing date
set_fact:
service_start_date: "2024-01-15"
next_billing_date: "2024-02-01"

- name: Calculate days between dates
shell: |
echo $(( ( $(date -d "{{ next_billing_date }}" +%s) - $(date -d "{{ service_start_date }}" +%s) ) / 86400 ))
register: days_until_billing

- name: Get days in billing period (usually 30)
set_fact:
billing_period_days: 30

- name: Calculate pro-rata cost
set_fact:
pro_rata_cost: "{{ ((monthly_cost | float) * (days_until_billing.stdout | float) / (billing_period_days | float)) | round(2) }}"

- name: Display calculation
debug:
msg:
- "Start date: {{ service_start_date }}"
- "Next billing: {{ next_billing_date }}"
- "Days until billing: {{ days_until_billing.stdout }}"
- "Pro-rata charge: ${{ pro_rata_cost }}"

Pro-Rata Example Scenarios​

Scenario 1: Mid-Month Sign-Up

  • Customer signs up on January 15th
  • Monthly cost: $60.00
  • Days in January: 31
  • Days remaining: 17 (15th to 31st inclusive)
  • Pro-rata charge: $60.00 Γ— 17 Γ· 31 = $32.90

Scenario 2: Service Upgrade

  • Customer upgrades on the 10th day of billing cycle
  • Old plan: $30/month
  • New plan: $50/month
  • Days in cycle: 30
  • Days remaining: 21
  • Difference: $20/month
  • Pro-rata charge: $20.00 Γ— 21 Γ· 30 = $14.00

Best Practices​

1. Always Use Block/Rescue Pattern​

Wrap payment capture and provisioning in a block/rescue to ensure rollback:

- block:
# Provisioning tasks
- name: Provision service
uri: ...

# Capture payment only after success
- name: Capture payment
uri: ...

rescue:
# Release authorization if anything fails
- name: Release payment authorization
uri: ...

- name: Fail playbook
fail:
msg: "Provisioning failed, customer not charged"

2. Validate All API Responses​

Always assert that critical operations succeeded:

- name: Authorize payment
uri: ...
register: api_response_authorization

- name: Assert authorization was successful
assert:
that:
- api_response_authorization.status == 200
- api_response_authorization.json.success == true
fail_msg: "Payment authorization failed: {{ api_response_authorization.json }}"

3. Store Authorization ID​

Always save the authorization_id for use in capture/release:

- name: Store authorization_id for capture/release
set_fact:
authorization_id: "{{ api_response_authorization.json.data.authorization_id }}"

4. Use Metadata Effectively​

Include comprehensive metadata in authorization requests:

metadata:
description: "Clear description of what customer is being charged for"
service_id: "{{ service_id | int }}"
product_id: "{{ product_id }}"
user_id: "{{ initiating_user | int }}"
title: "Short title for transaction"
wholesale_cost: "{{ wholesale_cost | float }}"
invoice: true # Auto-create transaction on capture
send_email: true # Send customer notification

5. Round Currency Values​

Always round currency calculations to 2 decimal places:

- name: Calculate cost with rounding
set_fact:
final_cost: "{{ (base_cost | float * multiplier | float) | round(2) }}"

6. Handle Missing Payment Methods​

Check if customer has a default payment method before attempting authorization:

- name: Get default payment method
set_fact:
payment_method_id: "{{ api_response_payment_methods.json | json_query(query) }}"
vars:
query: "data[?is_default==`true`].payment_method_id | [0]"

- name: Verify payment method exists
assert:
that:
- payment_method_id is defined
- payment_method_id != ""
- payment_method_id != None
fail_msg: "No default payment method found for customer {{ customer_id }}"

Common Patterns​

Pattern 1: Paid Service Provisioning​

For services that require immediate payment:

  1. Get customer's payment method
  2. Authorize payment
  3. Provision service in OCS/CGRateS
  4. Create service record in CRM
  5. Capture payment
  6. On failure: Release authorization and rollback

See play_topup_charge_then_action.yaml for complete example.

Pattern 2: Free Service with Setup Fee​

For services that are free but have a one-time setup cost:

  1. Provision service
  2. Create service record
  3. Add setup fee transaction directly (no payment processing)
  4. Setup fee appears on next invoice

See play_simple_service.yaml at lines 202-232 for complete example.

Pattern 3: Free Topup/Addon​

For free topups that don't require payment:

  1. Get service information
  2. Execute CGRateS action
  3. Update service dates
  4. No payment or transaction creation needed

Pattern 4: Recurring Charges via ActionPlan​

For automatic recurring charges:

  1. Create Action with *http_post to provisioning endpoint
  2. Create ActionPlan with *monthly timing
  3. Assign ActionPlan to account
  4. CGRateS will automatically call the endpoint monthly
  5. Endpoint playbook handles payment processing

Troubleshooting​

Authorization Fails​

Symptom: Authorization endpoint returns error

Common Causes:

  • Payment method doesn't exist or is invalid
  • Insufficient funds
  • Payment method expired
  • Customer ID mismatch

Solution: Check payment method status and customer balance.

Capture Fails After Successful Provisioning​

Symptom: Service provisioned but payment capture fails

Issue: This is a critical failure state - service is active but customer not charged

Solution:

  • Authorization may have expired (typically 7 days)
  • Check authorization is still valid before attempting capture
  • Implement monitoring for failed captures
  • Manual intervention may be required

Transaction Not Created After Capture​

Symptom: Payment captured but no transaction in billing

Cause: invoice: true was not set in authorization metadata

Solution: Either:

  • Set invoice: true in authorization metadata, OR
  • Manually create transaction after successful capture

Pro-Rata Calculation Incorrect​

Symptom: Pro-rata charges don't match expected values

Common Issues:

  • Off-by-one errors in day counting (include/exclude start/end dates)
  • Wrong month used for day count
  • Rounding errors

Solution:

  • Use inclusive date ranges (include both start and end day)
  • Always round to 2 decimal places
  • Test calculations with known values
  • Document which dates are included in calculations

Refunds and Error Handling​

Refund Options​

The payment system supports two types of refunds:

1. Refund to Payment Source - Money returned to original card/PayPal

- name: Refund payment to customer's card
uri:
url: "http://localhost:5000/crm/payments/refund"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"transaction_id": "{{ vendor_transaction_id }}",
"vendor": "stripe",
"amount": "{{ refund_amount | float }}",
"reason": "customer_request"
}

2. Credit to Wallet - Immediate balance for future purchases (handled automatically for error scenarios)

For provisioning failures, the system automatically credits wallet instead of refunding card to:

  • Avoid refund fees
  • Provide immediate availability for retry
  • Improve customer experience

See Refund Options for complete details.

Vendor Support​

The payment system is vendor-agnostic and currently supports:

  • βœ… Stripe (cards, ACH)
  • βœ… PayPal (PayPal accounts, cards)

New payment vendors (Square, Adyen, Braintree, etc.) can be added without changing playbooks.

See also:

Playbook-Specific Guides​

  • concepts_ansible.md - General playbook patterns and structure
  • concepts_provisioning.md - Provisioning system overview

Payment System Documentation​