Skip to main content

OmniCRM Payment System API Guide

Overview

The OmniCRM payment system provides a comprehensive, vendor-agnostic payment processing infrastructure. Today it supports Stripe and PayPal, but the modular architecture allows integration with any payment provider (Square, Adyen, Braintree, etc.) without changing application code.

This document covers all payment APIs and workflows available in the system.

🔧 Ansible Playbook Integration: For implementing these payment APIs in provisioning playbooks, see Charging and Payments from Playbooks


Table of Contents

  1. Modular Architecture
  2. Core Concepts
  3. Financial Documents
  4. Payment Method APIs
  5. Payment Flow APIs
  6. Wallet APIs
  7. API Reference Summary
  8. Common Use Cases

Modular Architecture

Why Vendor-Agnostic?

The system uses abstraction layers to separate business logic from payment vendor specifics. This means:

  • ✅ Add new payment providers without touching application code
  • ✅ Switch vendors without database migrations
  • ✅ Support multiple vendors simultaneously
  • ✅ Consistent API regardless of backend provider

Architecture Layers

Adding a New Payment Vendor

To add a new provider (e.g., Square, Adyen, Braintree), contact your OmniCRM technical team.

For configuration details on existing vendors (Stripe, PayPal), see Vendor Configuration.

Process Overview:

Steps:

  1. Implement processor class with standard PaymentVendorInterface (authorize, capture, charge, refund, release)
  2. Register processor in VendorFactory with vendor name
  3. Test integration with vendor's sandbox environment
  4. Deploy - All existing APIs automatically support the new vendor

Result: Once deployed, the new vendor works seamlessly:

# Add Square payment method
curl -X POST /api/payments/methods \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"vendor": "square",
"payment_token": "sq_xxxxx"
}'

All payment APIs automatically support the new vendor with no application code changes required.

Integration Time: Typically 1-2 days for a new vendor processor.


Vendor Configuration

Enabling Payment Vendors

The UI controls which payment vendors are available to customers. This is set via the VITE_PAYMENT_VENDORS variable in the UI .env file:

# Enable Stripe only (default)
VITE_PAYMENT_VENDORS="stripe"

# Enable both Stripe and PayPal
VITE_PAYMENT_VENDORS="stripe,paypal"

# Enable PayPal only
VITE_PAYMENT_VENDORS="paypal"

Important: VITE_PAYMENT_VENDORS is a build-time variable. Changing it requires rebuilding and restarting the UI.

Stripe Configuration

Stripe is the primary payment vendor, providing card processing with 3D Secure support.

1. Obtain Stripe API Keys

  • Sign up at https://stripe.com
  • Navigate to Developers → API Keys
  • Copy your Publishable Key (pk_live_... or pk_test_...)
  • Copy your Secret Key (sk_live_... or sk_test_...)

Important:

  • Use test keys (pk_test_/sk_test_) for development
  • Use live keys (pk_live_/sk_live_) for production only
  • Never commit API keys to version control

2. Configure Backend

Add to crm_config.yaml (root level, not nested under payment_vendors):

stripe:
secret_key: 'sk_live_YOUR_SECRET_KEY_HERE'
publishable_key: 'pk_live_YOUR_PUBLISHABLE_KEY_HERE'
currency: 'aud'
statement_descriptor_suffix: 'YOUR_COMPANY'

The API reads Stripe credentials from crm_config.yaml at startup. Changing these values requires restarting the API.

3. Configure Frontend

Add to the UI .env file:

VITE_STRIPE_PUBLISHABLE_KEY=pk_live_YOUR_PUBLISHABLE_KEY_HERE

Security Note: Only the publishable key goes in the frontend. The secret key must never be exposed to the browser.

PayPal Configuration

PayPal provides card vaulting and PayPal account payments.

1. Obtain PayPal API Credentials

2. Configure Backend

Add to crm_config.yaml (root level):

paypal:
client_id: 'YOUR_PAYPAL_CLIENT_ID'
client_secret: 'YOUR_PAYPAL_CLIENT_SECRET'
mode: 'sandbox' # 'sandbox' for testing, 'live' for production

The API reads PayPal credentials from crm_config.yaml at startup. Changing these values requires restarting the API.

3. Configure Frontend

Add to the UI .env file:

VITE_PAYMENT_VENDORS="stripe,paypal"
VITE_PAYPAL_CLIENT_ID=YOUR_PAYPAL_CLIENT_ID

VITE_PAYPAL_CLIENT_ID should match the paypal.client_id value in crm_config.yaml.

Sandbox vs Live

SandboxLive
Stripe APIsk_test_ / pk_test_sk_live_ / pk_live_
PayPal APIapi-m.sandbox.paypal.comapi-m.paypal.com
PayPal modesandboxlive
MoneyFakeReal

When switching from sandbox to live, update both crm_config.yaml (credentials and mode) and the UI .env (publishable key / client ID), then rebuild the UI and restart the API.

PCI Compliance

How OmniCRM Maintains PCI Compliance:

  • Card data entered directly into vendor-hosted iframes (Stripe Elements, PayPal Card Fields)
  • OmniCRM never sees or stores full card numbers
  • Only tokenized payment methods stored in database
  • Reduced PCI compliance scope for your business

Payment Processing Metrics

