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.
- Two-Phase Commit Payment Flow - For paid services requiring immediate payment authorization and capture (ie Prepaid Services)
- 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_costin 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_idfor use in capture or release calls - Note the
card_amountshows 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:
- Card captured for shortfall amount (if card was authorized)
- Wallet credited with captured card amount
- Wallet debited for full service amount
- If
invoice: truewas 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
- Payment record is created in the database
- 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:
- Card authorization released (if card was authorized)
- Held funds returned to customer's available credit
- Wallet NOT debited (since debit only happens at capture)
- Customer is NOT charged
- 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:
- Get customer's payment method
- Authorize payment
- Provision service in OCS/CGRateS
- Create service record in CRM
- Capture payment
- 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:
- Provision service
- Create service record
- Add setup fee transaction directly (no payment processing)
- 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:
- Get service information
- Execute CGRateS action
- Update service dates
- No payment or transaction creation needed
Pattern 4: Recurring Charges via ActionPlanβ
For automatic recurring charges:
- Create Action with
*http_postto provisioning endpoint - Create ActionPlan with
*monthlytiming - Assign ActionPlan to account
- CGRateS will automatically call the endpoint monthly
- 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: truein 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:
- Modular Architecture - How vendor abstraction works
- Vendor Configuration - Stripe/PayPal setup and API keys
Related Documentationβ
Playbook-Specific Guidesβ
concepts_ansible.md- General playbook patterns and structureconcepts_provisioning.md- Provisioning system overview
Payment System Documentationβ
- Payment System API Guide - Complete API reference, wallet routing, refunds
payments_transaction.md- Transaction management and manual chargespayments_process.md- Processing payments and invoicesbasics_payment.md- Payment methods and customer billing