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
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-deposittransaction. - Initiating a payout creates an
escrow-payouttransaction. - Initiating a refund creates an
escrow-refundtransaction.
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
balanceis 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_successfulorrefund_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:
-
Charge Successful (
escrow-depositsucceeds):- 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
- Seller balance increases by
-
Payout Successful (
escrow-payoutsucceeds):- Seller balance decreases by
-payoutAmount - Example: KES balance goes from 598 → 548 (for 50 KES payout)
- Seller balance decreases by
-
Refund Successful (
escrow-refundsucceeds):- Seller balance decreases by
-refundAmount - Example: KES balance goes from 598 → 568 (for 30 KES refund)
- Seller balance decreases by
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:
- Atomic Updates: All balance changes use Firestore atomic increments
- Event-Driven: Balance updates are triggered by actual transaction completions
- Audit Trail: All transactions are logged and can be queried for reconciliation
- 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 statusbalance: Current escrow balanceexternalReference: Your tracking reference
Status Values:
pending_charge- Escrow created, awaiting chargecharge_initiated- Charge request submittedcharge_successful- Funds successfully collectedcharge_failed- Charge attempt failedpayout_initiated- Payout request submittedpayout_successful- Payout completedpayout_failed- Payout failedrefund_initiated- Refund request submittedrefund_successful- Refund completedrefund_failed- Refund failedclosed- Escrow formally closed
Error Handling
Common Error Scenarios
-
Insufficient Escrow Balance
{ "error": "Insufficient funds in escrow. Available amount is 50.0" } -
Seller Not Found
{ "error": "Seller not found." } -
Unauthorized Seller Access
{ "error": "Unauthorized access." } -
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.
Updated 29 days ago