OmniCRM provides comprehensive metrics for monitoring payment processing operations. For complete details on Stripe payment metrics including API call tracking, payment volumes, failure rates, and response times, see Monitoring & Metrics.


Database Schema is Vendor-Agnostic

The database schema supports any payment vendor without migrations:

Example - Saving a Square card:

{
"vendor": "square",
"vendor_payment_method_id": "sq_card_abc123",
"payment_type": "card"
}

No code changes. No migrations. Just works.


Core Concepts

Data Models

PaymentMethod

Vendor-agnostic storage of customer payment methods.

{
"payment_method_id": 789,
"customer_id": 123,
"vendor": "stripe", // 'stripe', 'paypal', or any added vendor
"vendor_payment_method_id": "pm_xxx", // Vendor's internal ID
"payment_type": "card", // 'card', 'paypal', 'ach', etc.
"is_default": true,
"card_brand": "visa",
"card_last4": "4242",
"card_exp_month": 12,
"card_exp_year": 2025,
"card_nickname": "My Visa Card",
"status": "active"
}

PaymentAuthorization

Two-phase commit authorization records (hold funds).

{
"authorization_id": 301,
"customer_id": 123,
"payment_method_id": 789,
"vendor": "stripe", // Which vendor authorized
"vendor_authorization_id": "auth_xxx", // Vendor's authorization ID
"amount": 200.00,
"currency": "USD",
"status": "authorized", // 'authorized', 'captured', 'released'
"authorized_at": "2025-12-27T10:00:00Z",
"expires_at": "2026-01-03T10:00:00Z",
"meta": {}
}

PaymentCapture

Captured/completed payments.

{
"capture_id": 103,
"authorization_id": 301,
"customer_id": 123,
"payment_method_id": 789,
"vendor": "stripe",
"vendor_transaction_id": "ch_xxx", // Vendor's transaction ID
"amount": 200.00,
"currency": "USD",
"status": "succeeded", // 'succeeded', 'failed', 'refunded'
"captured_at": "2025-12-27T10:30:00Z",
"vendor_response": {}, // Full vendor response
"meta": {}
}

WalletAccount

Customer wallet with balance tracking (1-to-1 with customer).

{
"wallet_account_id": 456,
"customer_id": 123,
"balance": 150.50,
"currency": "USD",
"auto_recharge_enabled": true,
"auto_recharge_amount": 100.00,
"auto_recharge_threshold": 10.00,
"low_balance_warning_threshold": 10.00
}

WalletLedger

Full audit trail of all wallet transactions.

{
"ledger_id": 501,
"customer_id": 123,
"wallet_account_id": 456,
"transaction_type": "credit", // 'credit', 'debit', 'refund', 'adjustment'
"amount": 100.00,
"balance_before": 150.50,
"balance_after": 250.50,
"currency": "USD",
"description": "Card top-up",
"reference_type": "payment_capture", // Links to related object
"reference_id": 103,
"meta": {},
"created_at": "2025-12-27T10:35:00Z"
}

Financial Documents

For invoice templating and customization, see Customer Invoices.

Invoices

Definition: An invoice is a document that contains a list of DEBIT transactions (charges). When the API is called to create an invoice, an array of debit transaction IDs should be provided. Those transactions are "linked" to the invoice by setting their invoice_id field.

For transaction management details, see Transactions.

Key Fields:

{
"invoice_id": 12345,
"invoice_number": "INV-2025-000001", // Auto-generated: INV-YYYY-NNNNNN
"customer_id": 123,
"title": "Monthly Services Invoice",
"paid": true, // Payment status
"void": false, // Void status
"payment_reference": "ch_xxxxx", // Last/primary payment ID
"payment_type": "stripe_capture", // Last payment type
"payment_time": "2025-12-27T10:30:00",
"start_date": "2025-12-01",
"end_date": "2025-12-31",
"due_date": "2026-01-15",
"retail_cost": 500.00, // Total invoice amount
"wholesale_cost": 250.00
}

Invoice Generation: Invoice numbers are automatically generated in format INV-YYYY-NNNNNN, sequential within the calendar year, resetting each January 1st.

Payments on Invoices

How Payments Work: Once an invoice is created from debit transactions, a PAYMENT can be applied by creating a credit transaction (negative retail_cost) linked to the invoice.

Payments should:

  • Be listed on the invoice clearly marked as "PAYMENTS"
  • Show the relevant payment date (which can differ from invoice creation date)
  • Support multiple payments per invoice
  • Net against debits to calculate invoice balance

Invoice Status:

  • PAID: Total payments (credits) equal or exceed total debits
  • PARTIALLY PAID: Some payments applied but balance remains
  • OVERPAID: Not currently handled - requires credit splitting (future feature)

Double-Entry Accounting: The system implements proper accounting where every charge has an offsetting payment:

// 1. Debit transaction (charge)
{
"transaction_id": 7001,
"invoice_id": 12345,
"retail_cost": 100.00, // Positive = customer owes
"title": "Service Charge"
}

// 2. Credit transaction (payment)
{
"transaction_id": 7002,
"invoice_id": 12345,
"retail_cost": -100.00, // Negative = payment received
"title": "Payment for Invoice: Service Charge (12345)",
"payment_type": "stripe_capture",
"payment_reference": "ch_xxxxx"
}

