Skip to content

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:

json
{
  "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

FieldTypeRequiredDescription
eventstringYesEvent type that triggered the webhook (e.g., transaction.completed)
event_idstringYesUnique identifier for this event (format: evt_[24-char-hex])
timestampstringYesISO 8601 timestamp when the event occurred (UTC)
api_versionstringYesAPI version used for this webhook (currently v1)
dataobjectYesEvent-specific data containing transaction and metadata

data Object

FieldTypeRequiredDescription
transactionobjectYesComplete transaction details (see below)
previous_statusstringNoTransaction status before this event (null for transaction.created)
metadataobjectNoCustom metadata provided during transaction creation
error_detailsobjectNoError information (only present for transaction.failed)

data.transaction Object

FieldTypeRequiredDescription
idstringYesFLUID transaction identifier (format: dt_[id] for debits, ct_[id] for credits)
uuidstringYesUUID v4 universally unique identifier
referencestringYesFLUID-generated reference (format: FLU-YYYYMMDD-XXXXXX)
partner_referencestringYesYour unique transaction reference
statusstringYesCurrent transaction status (see Transaction Statuses)
amountnumberYesTransaction amount (decimal, 2 decimal places)
currencystringYesISO 4217 currency code (currently only GHS supported)
feenumberYesTotal transaction fee charged (decimal, 2 decimal places)
narrationstringYesTransaction description/narration
approval_methodstringNoBank approval method (ussd, otp, biometric, pin)
bank_referencestringNoBank's unique transaction identifier (available after submission to bank)
error_messagestringNoError message from bank (only present if transaction failed)
created_atstringYesISO 8601 timestamp when transaction was created
processed_atstringNoISO 8601 timestamp when transaction was submitted to bank
completed_atstringNoISO 8601 timestamp when transaction was completed
bankobjectYesBank information (see below)
customerobjectYesCustomer information (see below)

data.transaction.bank Object

FieldTypeRequiredDescription
namestringYesFull bank name (e.g., Example Bank Ghana, Sample Bank)
identifierstringYesFLUID bank code (e.g., EXB, SAM, DEM)
country_codestringYesISO 3166-1 alpha-2 country code (e.g., GH, NG, KE)

data.transaction.customer Object

FieldTypeRequiredDescription
namestringYesCustomer full name
phone_numberstringYesCustomer phone number (E.164 format: +233XXXXXXXXX)

data.error_details Object

Only present for transaction.failed events.

FieldTypeRequiredDescription
messagestringYesHuman-readable error message
codestringYesMachine-readable error code (see Error Codes)

Transaction Statuses

StatusDescriptionWebhook Event
createdTransaction initiatedtransaction.created
pendingQueued for bank processingtransaction.pending
processingSubmitted to bank, awaiting confirmationtransaction.processing
completedSuccessfully processed by banktransaction.completed
failedProcessing failedtransaction.failed
reversedCompleted transaction was reversed/refundedtransaction.reversed

Error Codes

Error codes in data.error_details.code for failed transactions:

CodeDescriptionRecommended Action
insufficient_fundsCustomer account has insufficient balanceInform customer to add funds and retry
invalid_accountPhone number or account number is invalidVerify customer details and retry
transaction_declinedCustomer declined USSD/OTP promptInform customer to approve next attempt
timeoutRequest timed out (network or bank delay)Retry transaction after a few minutes
network_errorNetwork connectivity issueRetry transaction after connectivity restored
unknown_errorUnspecified error from bankContact FLUID support with transaction reference

Complete Examples

Example 1: Successful Transaction (completed)

json
{
  "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)

json
{
  "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)

json
{
  "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:

Fieldcreatedpendingprocessingcompletedfailedreversed
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

javascript
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

javascript
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

python
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.
    pass

PHP Example

php
<?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:45Z

Parse examples:

javascript
// 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:

json
{
  "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  // Ghana

Next Steps

Signature Verification →

Secure your webhook endpoint by verifying HMAC signatures.

Event Types →

Explore all available webhook events and when they're triggered.

Retry Logic →

Understand how FLUID handles failed webhook deliveries.


See Also