Escrow & Seller Balance Flows

The Escrows API provides a secure way to hold funds in escrow for business-to-business (B2B) transactions. It supports charging customers, holding funds, and disbursing payouts or refunds as needed.

This guide walks you through the entire lifecycle of a HoneyCoin escrow transaction and seller balance management. The process is asynchronous and event-driven. You will make API calls to initiate actions, and then listen for webhooks to receive the results and determine the next step.

Table of Contents

  1. Core Concepts
  2. Escrow Transaction Flow
  3. Seller Balance Management
  4. Webhook Events
  5. Error Handling

Core Concepts

How Status Changes Work: Transactions Drive Escrows

An Escrow's status is directly controlled by the status of its associated Transactions.

  • Initiating a charge creates an escrow-deposit transaction.
  • Initiating a payout creates an escrow-payout transaction.
  • Initiating a refund creates an escrow-refund transaction.

When one of these transactions changes state (e.g., from pending to successful), it automatically updates the parent Escrow's status. This update triggers the escrow_updated webhook, which is your signal to proceed or retry.

Seller Balance System

If an escrow includes a sellerId, the system automatically tracks seller balances:

  • Charge successful: Seller balance increases by the escrow amount
  • Payout successful: Seller balance decreases by the payout amount
  • Refund successful: Seller balance decreases by the refund amount

Balances are tracked per currency in the seller's aggregatedBalance object.


Escrow Transaction Flow

Step 1: Create the Escrow ➡️

First, create the escrow transaction. This defines the sender, receiver, and amount. The escrow is created with an initial status of pending_charge.

Action: POST /api/production/v1/escrows

Request Body:

{
    "amount": 100,
    "chargeCurrency": "KES",
    "receiverCurrency": "KES",
    "externalReference": "test1f3256",
    "sellerId": "seller-uuid-here", // Optional: Include for seller balance tracking
    "chargeDetails": {
        "method": "momo",
        "firstName": "Billy",
        "lastName": "Batson",
        "country": "KE",
        "email": "[email protected]",
        "phoneNumber": "254712345678",
        "momoOperatorId": "mpesa"
    },
    "payoutDetails": {
        "method": "momo",
        "country": "KE",
        "payoutMethod": {
            "accountName": "Virgil Hawkins",
            "accountNumber": "254712345678"
        }
    }
}

The API response will contain the id for this escrow. Save this id. You'll receive your first escrow_updated webhook confirming the pending_charge status.

Step 2: Fund the Escrow 💰

Using the escrowId from Step 1, initiate the charge to collect funds from the sender.

Action: POST /charges/{escrowId}

This call simply starts the process. The escrow status will change to charge_initiated.

Step 3: Listen for the Charge Result Webhook 👂

The escrow-deposit transaction will eventually settle as successful or failed. This settlement updates the escrow's status and triggers a webhook.

Action: Check the status field in the escrow_updated webhook payload.

// Charge Successful
{  
  "event": "escrow_updated",  
  "data": {  
    "id": "escrow-id-123", 
    "publicKey": "test-key", 
    "status": "charge_successful", // ←-- Caused by the deposit transaction succeeding.  
    "balance": 100.0, // ←-- Escrow now holds funds
    "externalReference": "order-ABC-456"  
  }  
}
// Charge Failed
{  
  "event": "escrow_updated",  
  "data": {  
    "id": "escrow-id-123", 
    "publicKey": "test-key", 
    "status": "charge_failed", // ←-- Caused by the deposit transaction failing.  
    "balance": 0.0,
    "externalReference": "order-ABC-456"  
  }  
}

If the deposit transaction fails, you'll receive a webhook where the status is charge_failed and you can retry Step 2. Once the status is charge_successful, you can disburse the funds.

Step 4: Disburse the Funds ✅

With the funds held securely, you can now trigger a disbursement—either a payout to the receiver or a refund to the sender.

Path A: Payout to the Receiver

Action: POST /payouts/{escrowId}

To pay out a partial amount, include it in the body. For a full payout, send an empty body. To override the default escrow payout details used in the initial escrow creation flow, pass a new payout details object. here.