// Net result: $0 balance → invoice marked as PAID

Supplementary Invoice Metadata: In addition to credit transactions, the invoice also stores summary fields (payment_reference, payment_type, payment_time) for quick lookups. However:

  • Primary method: Credit transactions linked via invoice_id (Hayden's spec)
  • Secondary metadata: Invoice fields store summary of last/primary payment
  • For multiple payments: Query credit transactions for full history

Statements

Definition: A statement shows all DEBITS and CREDITS from a customer's transactions over a specified period. This is the only document type where both debits and credits are shown as line items together, like a bank statement.

Credit Notes

Purpose: Only applied to invoices with payments already applied. Must be done as part of the invoice voiding process.

Process:

  1. An invoice is voided (along with its associated DEBIT transactions)
  2. Any PAYMENTS already applied are linked to the created CREDIT NOTE
  3. Customer's balance goes into credit by the equivalent amount
  4. The credit note balance can then be:
    • Applied to another invoice as a PAYMENT, OR
    • REFUNDED to the customer

Refund Logic: If refund is chosen, it's invoked based on the associated CREDIT transactions and how they were originally paid (e.g., Stripe refund if the credit transaction type was Stripe).


Payment Method APIs

All endpoints use the base URL: /api/payments/

For detailed payment method management and card handling, see Payment Methods.

Add Payment Method

Save a new payment method for a customer.

Endpoint: POST /api/payments/methods

Request:

curl -X POST https://your-domain.com/api/payments/methods \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"vendor": "stripe",
"payment_token": "pm_xxxxx",
"is_default": false,
"card_nickname": "My Visa Card"
}'

Request Body:

{
"customer_id": 123,
"vendor": "stripe", // 'stripe', 'paypal', or any added vendor
"payment_token": "pm_xxxxx", // One-time token from frontend SDK
"is_default": false, // Set as default payment method?
"card_nickname": "My Visa Card" // Optional friendly name
}

Response (201 Created):

{
"success": true,
"message": "Payment method added successfully",
"data": {
"payment_method_id": 789,
"customer_id": 123,
"vendor": "stripe",
"payment_type": "card",
"card_brand": "visa",
"card_last4": "4242",
"card_exp_month": 12,
"card_exp_year": 2025,
"card_nickname": "My Visa Card",
"is_default": false,
"status": "active"
}
}

Get Payment Methods

Retrieve all payment methods for a customer.

Endpoint: GET /api/payments/methods?customer_id={id}

Request:

curl -X GET "https://your-domain.com/api/payments/methods?customer_id=123" \
-H "Authorization: Bearer YOUR_API_KEY"

Response (200 OK):

{
"success": true,
"data": [
{
"payment_method_id": 789,
"customer_id": 123,
"vendor": "stripe",
"payment_type": "card",
"card_brand": "visa",
"card_last4": "4242",
"is_default": true
},
{
"payment_method_id": 790,
"customer_id": 123,
"vendor": "paypal",
"payment_type": "paypal",
"paypal_email": "user@example.com",
"is_default": false
}
]
}

Get Default Payment Method

Get the default payment method for a customer.

Endpoint: GET /api/payments/methods/default?customer_id={id}

Request:

curl -X GET "https://your-domain.com/api/payments/methods/default?customer_id=123" \
-H "Authorization: Bearer YOUR_API_KEY"

Response (200 OK):

{
"success": true,
"data": {
"payment_method_id": 789,
"vendor": "stripe",
"payment_type": "card",
"card_brand": "visa",
"card_last4": "4242",
"is_default": true
}
}

Set Default Payment Method

Set a payment method as the default.

Endpoint: PUT /api/payments/methods/set-default

Request:

curl -X PUT https://your-domain.com/api/payments/methods/set-default \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"payment_method_id": 790
}'

Response (200 OK):

{
"success": true,
"message": "Default payment method updated",
"data": {
"payment_method_id": 790,
"is_default": true
}
}

Delete Payment Method

Delete a saved payment method.

Endpoint: DELETE /api/payments/methods/{payment_method_id}?customer_id={id}

Request:

curl -X DELETE "https://your-domain.com/api/payments/methods/789?customer_id=123" \
-H "Authorization: Bearer YOUR_API_KEY"

Response (200 OK):

{
"success": true,
"message": "Payment method deleted successfully"
}

Payment Flow APIs

The system supports several payment flows depending on your use case.

1. Direct Payment (Simple Charge)

Use Case: Simple one-step payment without service provisioning.

Endpoint: POST /api/payments/charge

Request:

curl -X POST https://your-domain.com/api/payments/charge \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 50.00,
"currency": "USD",
"payment_method_id": 789,
"metadata": {
"order_id": "12345"
}
}'

Request Body:

{
"customer_id": 123,
"amount": 50.00,
"currency": "USD", // Default: USD
"payment_method_id": 789, // Optional - uses default if omitted
"vendor": "stripe", // Required if using payment_token
"payment_token": "pm_xxxxx", // Optional - one-time token
"save_method": false, // Save payment method for future use?
"metadata": {
"order_id": "12345"
}
}

Response (200 OK):

