Getting Started
This guide walks you through the entire lifecycle of a Cashless 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.
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.
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.
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.
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.
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.
With the funds held securely, you can now trigger a disbursement—either a payout to the receiver or a refund to the sender.
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"
}
}
}
Action: POST /refunds/{escrowId}
{}
{
"amount": 30.0
}
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"
}
}
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.
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.
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.
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
}
}
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
Sellers can have balances in multiple currencies:
{
"aggregatedBalance": {
"KES": 1250.50,
"USD": 75.25,
"NGN": 45000.00
}
}
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
}
}
Action: GET /api/production/v1/escrows/sellers?currency=KES&limit=50
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"
}
}
]
}
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
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
-
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." }
- 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
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
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 } }
// Final state: Seller balance back to original 500 KES
This flow demonstrates how seller balances accurately track the net effect of all escrow operations, providing real-time financial visibility for marketplace operators.
Updated about 3 hours ago
