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
- Modular Architecture
- Core Concepts
- Financial Documents
- Payment Method APIs
- Payment Flow APIs
- Wallet APIs
- API Reference Summary
- 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:
- Implement processor class with standard
PaymentVendorInterface(authorize, capture, charge, refund, release) - Register processor in VendorFactory with vendor name
- Test integration with vendor's sandbox environment
- 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
- Create a REST API app at https://developer.paypal.com/dashboard/
- Copy your Client ID and Client Secret
- For testing, use Sandbox credentials; for production, use Live 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
| Sandbox | Live | |
|---|---|---|
| Stripe API | sk_test_ / pk_test_ | sk_live_ / pk_live_ |
| PayPal API | api-m.sandbox.paypal.com | api-m.paypal.com |
PayPal mode | sandbox | live |
| Money | Fake | Real |
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:
- An invoice is voided (along with its associated DEBIT transactions)
- Any PAYMENTS already applied are linked to the created CREDIT NOTE
- Customer's balance goes into credit by the equivalent amount
- 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:
- Check wallet balance
- Use wallet funds first
- Charge card for shortfall (if any)
- Credit wallet with card amount
- 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:
- Check wallet balance
- Calculate shortfall (amount - wallet_balance)
- Authorize card for shortfall only
- 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:
- Capture card (if authorized) - tops up wallet
- Credit wallet with captured card amount
- Debit wallet for full service amount
- 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 IDlimit(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 source | wallet_ledger_id | Wallet on void | Card refund on void |
|---|---|---|---|
| Manual charge/credit | No | No | No |
Add Funds (POST /wallet/topup) | Yes | Debit wallet | Yes (wallet_topup_card) |
| Cash / manual wallet credit | Yes | Debit wallet | No |
| Invoice paid from wallet | Yes | Credit wallet | No |
| Service / provisioning (hybrid capture) | Usually no on the charge CT | No* | 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
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/payments/methods | Add 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-default | Set default payment method |
| DELETE | /api/payments/methods/{id}?customer_id={id} | Delete payment method |
Payment Flow Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/payments/charge | Direct payment (one-step) |
| POST | /api/payments/invoice | Invoice payment (wallet-first) |
| POST | /api/payments/topup | Top-up with provisioning |
| POST | /api/payments/authorize/hold | Create authorization hold |
| POST | /api/payments/capture/{id} | Capture authorization |
| POST | /api/payments/release/{id} | Release authorization |
| POST | /api/payments/rental | Rental/third-party payment |
| POST | /api/payments/refund | Refund payment |
Wallet Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/wallet/balance?customer_id={id} | Get wallet balance |
| GET | /api/wallet/info?customer_id={id} | Get wallet info + settings |
| POST | /api/wallet/topup | Top up wallet via payment method (creates wallet_topup_card billing line) |
| GET | /api/wallet/transactions?customer_id={id} | Get transaction history |
Transaction Endpoints
| Method | Endpoint | Description |
|---|---|---|
| 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
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/payments/paypal/vault/setup-token | Create PayPal setup token for Card Fields SDK |
| POST | /api/payments/paypal/vault/finalize | Finalize PayPal vault and save payment method |
| POST | /api/payments/paypal/vault/update-setup-token | Update 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:
- Authorize $149.99 on card
- Provision fiber internet service
- If provision succeeds → capture payment
- 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
PaymentCapturetable
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
WalletLedgertable
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:
- Customer pays $100 for service
- Card charged successfully
- Provisioning fails
- 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:
- System checks current wallet balance
- Calculates shortfall:
amount - wallet_balance - If shortfall > 0, charges card for shortfall only
- Credits wallet with card payment
- Debits wallet for full service amount
- 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
- Always use wallet-first routing for customer payments to enable prepaid functionality
- Use two-phase payments (topup endpoint) when provisioning services to avoid charging if provisioning fails
- Set metadata for all payments to maintain audit trail
- Use anonymous payments only for truly anonymous users (hotspots, rentals)
- Handle errors gracefully and provide clear user feedback
- Test payment flows in both success and failure scenarios
- Monitor authorization expirations (typically 7 days for cards)
- Implement auto-recharge for recurring services to improve customer experience
Playbook-Specific Best Practices
When implementing payment flows in Ansible playbooks:
- Always use block/rescue pattern - Wrap provisioning in try/catch for automatic rollback
- Store authorization_id - Save for capture/release operations
- Validate API responses - Assert success before proceeding
- Round currency values - Always use 2 decimal places
- 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.
Related Documentation
Implementation Guides
- Charging and Payments from Playbooks - Ansible playbook implementation
Feature-Specific Guides
- Customer Invoices - Invoice generation, templating, and customization
- Payment Methods - Managing customer payment methods, cards, and PayPal accounts
- Transactions - Transaction management, manual charges, and credits
- Payment Processing - Payment workflows and processing
- Billing Basics - Customer billing fundamentals
Quick Navigation
- Adding vendors? → See Modular Architecture
- Configuring Stripe/PayPal? → See Vendor Configuration
- Customizing invoices? → See Customer Invoices
- Provisioning services? → See Playbook Integration