{
"success": true,
"message": "Payment successful",
"data": {
"transaction_id": "ch_xxxxx",
"capture_id": 101,
"amount": 50.00,
"currency": "USD",
"status": "succeeded"
}
}

2. Invoice Payment (Wallet-First Routing)

Use Case: Pay an invoice using wallet balance first, then card for remainder.

Endpoint: POST /api/payments/invoice

Flow:

  1. Check wallet balance
  2. Use wallet funds first
  3. Charge card for shortfall (if any)
  4. Credit wallet with card amount
  5. Debit wallet for full invoice amount

Request:

curl -X POST https://your-domain.com/api/payments/invoice \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 200.00,
"payment_method_id": 789,
"metadata": {
"invoice_id": 12345,
"description": "Invoice payment"
}
}'

Response (200 OK):

{
"success": true,
"message": "Invoice payment processed",
"data": {
"customer_id": 123,
"service_amount": 200.00,
"routing_mode": "hybrid",
"initial_balance": 150.00,
"wallet_portion_used": 150.00, // Wallet covered this much
"card_portion_used": 50.00, // Card charged for remainder
"charged_amount": 50.00,
"wallet_credited": 50.00,
"wallet_debited": 200.00,
"final_balance": 0.00,
"payment_method_used": true
}
}

3. Authorization Hold (Reserve Funds)

Use Case: Hold funds for later capture (e.g., hotel reservations, rentals).

🔧 Playbook Implementation: For Ansible playbook examples of two-phase payment flows, see Two-Phase Commit Pattern

Step 1: Create Authorization Hold

Endpoint: POST /api/payments/authorize/hold

Wallet-First Flow:

  1. Check wallet balance
  2. Calculate shortfall (amount - wallet_balance)
  3. Authorize card for shortfall only
  4. Wallet debit happens at capture time, not here

Request:

curl -X POST https://your-domain.com/api/payments/authorize/hold \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 200.00,
"payment_method_id": 789,
"use_wallet": true,
"metadata": {
"reservation_id": "RES-001"
}
}'

Request Body:

{
"customer_id": 123,
"amount": 200.00,
"currency": "USD",
"payment_method_id": 789, // Optional - uses default if omitted
"vendor": "stripe", // Required if using payment_token
"payment_token": "pm_xxxxx", // Optional - one-time token
"save_method": false,
"use_wallet": true, // Enable wallet-first routing (default: true)
"metadata": {
"reservation_id": "RES-001"
}
}

Response (200 OK):

{
"success": true,
"message": "Payment authorized (hold created)",
"data": {
"authorization_id": 301,
"vendor_authorization_id": "auth_xxxxx",
"amount": 200.00,
"currency": "USD",
"status": "authorized",
"wallet_balance": 150.00,
"wallet_to_use": 150.00, // Wallet will cover this much
"card_amount": 50.00, // Card authorized for this much
"message": "Card authorized for $50 (wallet top-up). Wallet debit of $200 will occur at capture."
}
}

Step 2: Capture Authorization

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

Wallet-First Capture Flow:

  1. Capture card (if authorized) - tops up wallet
  2. Credit wallet with captured card amount
  3. Debit wallet for full service amount
  4. Create invoice/transactions (if requested)

Request:

curl -X POST https://your-domain.com/api/payments/capture/301 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"amount": 200.00,
"metadata": {
"invoice": true,
"title": "Hotel Reservation",
"description": "3-night stay",
"wholesale_cost": 100.00,
"contract_days": 3,
"send_email": true
}
}'

Request Body:

{
"amount": 200.00, // Optional - captures full amount if omitted
"metadata": {
"invoice": true, // Create invoice and transactions?
"create_transaction": true, // Create transaction record?
"title": "Hotel Reservation",
"description": "3-night stay",
"wholesale_cost": 100.00,
"contract_days": 3,
"send_email": true // Send invoice email?
}
}

Response (200 OK):

{
"success": true,
"message": "Authorization captured",
"data": {
"capture_id": 103,
"transaction_id": "ch_xxxxx",
"authorization_id": 301,
"amount": 200.00,
"currency": "USD",
"status": "succeeded",
"wallet_credit": { // Card topped up wallet
"ledger_id": 401,
"amount": 50.00
},
"wallet_debit": { // Service charged from wallet
"ledger_id": 402,
"amount": 200.00
},
"transaction": { // Created if invoice=true
"transaction_id": 7001
},
"invoice": { // Created if invoice=true
"invoice_id": 12345,
"invoice_number": "INV-2025-000001"
}
}
}

Step 3: Release Authorization (Cancel)

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

Use Case: Cancel reservation, provision failed, or customer changed mind.

Request:

curl -X POST https://your-domain.com/api/payments/release/301 \
-H "Authorization: Bearer YOUR_API_KEY"

Response (200 OK):

{
"success": true,
"message": "Authorization released",
"data": {
"authorization_id": 301,
"vendor_authorization_id": "auth_xxxxx",
"status": "released",
"released_at": "2025-12-27T10:45:00Z"
}
}

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

4. Top-Up Payment (Two-Phase with Provisioning)

Use Case: Process payment for service top-up that requires provisioning (e.g., hotspot/dongle activation). If provisioning fails, authorization is released.

Endpoint: POST /api/payments/topup

Flow: Authorize → Provision service → Capture

