Payload Structure
Every FLUID webhook request includes a JSON payload with detailed information about the event. This page documents the complete structure and all available fields.
Top-Level Structure
All webhook payloads follow this consistent structure:
{
"event": "string", // Event type (e.g., "transaction.completed")
"event_id": "string", // Unique event identifier
"timestamp": "string", // ISO 8601 timestamp
"api_version": "string", // API version (e.g., "v1")
"data": { // Event-specific data
"transaction": { ... }, // Transaction details
"previous_status": "string", // Previous transaction status
"metadata": { ... }, // Custom metadata
"error_details": { ... } // Error details (failed transactions only)
}
}Field Reference
Root Fields
| Field | Type | Required | Description |
|---|---|---|---|
event | string | Yes | Event type that triggered the webhook (e.g., transaction.completed) |
event_id | string | Yes | Unique identifier for this event (format: evt_[24-char-hex]) |
timestamp | string | Yes | ISO 8601 timestamp when the event occurred (UTC) |
api_version | string | Yes | API version used for this webhook (currently v1) |
data | object | Yes | Event-specific data containing transaction and metadata |
data Object
| Field | Type | Required | Description |
|---|---|---|---|
transaction | object | Yes | Complete transaction details (see below) |
previous_status | string | No | Transaction status before this event (null for transaction.created) |
metadata | object | No | Custom metadata provided during transaction creation |
error_details | object | No | Error information (only present for transaction.failed) |
data.transaction Object
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | FLUID transaction identifier (format: dt_[id] for debits, ct_[id] for credits) |
uuid | string | Yes | UUID v4 universally unique identifier |
reference | string | Yes | FLUID-generated reference (format: FLU-YYYYMMDD-XXXXXX) |
partner_reference | string | Yes | Your unique transaction reference |
status | string | Yes | Current transaction status (see Transaction Statuses) |
amount | number | Yes | Transaction amount (decimal, 2 decimal places) |
currency | string | Yes | ISO 4217 currency code (currently only GHS supported) |
fee | number | Yes | Total transaction fee charged (decimal, 2 decimal places) |
narration | string | Yes | Transaction description/narration |
approval_method | string | No | Bank approval method (ussd, otp, biometric, pin) |
bank_reference | string | No | Bank's unique transaction identifier (available after submission to bank) |
error_message | string | No | Error message from bank (only present if transaction failed) |
created_at | string | Yes | ISO 8601 timestamp when transaction was created |
processed_at | string | No | ISO 8601 timestamp when transaction was submitted to bank |
completed_at | string | No | ISO 8601 timestamp when transaction was completed |
bank | object | Yes | Bank information (see below) |
customer | object | Yes | Customer information (see below) |
data.transaction.bank Object
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Full bank name (e.g., Example Bank Ghana, Sample Bank) |
identifier | string | Yes | FLUID bank code (e.g., EXB, SAM, DEM) |
country_code | string | Yes | ISO 3166-1 alpha-2 country code (e.g., GH, NG, KE) |
data.transaction.customer Object
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Customer full name |
phone_number | string | Yes | Customer phone number (E.164 format: +233XXXXXXXXX) |
data.error_details Object
Only present for transaction.failed events.
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | Human-readable error message |
code | string | Yes | Machine-readable error code (see Error Codes) |
Transaction Statuses
| Status | Description | Webhook Event |
|---|---|---|
created | Transaction initiated | transaction.created |
pending | Queued for bank processing | transaction.pending |
processing | Submitted to bank, awaiting confirmation | transaction.processing |
completed | Successfully processed by bank | transaction.completed |
failed | Processing failed | transaction.failed |
reversed | Completed transaction was reversed/refunded | transaction.reversed |
Error Codes
Error codes in data.error_details.code for failed transactions:
| Code | Description | Recommended Action |
|---|---|---|
insufficient_funds | Customer account has insufficient balance | Inform customer to add funds and retry |
invalid_account | Phone number or account number is invalid | Verify customer details and retry |
transaction_declined | Customer declined USSD/OTP prompt | Inform customer to approve next attempt |
timeout | Request timed out (network or bank delay) | Retry transaction after a few minutes |
network_error | Network connectivity issue | Retry transaction after connectivity restored |
unknown_error | Unspecified error from bank | Contact FLUID support with transaction reference |
Complete Examples
Example 1: Successful Transaction (completed)
{
"event": "transaction.completed",
"event_id": "evt_a1b2c3d4e5f6g7h8i9j0k1l2",
"timestamp": "2025-01-28T14:23:45Z",
"api_version": "v1",
"data": {
"transaction": {
"id": "dt_12345",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"reference": "FLU-20250128-ABC123",
"partner_reference": "ORDER-67890",
"status": "completed",
"amount": 100.0,
"currency": "GHS",
"fee": 1.5,
"narration": "Payment for Order #67890",
"approval_method": "ussd",
"bank_reference": "EXB-TXN-789012345",
"created_at": "2025-01-28T14:23:12Z",
"processed_at": "2025-01-28T14:23:20Z",
"completed_at": "2025-01-28T14:23:45Z",
"bank": {
"name": "Example Bank Ghana",
"identifier": "EXB",
"country_code": "GH"
},
"customer": {
"name": "Kwame Mensah",
"phone_number": "+233241234567"
}
},
"previous_status": "processing",
"metadata": {
"order_id": "67890",
"customer_email": "kwame@example.com",
"product_sku": "PROD-001",
"shipping_address": "123 Main St, Accra"
}
}
}Example 2: Failed Transaction (insufficient funds)
{
"event": "transaction.failed",
"event_id": "evt_b2c3d4e5f6g7h8i9j0k1l2m3",
"timestamp": "2025-01-28T15:10:32Z",
"api_version": "v1",
"data": {
"transaction": {
"id": "dt_12346",
"uuid": "660f9511-f39c-52e5-b827-557766551111",
"reference": "FLU-20250128-DEF456",
"partner_reference": "ORDER-67891",
"status": "failed",
"amount": 500.0,
"currency": "GHS",
"fee": 7.5,
"narration": "Payment for Order #67891",
"approval_method": "ussd",
"bank_reference": "MOB-TXN-123456789",
"error_message": "Insufficient funds in customer account",
"created_at": "2025-01-28T15:09:45Z",
"processed_at": "2025-01-28T15:10:10Z",
"completed_at": null,
"bank": {
"name": "Sample Bank",
"identifier": "SAM",
"country_code": "GH"
},
"customer": {
"name": "Ama Asante",
"phone_number": "+233247654321"
}
},
"previous_status": "processing",
"metadata": {
"order_id": "67891",
"customer_email": "ama@example.com",
"product_sku": "PROD-002"
},
"error_details": {
"message": "Insufficient funds in customer account",
"code": "insufficient_funds"
}
}
}Example 3: Transaction Created (initial state)
{
"event": "transaction.created",
"event_id": "evt_c3d4e5f6g7h8i9j0k1l2m3n4",
"timestamp": "2025-01-28T16:05:00Z",
"api_version": "v1",
"data": {
"transaction": {
"id": "dt_12347",
"uuid": "770g0622-g40d-63f6-c938-668877662222",
"reference": "FLU-20250128-GHI789",
"partner_reference": "ORDER-67892",
"status": "created",
"amount": 250.0,
"currency": "GHS",
"fee": 3.75,
"narration": "Payment for Order #67892",
"created_at": "2025-01-28T16:05:00Z",
"processed_at": null,
"completed_at": null,
"bank": {
"name": "Mobile Bank",
"identifier": "MOB",
"country_code": "GH"
},
"customer": {
"name": "Kofi Owusu",
"phone_number": "+233209876543"
}
},
"previous_status": null,
"metadata": {
"order_id": "67892",
"customer_email": "kofi@example.com"
}
}
}Field Availability by Event
Not all fields are available in every event. This table shows field availability across events:
| Field | created | pending | processing | completed | failed | reversed |
|---|---|---|---|---|---|---|
id | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
uuid | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
reference | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
partner_reference | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
status | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
amount | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
currency | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
fee | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
narration | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
approval_method | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
bank_reference | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
error_message | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
created_at | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
processed_at | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
completed_at | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
bank | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
customer | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
previous_status | ❌ (null) | ✅ | ✅ | ✅ | ✅ | ✅ |
metadata | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
error_details | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
Null vs Absent Fields
Fields marked ❌ may be absent from the payload or set to null. Always check for field existence before accessing nested properties.
Using Metadata
The metadata object contains custom data you provided when creating the transaction. This is useful for:
- Linking transactions to your internal records
- Storing customer information
- Tracking order details
- Adding context for business logic
Example: Processing with Metadata
function handleTransactionCompleted(payload) {
const transaction = payload.data.transaction;
const metadata = payload.data.metadata;
// Use metadata to link to your order
const orderId = metadata.order_id;
const customerEmail = metadata.customer_email;
// Update your order status
await db.orders.update(orderId, {
status: 'paid',
payment_reference: transaction.reference,
paid_at: transaction.completed_at
});
// Send confirmation email
await sendEmail(customerEmail, {
subject: 'Payment Successful',
template: 'payment-success',
data: {
amount: transaction.amount,
reference: transaction.reference,
order_id: orderId
}
});
}Metadata Storage
Metadata is stored exactly as provided. Ensure you validate and sanitize metadata before using it in your business logic.
Parsing Webhook Payloads
Node.js/Express Example
const express = require('express');
const app = express();
app.use(express.json()); // Parse JSON payloads
app.post('/webhooks/fluid', (req, res) => {
const payload = req.body;
// Access fields safely with optional chaining
const eventType = payload.event;
const transactionId = payload.data?.transaction?.id;
const transactionStatus = payload.data?.transaction?.status;
const amount = payload.data?.transaction?.amount;
const currency = payload.data?.transaction?.currency;
// Handle different event types
switch (eventType) {
case 'transaction.completed':
console.log(`Transaction ${transactionId} completed: ${amount} ${currency}`);
break;
case 'transaction.failed':
const errorCode = payload.data?.error_details?.code;
console.log(`Transaction ${transactionId} failed: ${errorCode}`);
break;
default:
console.log(`Received event: ${eventType}`);
}
res.status(200).json({ received: true });
});Python/Flask Example
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/fluid', methods=['POST'])
def handle_webhook():
payload = request.get_json()
# Access fields with .get() for safe access
event_type = payload.get('event')
transaction = payload.get('data', {}).get('transaction', {})
transaction_id = transaction.get('id')
status = transaction.get('status')
amount = transaction.get('amount')
currency = transaction.get('currency')
# Handle different event types
if event_type == 'transaction.completed':
print(f"Transaction {transaction_id} completed: {amount} {currency}")
handle_completed_transaction(transaction)
elif event_type == 'transaction.failed':
error_details = payload.get('data', {}).get('error_details', {})
error_code = error_details.get('code')
print(f"Transaction {transaction_id} failed: {error_code}")
handle_failed_transaction(transaction, error_details)
return jsonify({'received': True}), 200
def handle_completed_transaction(transaction):
# Update database, send notifications, etc.
pass
def handle_failed_transaction(transaction, error_details):
# Log failure, notify customer, etc.
passPHP Example
<?php
// webhook.php
// Read raw POST data
$payload = json_decode(file_get_contents('php://input'), true);
// Access fields with null coalescing operator
$eventType = $payload['event'] ?? null;
$transaction = $payload['data']['transaction'] ?? [];
$transactionId = $transaction['id'] ?? null;
$status = $transaction['status'] ?? null;
$amount = $transaction['amount'] ?? 0;
$currency = $transaction['currency'] ?? '';
// Handle different event types
switch ($eventType) {
case 'transaction.completed':
error_log("Transaction $transactionId completed: $amount $currency");
handleCompletedTransaction($transaction);
break;
case 'transaction.failed':
$errorDetails = $payload['data']['error_details'] ?? [];
$errorCode = $errorDetails['code'] ?? 'unknown';
error_log("Transaction $transactionId failed: $errorCode");
handleFailedTransaction($transaction, $errorDetails);
break;
default:
error_log("Received event: $eventType");
}
// Respond with 200 OK
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true]);
function handleCompletedTransaction($transaction) {
// Update database, send notifications, etc.
}
function handleFailedTransaction($transaction, $errorDetails) {
// Log failure, notify customer, etc.
}Data Types & Formats
Date/Time Format
All timestamp fields use ISO 8601 format with UTC timezone:
2025-01-28T14:23:45ZParse examples:
// JavaScript
const timestamp = new Date(payload.timestamp);
// Python
from datetime import datetime
timestamp = datetime.fromisoformat(payload['timestamp'].replace('Z', '+00:00'))
// PHP
$timestamp = new DateTime($payload['timestamp']);Number Format
All numeric fields (amount, fee) are decimals with 2 decimal places:
{
"amount": 100.50, // Not "100.5" or "100.500"
"fee": 1.50 // Not "1.5" or "1.500"
}Phone Number Format
Phone numbers use E.164 international format:
+233241234567 // GhanaNext Steps
See Also
- Webhooks Overview - Introduction to webhooks
- Error Handling Guide - Handle errors and failures
- API Reference - API documentation