{}
{  
  "amount": 50.0  
}
{  
	"payoutDetails": {
        "method": "momo",
        "country": "KE",
        "payoutMethod": {
            "accountName": "Terry McGinnis",
            "accountNumber": "254712345678"
        }
    }
}
{
    "amount": 50,
    "payoutDetails": {
        "method": "momo",
        "country": "KE",
        "payoutMethod": {
            "accountName": "Terry McGinnis",
            "accountNumber": "254712345678"
        }
    }
}

Path B: Refund to the Sender ↩️

Action: POST /refunds/{escrowId}

{}
{  
  "amount": 30.0  
}

Step 5: Listen for Disbursement Results & Check the Balance 👂

Each disbursement transaction will trigger an escrow_updated webhook. Crucially, you must check both the status and the balance to know the state of the escrow.

Action: Inspect the status and balance from the webhook payload.

{  
  "event": "escrow_updated",  
  "data": {  
    "id": "escrow-id-123",  
    "publicKey": "test-key", 
    "status": "payout_successful",  
    "balance": 50.0, // ←-- The remaining balance after payout
    "externalReference": "order-ABC-456"  
  }  
}

Step 6: Repeat or Conclude the Flow 🔁

Now, decide what to do next based on the escrow's balance.

  • If the balance is greater than zero: The escrow is still active. You can return to Step 4 to initiate another payout or refund.
  • If the balance is zero: The escrow is fully depleted, and its lifecycle is complete. The final status will typically be payout_successful or refund_successful.

Step 7: (Optional) Formally Close the Escrow 🚪

Closing an escrow is a final, irreversible action that formally marks its lifecycle as complete.

Prerequisites:
Before you can close an escrow, two conditions must be met:

  • The escrow balance must be zero.
  • All associated transactions must be settled (i.e., none can have a pending status).

Action: POST /escrows/{id}/close

Upon success, the escrow status will be set to closed, and you will receive one last escrow_updated webhook.


Seller Balance Management

Overview

The seller balance system automatically tracks cumulative balances for sellers across all their escrows. Balances are maintained per currency and updated in real-time as escrow transactions complete.

Seller Creation

Before creating escrows with seller tracking, you must first create a seller profile.

Action: POST /api/production/v1/escrows/sellers

{
    "name": "John's Coffee Shop",
    "email": "[email protected]",
    "currency": "KES",
    "defaultPayoutDetails": {
        "method": "momo",
        "country": "KE",
        "payoutMethod": {
            "accountName": "John Doe",
            "accountNumber": "254712345678"
        }
    }
}

Response:

{
    "success": true,
    "data": {
        "id": "seller-uuid-123",
        "name": "John's Coffee Shop",
        "email": "[email protected]",
        "currency": "KES",
        "aggregatedBalance": {},
        "defaultPayoutDetails": { ... },
        "createdAt": 1699123456789,
        "updatedAt": 1699123456789
    }
}

How Seller Balances Update

Seller balances automatically update when escrow transactions complete:

  1. Charge Successful (escrow-deposit succeeds):

    • Seller balance increases by +receiverAmount - escrowFees
    • Example: For 100 KES charge with 2 KES escrow fee → Seller balance increases by 98 KES
    • Note: Balance tracks the net amount that will be available for payout
  2. Payout Successful (escrow-payout succeeds):

    • Seller balance decreases by -payoutAmount
    • Example: KES balance goes from 598 → 548 (for 50 KES payout)
  3. Refund Successful (escrow-refund succeeds):

    • Seller balance decreases by -refundAmount
    • Example: KES balance goes from 598 → 568 (for 30 KES refund)

Multi-Currency Support

Sellers can have balances in multiple currencies:

{
    "aggregatedBalance": {
        "KES": 1250.50,
        "USD": 75.25,
        "NGN": 45000.00
    }
}

Seller Balance Queries

Get Seller Details (including balance)

Action: GET /api/production/v1/escrows/sellers/{sellerId}

Response:

{
    "success": true,
    "data": {
        "id": "seller-uuid-123",
        "name": "John's Coffee Shop",
        "email": "[email protected]",
        "currency": "KES",
        "aggregatedBalance": {
            "KES": 1250.50,
            "USD": 75.25
        },
        "defaultPayoutDetails": { ... },
        "createdAt": 1699123456789,
        "updatedAt": 1699123456789
    }
}