Request:

curl -X POST https://your-domain.com/api/payments/topup \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 30.00,
"payment_method_id": 789,
"service_uuid": "svc-uuid-123",
"imsi": "123456789012345",
"days": 30,
"metadata": {
"is_rental": false
}
}'

For Anonymous Rental/Hotspot (payment method NOT saved):

curl -X POST https://your-domain.com/api/payments/topup \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 1,
"amount": 5.00,
"vendor": "stripe",
"payment_token": "pm_xxxxx",
"service_uuid": "hotspot-uuid",
"imsi": "123456789012345",
"days": 1,
"metadata": {
"is_rental": true,
"billing_email": "user@example.com"
}
}'

Response (200 OK):

{
"success": true,
"message": "Topup payment processed successfully",
"data": {
"transaction_id": "ch_xxxxx",
"authorization_id": 302,
"capture_id": 104,
"amount": 30.00,
"status": "succeeded",
"provision_result": {
"success": true,
"topup_result": {...},
"service_uuid": "svc-uuid-123",
"imsi": "123456789012345",
"days": 30
},
"payment_method_saved": false // False for anonymous/rental
}
}

5. Rental Payment (Third-Party)

Use Case: Charge one customer's card to pay for another customer's service.

Endpoint: POST /api/payments/rental

Request:

curl -X POST https://your-domain.com/api/payments/rental \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"beneficiary_customer_id": 456,
"charge_customer_id": 123,
"amount": 75.00,
"payment_method_id": 789,
"service_description": "Rental service payment",
"metadata": {
"rental_agreement_id": "RA-001"
}
}'

For Anonymous Rental (renter's card NOT saved):

curl -X POST https://your-domain.com/api/payments/rental \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"beneficiary_customer_id": 456,
"amount": 75.00,
"vendor": "stripe",
"payment_token": "pm_xxxxx",
"service_description": "Anonymous rental payment",
"metadata": {
"billing_email": "renter@example.com"
}
}'

Response (200 OK):

{
"success": true,
"message": "Rental payment processed",
"data": {
"transaction_id": "ch_xxxxx",
"amount": 75.00,
"payment_method_saved": false // Anonymous: no method saved
}
}

6. Refund Payment

Endpoint: POST /api/payments/refund

Request:

curl -X POST https://your-domain.com/api/payments/refund \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"transaction_id": "ch_xxxxx",
"vendor": "stripe",
"amount": 50.00,
"reason": "customer_request"
}'

Request Body:

{
"transaction_id": "ch_xxxxx", // Vendor transaction ID
"vendor": "stripe", // 'stripe', 'paypal', etc.
"amount": 50.00, // Optional - full refund if omitted
"reason": "customer_request" // Optional refund reason
}

Response (200 OK):

{
"success": true,
"message": "Refund processed",
"data": {
"refund_id": "re_xxxxx",
"amount": 50.00,
"status": "succeeded"
}
}

Wallet APIs

All wallet endpoints use the base URL: /api/wallet/

Get Wallet Balance

Endpoint: GET /api/wallet/balance?customer_id={id}

Request:

curl -X GET "https://your-domain.com/api/wallet/balance?customer_id=123" \
-H "Authorization: Bearer YOUR_API_KEY"

Response (200 OK):

{
"customer_id": 123,
"balance": 150.50,
"currency": "USD"
}

Get Wallet Info

Get complete wallet and credit information including auto-recharge settings.

Endpoint: GET /api/wallet/info?customer_id={id}

Request:

curl -X GET "https://your-domain.com/api/wallet/info?customer_id=123" \
-H "Authorization: Bearer YOUR_API_KEY"

Response (200 OK):

{
"customer_id": 123,
"wallet": {
"wallet_account_id": 456,
"balance": 150.50,
"currency": "USD",
"auto_recharge_enabled": true,
"auto_recharge_amount": 100.00,
"auto_recharge_threshold": 10.00,
"low_balance_warning_threshold": 10.00
}
}

Wallet Top-Up

Top up wallet by charging a payment method.

Endpoint: POST /api/wallet/topup

Flow: Charge card/PayPal → Credit wallet

Request:

curl -X POST https://your-domain.com/api/wallet/topup \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 100.00,
"payment_method_id": 789
}'

Request Body:

{
"customer_id": 123,
"amount": 100.00,
"currency": "USD", // Default: USD
"payment_method_id": 789 // Optional - uses default if omitted
}

Response (200 OK):

{
"success": true,
"payment": {
"transaction_id": "ch_xxxxx",
"amount": 100.00
},
"wallet": {
"ledger_id": 503,
"balance_after": 250.50,
"customer_transaction_id": 7001
},
"message": "Wallet topped up with 100.00 USD"
}

Creates a billing credit line (customer_transaction with payment_type=wallet_topup_card, negative retail_cost, wallet_ledger_id set). This is distinct from hybrid provisioning, where card shortfalls credit the wallet ledger as card_capture_topup without a top-up billing line.

Voiding Add Funds top-ups: DELETE /crm/transaction/transaction_id/{transaction_id} debits the wallet and refunds the card. See Void Transaction below.

Get Wallet Transactions

Get wallet transaction history (ledger).

Endpoint: GET /api/wallet/transactions?customer_id={id}&limit={n}&offset={n}&type={type}

