Transaction Lifecycle
Complete end-to-end transaction flow from initiation to settlement in the FLUID Network.
Overview
This guide provides a comprehensive walkthrough of the debit transaction lifecycle, covering all stages from initial API request through to final settlement. Understanding this flow is critical for implementing robust integrations that handle all transaction states correctly.
Transaction States
FLUID Network uses a state machine to manage transaction lifecycle with clear transitions and rules:
| Status | Description | Webhook Event | Retryable | Terminal |
|---|---|---|---|---|
pending | Transaction created, queued for bank processing | - | No | No |
processing | Sent to bank, awaiting customer approval | - | No | No |
completed | Successfully approved and debited | transaction.completed | No | No |
failed | Rejected, declined, or error occurred | transaction.failed | Yes | No |
reversed | Previously completed transaction reversed | transaction.reversed | No | Yes |
INFO
Asynchronous Processing: FLUID Network uses background job processing for bank requests. The API responds immediately (<200ms) with pending status while bank processing happens asynchronously.
Complete Transaction Flow
State Transition Diagram
Status Transition Rules
Valid Transitions
| From Status | To Status | Trigger | Notes |
|---|---|---|---|
pending | processing | Background job processes bank request | Automatic transition |
pending | failed | Validation error or immediate bank rejection | Instant failure |
processing | completed | Customer approves via USSD/App | Success path |
processing | failed | Customer denies, timeout, or bank error | Failure path |
completed | reversed | Reversal request (rare) | Manual operation |
failed | pending | Manual retry or automatic retry logic | Allows retry |
Invalid Transitions
DANGER
These transitions are not allowed and will result in a 422 Unprocessable Entity error:
completed→pending(cannot un-complete)completed→failed(cannot fail after completion)reversed→ any status (terminal state)processing→pending(cannot go backward)
Phase-by-Phase Breakdown
Phase 1: Transaction Initiation
Duration: <200ms (synchronous)
POST /api/v1/payment-providers/debit-requests/charge
Authorization: Token YOUR_API_KEY
Content-Type: application/jsonRequest Body:
{
"phone_number": "+233241234567",
"bank_identifier": "ECO",
"amount": 100.00,
"currency": "GHS",
"narration": "Payment for Order #12345",
"partner_reference": "order_12345",
"metadata": {
"order_id": "12345",
"customer_email": "john@example.com"
}
}Immediate Response:
{
"success": true,
"reference": "FLU20250112ABC123",
"status": "pending",
"response": {
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "pending",
"timestamp": "2025-01-12T14:30:00Z",
"bank": {
"name": "Example Bank Ghana",
"identifier": "EXB",
"country_code": "GH"
}
}
}What Happens:
- FLUID validates request parameters
- Creates transaction record with
pendingstatus - Queues
BankDebitRequestJobfor background processing - Returns immediately with transaction reference
- Background job processes bank request asynchronously
TIP
Why Async? Synchronous bank API calls can take 2-5 seconds. Async processing provides:
- 90% faster API response times
- 10x better scalability
- 100% reliability (API succeeds even if bank temporarily fails)
- Dramatically improved user experience
Phase 2: Bank Processing
Duration: 1-30 seconds (asynchronous)
# Background job processes bank request
BankDebitRequestJob.perform_later(transaction_id)What Happens:
- Background job picks up transaction
- Selects appropriate bank connector
- Sends debit request to bank API
- Bank accepts request and sends notification to customer
- Transaction status updates to
processing - Job completes successfully
Potential Errors:
- Bank API timeout → Automatic retry with exponential backoff
- Bank API error → 3-5 retry attempts before marking
failed - Network issues → Retry logic preserves transaction state
Phase 3: Customer Approval
Duration: 30 seconds - 5 minutes (variable)
Customer Experience:
- Receives USSD prompt or mobile app notification
- Reviews transaction details:
- Merchant name
- Amount to debit
- Account to debit from
- Enters PIN or biometric approval
- Confirms or cancels transaction
Approval Methods:
| Method | Description | Typical Duration |
|---|---|---|
ussd | USSD push notification | 30s - 2min |
mobile_app | Bank mobile app approval | 1-3min |
web | Bank web portal approval | 2-5min |
auto | Pre-authorized automatic debit | <5s |
Phase 4: Status Update & Webhook
Duration: <1 second
When the bank updates the transaction status, FLUID automatically triggers a webhook notification:
Webhook Payload (Completed):
{
"event": "transaction.completed",
"event_id": "evt_a1b2c3d4e5f6",
"timestamp": "2025-01-12T14:32:30Z",
"api_version": "v1",
"data": {
"transaction": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"reference": "FLU20250112ABC123",
"partner_reference": "order_12345",
"status": "completed",
"amount": 100.00,
"currency": "GHS",
"narration": "Payment for Order #12345",
"approval_method": "ussd",
"bank_reference": "EXB-2025-001234",
"created_at": "2025-01-12T14:30:00Z",
"completed_at": "2025-01-12T14:32:30Z"
},
"customer": {
"phone_number": "+233241234567",
"name": "John Doe"
},
"bank": {
"name": "Example Bank Ghana",
"identifier": "EXB",
"country_code": "GH"
},
"previous_status": "processing"
}
}Webhook Payload (Failed):
{
"event": "transaction.failed",
"event_id": "evt_b2c3d4e5f6g7",
"timestamp": "2025-01-12T14:32:30Z",
"api_version": "v1",
"data": {
"transaction": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"reference": "FLU20250112ABC123",
"partner_reference": "order_12345",
"status": "failed",
"amount": 100.00,
"currency": "GHS",
"error_message": "Insufficient funds",
"created_at": "2025-01-12T14:30:00Z",
"completed_at": null
},
"customer": {
"phone_number": "+233241234567",
"name": "John Doe"
},
"bank": {
"name": "Example Bank Ghana",
"identifier": "EXB",
"country_code": "GH"
},
"previous_status": "processing"
}
}WARNING
Webhook Handling Best Practices:
- Always verify HMAC signature (see Signature Verification)
- Return
200 OKwithin 5 seconds to acknowledge receipt - Process webhook asynchronously in your system
- Implement idempotency using
event_idto avoid duplicate processing
Phase 5: Settlement (Daily Batch)
Duration: Processed daily at 00:00 UTC
What Happens:
- Daily job runs at 00:00 UTC
- Identifies all
completedtransactions from previous days - Groups transactions by payment partner and currency
- Creates
SettlementBatchrecords - Generates settlement line items
- Calculates fees, net amounts, and totals
- Processes batches through bank settlement APIs
- Generates settlement reports for partners
Settlement Timeline:
| Day | Action | Description |
|---|---|---|
| Day 0 | Transaction completes | Funds debited from customer |
| Day 1 | Settlement batch created | Included in next day's batch |
| Day 2 | Settlement processed | Funds transferred to partner account |
INFO
Automatic Settlement: All completed transactions automatically create settlement records. No manual intervention required.
Polling vs Webhooks
Polling Transaction Status
You can poll transaction status using the query endpoint:
GET /api/v1/payment-providers/debit-requests/{reference}
Authorization: Token YOUR_API_KEYResponse:
{
"success": true,
"data": {
"id": 12345,
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"reference": "FLU20250112ABC123",
"status": "completed",
"amount": 100.00,
"currency": "GHS",
"created_at": "2025-01-12T14:30:00Z",
"completed_at": "2025-01-12T14:32:30Z"
}
}Polling Strategy:
# Poll transaction status every 10 seconds for up to 2 minutes
reference="FLU20250112ABC123"
max_attempts=12
interval=10
for ((i=1; i<=max_attempts; i++)); do
response=$(curl -s "https://api.fluid-network.com/api/v1/payment-providers/debit-requests/${reference}" \
-H "Authorization: Token YOUR_API_KEY")
status=$(echo "$response" | jq -r '.data.status')
echo "Attempt $i: Status = $status"
# Check if terminal state reached
if [[ "$status" == "completed" || "$status" == "failed" || "$status" == "reversed" ]]; then
echo "Final state reached: $status"
echo "$response" | jq '.'
exit 0
fi
# Wait before next attempt
sleep $interval
done
echo "Polling timeout after 2 minutes"
exit 1Implementation Notes:
- Max attempts: 12 attempts × 10 seconds = 2 minutes maximum
- Interval: Poll every 10 seconds (don't poll more frequently - rate limit risk)
- Terminal states:
completed,failed, orreversedindicate transaction is done - Timeout handling: After max attempts, consider transaction timed out
- Implementation pattern:
- Make HTTP GET request to status endpoint
- Parse JSON response to extract status field
- Check if status is in terminal state list
- If terminal, return result and exit loop
- If not terminal, wait interval seconds before next attempt
- After max attempts, throw timeout error
- Language-specific approaches:
- JavaScript/Node.js: Use
setTimeout()in loop,fetch()or axios for HTTP - Python: Use
time.sleep()in loop,requestslibrary for HTTP - PHP: Use
sleep()in loop,curlor Guzzle for HTTP - Ruby: Use
sleep()in loop, RestClient or Net::HTTP for HTTP
- JavaScript/Node.js: Use
DANGER
Polling Anti-Patterns:
- ❌ Polling more frequently than every 10 seconds (rate limit violations)
- ❌ Polling indefinitely without timeout (resource waste)
- ❌ Using polling instead of webhooks for production (poor scalability)
Webhooks (Recommended)
TIP
Best Practice: Use webhooks for production systems. Webhooks provide:
- Real-time updates (no polling delay)
- Better scalability (no wasted API calls)
- Lower costs (reduced rate limit consumption)
- Simplified code (event-driven architecture)
See Webhooks Overview for complete webhook implementation guide.
Error Scenarios & Handling
Common Error Scenarios
| Scenario | Status | Error Message | Recovery |
|---|---|---|---|
| Insufficient funds | failed | "Insufficient funds" | Ask customer to fund account, retry |
| Customer declined | failed | "Customer declined transaction" | No retry, consider alternative payment |
| Timeout | failed | "Transaction timeout" | Retry with new transaction |
| Invalid account | failed | "Invalid account number" | Verify account details, update |
| Bank system error | failed | "Bank system unavailable" | Retry after 5 minutes |
| Network error | pending → retry | "Network error" | Automatic retry by background job |
Error Handling Best Practices
Retryable vs Non-Retryable Errors:
# Example: Check transaction and determine retry strategy
reference="FLU20250112ABC123"
response=$(curl -s "https://api.fluid-network.com/api/v1/payment-providers/debit-requests/${reference}" \
-H "Authorization: Token YOUR_API_KEY")
status=$(echo "$response" | jq -r '.data.status')
error_msg=$(echo "$response" | jq -r '.data.error_message // empty')
if [ "$status" == "completed" ]; then
echo "✅ Transaction successful"
# Process success: mark order as paid, send receipt, etc.
elif [ "$status" == "failed" ]; then
echo "❌ Transaction failed: $error_msg"
# Check if retryable
case "$error_msg" in
*"timeout"*|*"unavailable"*|*"Network error"*)
echo "⏱️ Retryable error - schedule retry after 5 minutes"
;;
*"Insufficient funds"*|*"declined"*)
echo "🚫 Non-retryable error - notify customer, offer alternative payment"
;;
esac
fiImplementation Notes:
- Success path (
completed):- Mark order/invoice as paid in your system
- Send receipt/confirmation to customer
- Update inventory, fulfill order
- Log successful payment
- Retryable failures:
- "Transaction timeout" → Retry with new transaction after 5 minutes
- "Bank system unavailable" → Retry after 5-10 minutes
- "Network error" → Automatic retry by FLUID background jobs
- Schedule retry with exponential backoff
- Non-retryable failures:
- "Insufficient funds" → Ask customer to fund account, don't retry automatically
- "Customer declined" → Offer alternative payment method
- "Invalid account" → Verify and update account details
- Notify customer of specific failure reason
- Processing state:
- Transaction awaiting customer approval
- Notify customer to check phone for USSD prompt
- Don't mark as failed yet - customer may still approve
- Error handling pattern:
- Use switch/case statement on transaction status
- For failed status, inspect error message to categorize
- Implement different recovery strategies per error type
- Log all transaction outcomes for debugging
- Maintain idempotency to prevent duplicate charges on retry
Transaction Timeouts
Default Timeouts
| Phase | Timeout | Behavior |
|---|---|---|
| API Request | 30 seconds | Returns 504 Gateway Timeout |
| Bank Processing | 10 minutes | Transaction marked as failed |
| Customer Approval | 5 minutes | Transaction marked as failed (timeout) |
| Webhook Delivery | 30 seconds | Retries with exponential backoff |
Handling Timeouts
WARNING
Important: Transactions that timeout during customer approval will be automatically marked as failed with error message "Transaction timeout". You can retry these transactions with a new API request.
Complete Implementation Example
This example demonstrates a complete transaction flow from initiation through status checking:
#!/bin/bash
# complete-transaction-flow.sh
# Complete FLUID Network debit transaction implementation
API_KEY="your_api_key_here"
BASE_URL="https://api.fluid-network.com/api/v1/payment-providers"
# Step 1: Initiate transaction
echo "=== Step 1: Initiating Transaction ==="
response=$(curl -s -X POST "$BASE_URL/debit-requests/charge" \
-H "Authorization: Token $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+233241234567",
"bank_identifier": "ECO",
"amount": 100.00,
"currency": "GHS",
"narration": "Payment for Order #12345",
"partner_reference": "order_12345",
"metadata": {
"customer_email": "john@example.com"
}
}')
# Extract reference and status
reference=$(echo "$response" | jq -r '.reference')
status=$(echo "$response" | jq -r '.status')
uuid=$(echo "$response" | jq -r '.response.uuid')
echo "Transaction initiated:"
echo " Reference: $reference"
echo " UUID: $uuid"
echo " Status: $status"
# Step 2: Poll for completion (max 2 minutes)
echo ""
echo "=== Step 2: Polling for Completion ==="
max_attempts=12
interval=10
for ((i=1; i<=max_attempts; i++)); do
echo "Attempt $i of $max_attempts..."
status_response=$(curl -s "$BASE_URL/debit-requests/$reference" \
-H "Authorization: Token $API_KEY")
current_status=$(echo "$status_response" | jq -r '.data.status')
echo " Current status: $current_status"
# Check for terminal state
if [[ "$current_status" == "completed" ]]; then
echo ""
echo "✅ Transaction completed successfully!"
echo "$status_response" | jq '.data'
exit 0
elif [[ "$current_status" == "failed" ]]; then
error_msg=$(echo "$status_response" | jq -r '.data.error_message')
echo ""
echo "❌ Transaction failed: $error_msg"
# Check if retryable
case "$error_msg" in
*"timeout"*|*"unavailable"*)
echo "⏱️ This is a retryable error. You can create a new transaction."
;;
*"Insufficient"*|*"declined"*)
echo "🚫 This is a permanent failure. Customer action required."
;;
esac
exit 1
elif [[ "$current_status" == "reversed" ]]; then
echo ""
echo "↩️ Transaction was reversed"
exit 1
fi
# Wait before next attempt (unless this is the last attempt)
if [ $i -lt $max_attempts ]; then
sleep $interval
fi
done
echo ""
echo "⏱️ Transaction still processing after 2 minutes"
echo "Consider implementing webhook notifications instead of polling"
exit 1Implementation Guide:
This bash script demonstrates the complete flow. Here's how to implement it in your language:
1. Create HTTP Client Wrapper Class
FluidDebitTransaction class/module with:
- Constructor accepting API key
- Base URL configuration
- Common headers setup (Authorization, Content-Type)2. Implement Transaction Initiation Method
initiate(params):
- Validate required params: phone_number, bank_identifier, amount
- Set defaults: currency='GHS', metadata={}
- POST to /debit-requests/charge
- Return structured response: {success, reference, uuid, status}
- Handle HTTP errors gracefully3. Implement Status Query Method
getStatus(reference):
- GET /debit-requests/{reference}
- Parse JSON response
- Return transaction data object
- Throw error if request fails4. Implement Polling Method
pollUntilComplete(reference, options):
- Default: max_attempts=12, interval=10 seconds
- Loop up to max_attempts times:
- Call getStatus(reference)
- Check if status in ['completed', 'failed', 'reversed']
- If terminal state, return transaction
- If not terminal, wait interval seconds
- After max_attempts, throw timeout error5. Implement Error Handler
handleTransactionResult(transaction):
- Switch on transaction.status:
- 'completed': Mark order paid, send receipt
- 'failed': Check error_message for retry strategy
- Retryable: Schedule retry after delay
- Non-retryable: Notify customer, offer alternatives
- 'processing': Notify customer to check phone
- 'reversed': Handle refund/reversal logic6. Usage Pattern
1. Create client instance with API key
2. Call initiate() with transaction params
3. Store reference for future queries
4. Either:
a) Poll with pollUntilComplete() (development/testing)
b) Wait for webhook (production - recommended)
5. Handle final transaction state appropriatelyLanguage-Specific Notes:
JavaScript/Node.js:
- Use
fetch()or axios for HTTP requests - Use
async/awaitfor asynchronous operations - Use
setTimeout()with Promise for polling delays - Use ES6 classes for wrapper implementation
Python:
- Use
requestslibrary for HTTP - Use
time.sleep()for polling delays - Use classes or functions based on preference
- Type hints recommended for better IDE support
PHP:
- Use Guzzle or native curl for HTTP
- Use
sleep()for polling delays - Use classes for clean encapsulation
- Consider using typed properties (PHP 7.4+)
Ruby:
- Use RestClient or Net::HTTP for requests
- Use
sleep()for polling delays - Ruby classes with idiomatic method naming
- Use symbols for hash keys
Production Considerations:
- ⚠️ Use webhooks instead of polling for production systems
- Store
referenceanduuidin your database - Implement proper logging for all API interactions
- Use exponential backoff for retry logic
- Implement idempotency checks using
partner_reference - Handle webhook signature verification (see webhook docs)
- Consider using background jobs for async processing
- Monitor transaction success/failure rates
Best Practices Summary
Transaction Lifecycle Best Practices
✅ DO:
- Use webhooks for production systems instead of polling
- Implement idempotency using
partner_referenceto prevent duplicates - Store transaction
referenceanduuidfor future queries - Handle all transaction states (
pending,processing,completed,failed) - Implement retry logic for retryable errors
- Verify webhook signatures before processing
- Return
200 OKfrom webhook endpoints within 5 seconds - Use proper error handling and logging
- Test all scenarios in sandbox environment
❌ DON'T:
- Poll more frequently than every 10 seconds
- Poll indefinitely without timeout
- Assume transactions will complete immediately
- Ignore
processingstate (customer still deciding) - Retry non-retryable errors (insufficient funds, customer declined)
- Process duplicate webhooks (check
event_id) - Block webhook responses with slow processing
- Expose sensitive data in
metadata
Related Resources
- Create Charge - Initiate debit transactions
- Get Status - Query transaction status
- Webhook Overview - Real-time notifications
- Error Handling Guide - Complete error reference
- Idempotency Guide - Preventing duplicate transactions
- Testing Guide - Sandbox testing strategies