Get All Sellers

Action: GET /api/production/v1/escrows/sellers?currency=KES&limit=50

Get Seller Transaction History

Action: GET /api/production/v1/escrows/sellers/{sellerId}/transactions?status=successful

Response:

{
    "success": true,
    "data": [
        {
            "id": "tx-123",
            "transactionId": "tx-123",
            "type": "escrow-deposit",
            "senderAmount": 100,
            "senderCurrency": "KES",
            "receiverAmount": 100,
            "receiverCurrency": "KES",
            "chargeStatus": "successful",
            "createdAt": 1699123456789,
            "additionalData": {
                "escrowId": "escrow-456"
            }
        }
    ]
}

Balance Reconciliation

The system maintains balance integrity through:

  1. Atomic Updates: All balance changes use Firestore atomic increments
  2. Event-Driven: Balance updates are triggered by actual transaction completions
  3. Audit Trail: All transactions are logged and can be queried for reconciliation
  4. Real-time Triggers: Firebase Functions ensure immediate balance updates

Webhook Events

escrow_updated

Triggered whenever an escrow's status or balance changes.

{
    "event": "escrow_updated",
    "data": {
        "id": "escrow-id-123",
        "publicKey": "user-public-key",
        "status": "charge_successful",
        "balance": 100.0,
        "externalReference": "order-ABC-456"
    }
}

Key Fields:

  • status: Current escrow status
  • balance: Current escrow balance
  • externalReference: Your tracking reference

Status Values:

  • pending_charge - Escrow created, awaiting charge
  • charge_initiated - Charge request submitted
  • charge_successful - Funds successfully collected
  • charge_failed - Charge attempt failed
  • payout_initiated - Payout request submitted
  • payout_successful - Payout completed
  • payout_failed - Payout failed
  • refund_initiated - Refund request submitted
  • refund_successful - Refund completed
  • refund_failed - Refund failed
  • closed - Escrow formally closed

Error Handling

Common Error Scenarios

  1. Insufficient Escrow Balance

    {
        "error": "Insufficient funds in escrow. Available amount is 50.0"
    }
    
  2. Seller Not Found

    {
        "error": "Seller not found."
    }
    
  3. Unauthorized Seller Access

    {
        "error": "Unauthorized access."
    }
    
  4. Escrow Already Closed

    {
        "error": "Escrow is closed."
    }
    

Retry Logic

  • Failed charges can be retried using the same escrow
  • Failed payouts/refunds can be retried if escrow has sufficient balance
  • Use exponential backoff for webhook failures
  • Monitor webhook delivery status in your system

Balance Integrity

The system ensures balance integrity through:

  • Atomic database operations
  • Event-driven updates (only completed transactions affect balances)
  • Comprehensive logging for audit trails
  • Automatic rollback on transaction failures

Example Complete Flow with Seller Balance

Here's a complete example showing how seller balances update throughout an escrow lifecycle:

// 1. Initial seller balance
{
    "sellerId": "seller-123",
    "aggregatedBalance": { "KES": 500.0 }
}

// 2. Create escrow with sellerId (100 KES charge, 2 KES escrow fee)
POST /escrows
{
    "amount": 100,
    "sellerId": "seller-123",
    // ... other fields
}

// 3. Charge successful → Balance increases by receiverAmount (after fees)
// Webhook: escrow_updated (status: "charge_successful", balance: 98)
// Seller balance: KES 500 → 598 (increased by 98, not 100)

// 4. Partial payout → Balance decreases
POST /payouts/escrow-id { "amount": 60 }
// Webhook: escrow_updated (status: "payout_successful", balance: 38)
// Seller balance: KES 598 → 538

// 5. Refund remaining → Balance decreases  
POST /refunds/escrow-id { "amount": 38 }
// Webhook: escrow_updated (status: "refund_successful", balance: 0)
// Seller balance: KES 538 → 500

// Final state: Seller balance back to original 500 KES
// Net effect: +98 (from charge) -60 (payout) -38 (refund) = 0

This flow demonstrates how seller balances accurately track the net effect of all escrow operations, providing real-time financial visibility for marketplace operators.