Request:

curl -X GET "https://your-domain.com/api/wallet/transactions?customer_id=123&limit=50&offset=0&type=credit" \
-H "Authorization: Bearer YOUR_API_KEY"

Query Parameters:

  • customer_id (required): Customer ID
  • limit (optional): Number of records (default: 50)
  • offset (optional): Pagination offset (default: 0)
  • type (optional): Filter by transaction type ('credit', 'debit', 'refund', 'adjustment')

Response (200 OK):

{
"customer_id": 123,
"count": 2,
"transactions": [
{
"ledger_id": 501,
"transaction_type": "credit",
"amount": 100.00,
"balance_before": 150.50,
"balance_after": 250.50,
"description": "Card top-up",
"reference_type": "payment_capture",
"reference_id": 103,
"created_at": "2025-12-27T10:35:00Z"
},
{
"ledger_id": 502,
"transaction_type": "debit",
"amount": 50.00,
"balance_before": 250.50,
"balance_after": 200.50,
"description": "Service charge",
"reference_type": "service_charge",
"reference_id": 789,
"created_at": "2025-12-27T11:00:00Z"
}
]
}

Void Transaction

Soft-void an uninvoiced customer transaction. Requires delete_customer_transaction.

Endpoint: DELETE /crm/transaction/transaction_id/{transaction_id}

Request:

curl -X DELETE "https://your-domain.com/api/transaction/transaction_id/7001" \
-H "Authorization: Bearer YOUR_API_KEY"

Success (200 OK) — plain manual charge (no wallet link):

{
"result": "Success"
}

Success (200 OK) — wallet-linked line (reversal only):

{
"result": "Success",
"wallet_reversal": {
"wallet_reversed": true,
"wallet_amount": 50.00,
"wallet_ledger_id": 503
}
}

Success (200 OK) — explicit Add Funds card top-up (payment_type=wallet_topup_card):

{
"result": "Success",
"wallet_reversal": {
"wallet_reversed": true,
"wallet_amount": 100.00,
"wallet_ledger_id": 503,
"refund": {
"capture_id": 88,
"vendor": "stripe",
"vendor_transaction_id": "ch_xxxxx",
"refund_id": "re_xxxxx",
"status": "succeeded",
"amount": 100.00
}
}
}

Void / refund policy

Transaction sourcewallet_ledger_idWallet on voidCard refund on void
Manual charge/creditNoNoNo
Add Funds (POST /wallet/topup)YesDebit walletYes (wallet_topup_card)
Cash / manual wallet creditYesDebit walletNo
Invoice paid from walletYesCredit walletNo
Service / provisioning (hybrid capture)Usually no on the charge CTNo*No

*Hybrid flows route card → wallet → service debit internally (card_capture_topup + service_charge ledger rows). The visible billing line is the service charge, not a top-up line. Richer reversal for those charges is planned on the financial-documents branch.

Errors (500):

{
"result": "Failed",
"Reason": "Insufficient wallet balance. Available: 0.00, Required: 20.00"
}
  • Invoiced transaction: "Cannot delete a transaction that is associated with an invoice"
  • Insufficient wallet: customer already spent topped-up funds; void is rejected (no partial void)
  • Card refund failure: wallet reversal is rolled back; transaction stays non-void

Note: Void marks void=true (excluded from invoicing/stats). It is not the same as invoice refund.


API Reference Summary

Payment Method Endpoints

MethodEndpointDescription
POST/api/payments/methodsAdd payment method
GET/api/payments/methods?customer_id={id}Get all payment methods
GET/api/payments/methods/default?customer_id={id}Get default payment method
PUT/api/payments/methods/set-defaultSet default payment method
DELETE/api/payments/methods/{id}?customer_id={id}Delete payment method

Payment Flow Endpoints

MethodEndpointDescription
POST/api/payments/chargeDirect payment (one-step)
POST/api/payments/invoiceInvoice payment (wallet-first)
POST/api/payments/topupTop-up with provisioning
POST/api/payments/authorize/holdCreate authorization hold
POST/api/payments/capture/{id}Capture authorization
POST/api/payments/release/{id}Release authorization
POST/api/payments/rentalRental/third-party payment
POST/api/payments/refundRefund payment

Wallet Endpoints

MethodEndpointDescription
GET/api/wallet/balance?customer_id={id}Get wallet balance
GET/api/wallet/info?customer_id={id}Get wallet info + settings
POST/api/wallet/topupTop up wallet via payment method (creates wallet_topup_card billing line)
GET/api/wallet/transactions?customer_id={id}Get transaction history

Transaction Endpoints

MethodEndpointDescription
PUT/api/transaction/Create customer transaction
GET/api/transaction/List transactions (paginated)
DELETE/api/transaction/transaction_id/{id}Void transaction; wallet reversal; card refund only for Add Funds top-ups

PayPal-Specific Endpoints

MethodEndpointDescription
POST/api/payments/paypal/vault/setup-tokenCreate PayPal setup token for Card Fields SDK
POST/api/payments/paypal/vault/finalizeFinalize PayPal vault and save payment method
POST/api/payments/paypal/vault/update-setup-tokenUpdate setup token for 3DS/SCA handling

Common Use Cases

Use Case 1: Charge Customer for Service (Prepaid)

Scenario: Customer paying for 30 days of service using wallet-first routing.

# Invoice payment (wallet first, card for shortfall)
curl -X POST https://your-domain.com/api/payments/invoice \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 99.99,
"metadata": {
"service_id": 456,
"contract_days": 30,
"description": "30-day Premium Service"
}
}'

Response:

{
"success": true,
"message": "Invoice payment processed",
"data": {
"service_amount": 99.99,
"wallet_portion_used": 50.00, // Wallet had $50
"card_portion_used": 49.99, // Card charged $49.99
"final_balance": 0.00
}
}

Use Case 2: Two-Phase Payment with Service Provisioning

Scenario: Provision a service only after payment is authorized. If provisioning fails, no charge occurs.

# Top-up payment with automatic provisioning
curl -X POST https://your-domain.com/api/payments/topup \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 149.99,
"payment_method_id": 789,
"service_uuid": "fiber-svc-uuid",
"imsi": "123456789012345",
"days": 30,
"metadata": {
"service_type": "fiber_internet"
}
}'

Flow:

  1. Authorize $149.99 on card
  2. Provision fiber internet service
  3. If provision succeeds → capture payment
  4. If provision fails → release authorization (no charge)

Use Case 3: Hotspot Anonymous Payment

Scenario: Anonymous user pays for WiFi hotspot access. No customer record, no saved payment method.

# Anonymous hotspot payment
curl -X POST https://your-domain.com/api/payments/topup \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 1,
"amount": 5.00,
"vendor": "stripe",
"payment_token": "pm_xxxxx",
"service_uuid": "hotspot-downtown",
"imsi": "999999999999999",
"days": 1,
"metadata": {
"is_rental": true,
"billing_email": "user@example.com",
"hotspot_location": "Cafe Downtown"
}
}'

Result:

  • Payment processed ✅
  • Hotspot activated ✅
  • Payment method NOT saved ✅
  • No customer record created ✅

Use Case 4: Hotel Reservation (Auth Hold + Capture)

Scenario: Hold funds for hotel reservation, capture on check-in, release if cancelled.

# Step 1: Check-in - hold funds
curl -X POST https://your-domain.com/api/payments/authorize/hold \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 500.00,
"use_wallet": true,
"metadata": {
"reservation_id": "RES-2025-001"
}
}'

# Response: {"authorization_id": 301, "status": "authorized"}

# Step 2a: Customer checks in - capture
curl -X POST https://your-domain.com/api/payments/capture/301 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"metadata": {
"invoice": true,
"title": "Hotel Stay - 3 Nights",
"description": "Room 302, Dec 27-30",
"send_email": true
}
}'

# Step 2b: Customer cancels - release hold
curl -X POST https://your-domain.com/api/payments/release/301 \
-H "Authorization: Bearer YOUR_API_KEY"

Use Case 5: Add Payment Method (Stripe)

Scenario: Customer adds a new Visa card to their account.

# Step 1: Frontend creates Stripe token via Stripe.js
# const {token} = await stripe.createToken(cardElement);

# Step 2: Backend saves payment method
curl -X POST https://your-domain.com/api/payments/methods \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"vendor": "stripe",
"payment_token": "pm_xxxxx",
"is_default": true,
"card_nickname": "Work Visa"
}'

Use Case 6: Top Up Wallet

Scenario: Customer tops up their wallet with $100 using saved payment method.

curl -X POST https://your-domain.com/api/wallet/topup \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 100.00
}'

Response:

{
"success": true,
"payment": {
"transaction_id": "ch_xxxxx",
"amount": 100.00
},
"wallet": {
"balance_after": 250.50
},
"message": "Wallet topped up with 100.00 USD"
}

Error Handling

Common Error Responses

Insufficient Funds (400 Bad Request):

{
"error": "Insufficient wallet balance. Available: 50.00, Required: 200.00"
}

Payment Failed (400 Bad Request):

{
"error": "Payment failed. Please try again."
}

Validation Error (400 Bad Request):

{
"error": "customer_id and amount are required"
}

Not Found (404 Not Found):

{
"error": "Authorization 999 not found"
}

Server Error (500 Internal Server Error):

{
"error": "An error occurred processing your payment"
}

Refunds and Wallet-First Routing

Refund Options

The system supports two types of refunds depending on your business needs:

1. Refund to Payment Source (Stripe/PayPal)

Use Case: Customer requests full refund for cancelled service or defective product.

Flow: Money goes back to original payment method (card, PayPal account, etc.)

Endpoint: POST /api/payments/refund

Example:

curl -X POST https://your-domain.com/api/payments/refund \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"transaction_id": "ch_xxxxx",
"vendor": "stripe",
"amount": 100.00,
"reason": "customer_request"
}'

Result:

  • Stripe/PayPal processes refund to original payment method
  • Customer sees credit on their card/PayPal account within 5-10 business days
  • Money does NOT go to wallet
  • Full audit trail maintained in PaymentCapture table

When to Use:

  • ✅ Customer cancelled order
  • ✅ Service not delivered
  • ✅ Billing error
  • ✅ Customer explicitly requests refund to card

2. Credit to Wallet

Use Case: Partial refund, service credit, or keeping funds for future purchases.

Flow: Money credited to customer's wallet balance for immediate use.

Note: Wallet credits are typically handled internally by the system during error scenarios. For manual wallet credits, contact support or use admin tools.

Result:

  • Funds immediately available in wallet
  • No waiting period
  • Can be used for future purchases
  • Full audit trail in WalletLedger table

When to Use:

  • ✅ Service credits (e.g., compensation for downtime)
  • ✅ Partial refunds where customer will repurchase
  • ✅ Error compensation (failed provisioning)
  • ✅ Promotional credits

Hybrid Refund Strategy

Best Practice: For error scenarios during payment flow, the system automatically credits wallet instead of refunding card.

Automatic Error Recovery:

Example Scenario:

  1. Customer pays $100 for service
  2. Card charged successfully
  3. Provisioning fails
  4. Instead of refunding $100 to card (5-10 days + refund fees):
    • Credit $100 to wallet immediately
    • Customer can retry purchase instantly
    • No refund fees
    • Better user experience

Wallet-First Routing: Optimized Card Charges

The system only charges your card for the shortfall, not the full amount, when you have wallet balance.

Example 1: $1 Wallet Balance + $10 Purchase

Scenario: You have $1 in your wallet and want to purchase a $10 addon.

Traditional Payment Flow (NOT how this system works):

OmniCRM Wallet-First Flow:

API Request:

curl -X POST https://your-domain.com/api/payments/invoice \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 10.00,
"metadata": {
"addon_id": 456
}
}'

API Response:

{
"success": true,
"message": "Invoice payment processed",
"data": {
"customer_id": 123,
"service_amount": 10.00,
"routing_mode": "hybrid",
"initial_balance": 1.00,
"wallet_portion_used": 1.00, // Used existing $1
"card_portion_used": 9.00, // Only charged $9 shortfall
"charged_amount": 9.00, // ← Card charge
"wallet_credited": 9.00, // ← Topped up wallet
"wallet_debited": 10.00, // ← Service charge
"final_balance": 0.00
}
}

Example 2: $50 Wallet Balance + $30 Purchase

Scenario: You have $50 in wallet and purchase costs $30.

Wallet-First Flow:

API Response:

{
"success": true,
"data": {
"service_amount": 30.00,
"initial_balance": 50.00,
"charged_amount": 0, // ← No card charge
"wallet_debited": 30.00,
"final_balance": 20.00,
"payment_method_used": false // ← Card not used
}
}

Example 3: Authorization Hold with Wallet Balance

Scenario: Hotel hold $500 authorization with $150 wallet balance.

Step 1: Authorization (POST /api/payments/authorize/hold):

API Response:

{
"authorization_id": 301,
"amount": 500.00,
"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."
}

Step 2: Capture (POST /api/payments/capture/301):

Why This Matters:

  • ✅ Customer only has $350 "held" on their card, not $500
  • ✅ Reduces customer's available credit impact
  • ✅ More accurate authorization amounts
  • ✅ Better customer experience

Implementation Details

Wallet-First Routing Logic:

How It Works:

  1. System checks current wallet balance
  2. Calculates shortfall: amount - wallet_balance
  3. If shortfall > 0, charges card for shortfall only
  4. Credits wallet with card payment
  5. Debits wallet for full service amount
  6. Result: Customer only charged for what wallet couldn't cover

Routing Mode Override

You can override wallet-first behavior if needed:

Bypass Mode - Always charge card even if wallet has funds:

curl -X POST https://your-domain.com/api/payments/charge \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"customer_id": 123,
"amount": 100.00,
"metadata": {
"routing_mode": "bypass"
}
}'

Result: Card charged $100, wallet credited $100, wallet debited $100 (net: $0)

Use Case: Customer wants to use card for rewards/points even with wallet balance.


Best Practices

  1. Always use wallet-first routing for customer payments to enable prepaid functionality
  2. Use two-phase payments (topup endpoint) when provisioning services to avoid charging if provisioning fails
  3. Set metadata for all payments to maintain audit trail
  4. Use anonymous payments only for truly anonymous users (hotspots, rentals)
  5. Handle errors gracefully and provide clear user feedback
  6. Test payment flows in both success and failure scenarios
  7. Monitor authorization expirations (typically 7 days for cards)
  8. Implement auto-recharge for recurring services to improve customer experience

Playbook-Specific Best Practices

When implementing payment flows in Ansible playbooks:

  1. Always use block/rescue pattern - Wrap provisioning in try/catch for automatic rollback
  2. Store authorization_id - Save for capture/release operations
  3. Validate API responses - Assert success before proceeding
  4. Round currency values - Always use 2 decimal places
  5. Check payment methods - Verify customer has default payment method before authorization

See Playbook Best Practices for complete details and examples.


Authentication

All API endpoints require authentication via API key in the Authorization header:

Authorization: Bearer YOUR_API_KEY

Contact your system administrator for API key provisioning.


Vendor Support

Currently Supported

  • Stripe - Full support (cards, ACH)
  • PayPal - Full support (PayPal accounts, cards via Card Fields SDK)

Adding New Vendors

The modular architecture makes it trivial to add new payment vendors. See Modular Architecture section for details.


Implementation Guides

Feature-Specific Guides

Quick Navigation