Skip to main content

Overview

The Zuba ledger system implements a robust double-entry bookkeeping system that tracks all financial movements across the platform. Every transaction is recorded as transfers between named accounts, providing complete audit trails and real-time balance tracking.

Core Concepts

Accounts

Accounts represent financial entities in the system and are identified by unique names with currency support. When querying your balances, you’ll see:
Account Balance Response
{
  "currency": "EUR",
  "balance": 1250.50,
  "pendingIn": 100.00,
  "pendingOut": 50.00,
  "totalBalance": 1300.50,
  "isActive": true,
  "lastTransactionAt": "2026-01-12T12:00:00Z"
}
FieldDescription
currencyISO 4217 currency code
balanceAvailable balance (funds you can use)
pendingInIncoming funds being processed
pendingOutOutgoing funds being processed
totalBalanceTotal balance including pending
isActiveWhether the account is active
lastTransactionAtTimestamp of most recent transaction

Transactions

All financial movements are recorded as double-entry transactions. When querying transactions, you’ll see them from your account’s perspective as debits (money out) or credits (money in):
Transaction Structure
{
  "id": "572e779e-6d71-4bd7-91f1-57109c562b56",
  "type": "debit",
  "amount": 0.95411814,
  "currency": "EUR",
  "balanceAfter": 87.57104914,
  "counterparty": "ext:****3000",
  "status": "confirmed",
  "createdAt": "2026-01-12T12:50:48.792Z",
  "confirmedAt": "2026-01-12T12:50:48.791Z"
}
FieldDescription
idUnique transaction identifier
typedebit (money out) or credit (money in)
amountTransaction amount
currencyISO 4217 currency code
balanceAfterYour account balance after this transaction
counterpartyThe external counterparty, masked (e.g. ext:****3000). Internal platform accounts are omitted, so this field is absent on internal transfers (such as fees and FX)
isFeetrue when the entry is a Zuba service fee; lets you label fee rows without inspecting account names
statusTransaction status (pending, confirmed, failed, cancelled)
createdAtWhen the transaction was created
confirmedAtWhen the transaction was confirmed
payoutIdPresent on payout entries — the originating payout’s id
payoutStatusPresent on payout entries — the payout’s delivery status (created, queued, processing, paid, failed, cancelled)
txHashPresent on crypto entries — the on-chain transaction hash, for looking the transfer up on a block explorer
A payout’s ledger transaction is confirmed as soon as the funds are debited from your balance — it does not mean the beneficiary has been paid. Slow rails (e.g. SWIFT) can remain in flight for days after the debit confirms. Use payoutStatus to track delivery; paid means the funds arrived.

Transaction States

StatusDescriptionActions Available
pendingTransaction created, awaiting confirmationConfirm, Cancel
confirmedTransaction successfully processedView only
failedTransaction failed during processingRetry, Investigate
cancelledTransaction cancelled before processingView only

Balance Management

Real-time Balance Calculation

Account balances are calculated in real-time from confirmed transactions:
// Simplified balance calculation
const balance = confirmedCredits - confirmedDebits;

Multi-Currency Support

Each account maintains balances in a single currency. When querying all balances, you’ll receive a list of accounts for each currency:
Multi-Currency Response
{
  "userId": "auth0|123456",
  "clientId": "cc82fa1d-fc7a-478c-a734-d3bce40464e7",
  "balances": [
    { "currency": "EUR", "balance": 1000.00, "pendingIn": 0, "pendingOut": 0, "totalBalance": 1000.00 },
    { "currency": "USD", "balance": 1200.00, "pendingIn": 50.00, "pendingOut": 0, "totalBalance": 1250.00 }
  ],
  "totalValueEur": 2100.00,
  "primaryCurrency": "EUR"
}

Payment Flow Integration

Ledger transactions are created automatically by the platform when processing deposits and payouts. You cannot create transactions directly - they are generated as a result of your payment operations.

Payin Transaction Flow

When a customer makes a deposit, the platform settles the funds internally and credits your account automatically. From your perspective it lands as a single credit: You’ll see the credit appear in your transaction history. The internal account that funded it is omitted, so no counterparty is present:
Credit Transaction
{
  "id": "572e779e-6d71-4bd7-91f1-57109c562b56",
  "type": "credit",
  "amount": 100.00,
  "currency": "EUR",
  "balanceAfter": 1100.00,
  "status": "confirmed",
  "createdAt": "2026-01-12T12:00:00.000Z",
  "confirmedAt": "2026-01-12T12:00:00.000Z"
}

Payout Transaction Flow

For payouts, the system creates transaction chains including FX conversion when needed: You’ll see the debit in your transaction history. The internal accounts used to route and convert the payout are omitted, so no counterparty is present:
Debit Transaction
{
  "id": "683f880f-7e82-5ce8-b202-68210d673c67",
  "type": "debit",
  "amount": 100.00,
  "currency": "EUR",
  "balanceAfter": 900.00,
  "status": "confirmed",
  "createdAt": "2026-01-12T14:30:00.000Z",
  "confirmedAt": "2026-01-12T14:30:00.000Z"
}

Querying the Ledger

Account Balance

curl -X GET "https://api.sandbox.zuba.com/v1/ledger/balances/EUR" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const balance = await fetch('https://api.sandbox.zuba.com/v1/ledger/balances/EUR', {
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await balance.json();
console.log(`Balance: ${data.balance} ${data.currency}`);
import requests

balance = requests.get(
    'https://api.sandbox.zuba.com/v1/ledger/balances/EUR',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = balance.json()
print(f"Balance: {data['balance']} {data['currency']}")
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.sandbox.zuba.com/v1/ledger/balances/EUR"))
    .header("Authorization", "Bearer YOUR_ACCESS_TOKEN")
    .GET()
    .build();

HttpResponse<String> response = client.send(request,
    HttpResponse.BodyHandlers.ofString());

JsonObject data = new Gson().fromJson(response.body(), JsonObject.class);
System.out.println("Balance: " + data.get("balance").getAsString() +
    " " + data.get("currency").getAsString());

Transaction History

curl -X GET "https://api.sandbox.zuba.com/v1/ledger/transactions?currency=EUR&limit=50" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const response = await fetch('https://api.sandbox.zuba.com/v1/ledger/transactions?currency=EUR&limit=50', {
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
data.transactions.forEach(tx => {
  const direction = tx.type === 'credit' ? '+' : '-';
  console.log(`${direction}${tx.amount} ${tx.currency} → Balance: ${tx.balanceAfter}`);
});
import requests

response = requests.get(
    'https://api.sandbox.zuba.com/v1/ledger/transactions',
    params={'currency': 'EUR', 'limit': 50},
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
for tx in data['transactions']:
    direction = '+' if tx['type'] == 'credit' else '-'
    print(f"{direction}{tx['amount']} {tx['currency']} → Balance: {tx['balanceAfter']}")
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.sandbox.zuba.com/v1/ledger/transactions?currency=EUR&limit=50"))
    .header("Authorization", "Bearer YOUR_ACCESS_TOKEN")
    .GET()
    .build();

HttpResponse<String> response = client.send(request,
    HttpResponse.BodyHandlers.ofString());

JsonObject data = new Gson().fromJson(response.body(), JsonObject.class);
JsonArray transactions = data.getAsJsonArray("transactions");
transactions.forEach(tx -> {
    JsonObject transaction = tx.getAsJsonObject();
    String direction = transaction.get("type").getAsString().equals("credit") ? "+" : "-";
    System.out.println(direction + transaction.get("amount").getAsString() + " " +
        transaction.get("currency").getAsString() + " → Balance: " +
        transaction.get("balanceAfter").getAsString());
});

Audit & Compliance

Audit Trail

Every transaction maintains a complete audit trail:
  • Immutable Records: Transactions cannot be modified once confirmed
  • Timestamps: All operations are timestamped with precision
  • Metadata: Additional context stored with each transaction
  • Reference IDs: Link to external provider transactions

Compliance Features

  • Daily balance reconciliation across all accounts
  • Provider settlement matching and verification
  • Automated discrepancy detection and alerts
  • Historical balance reconstruction capabilities
  • Transaction history export for audits
  • Real-time balance monitoring and limits
  • Suspicious activity pattern detection
  • Automated regulatory filing support
  • Double-entry validation on every transaction
  • Balance consistency checks across all accounts
  • Automated reconciliation with external providers
  • Immutable transaction records with cryptographic hashing

Advanced Features

Currency Conversion Tracking

When a payout converts currency, you still see a single debit on your account. It carries both the amount debited from your balance and the amount sent to the beneficiary, via the sourceAmount/sourceCurrency and payoutAmount/payoutCurrency fields. Internal FX accounts are not surfaced, so no counterparty is present:
FX Payout Debit
{
  "id": "572e779e-6d71-4bd7-91f1-57109c562b56",
  "type": "debit",
  "amount": 100.00,
  "currency": "EUR",
  "balanceAfter": 900.00,
  "sourceAmount": 100.00,
  "sourceCurrency": "EUR",
  "payoutAmount": 108.00,
  "payoutCurrency": "USD",
  "status": "confirmed",
  "createdAt": "2026-01-12T12:50:48.792Z",
  "confirmedAt": "2026-01-12T12:50:48.791Z"
}
The FX rate applied to your transaction is visible in the payout details, not the ledger transaction itself.

Account Statements

For detailed reconciliation, you can generate account statements for a specific period:
curl -X GET "https://api.sandbox.zuba.com/v1/ledger/statement/EUR?startDate=2026-01-01&endDate=2026-01-31" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const response = await fetch(
  'https://api.sandbox.zuba.com/v1/ledger/statement/EUR?startDate=2026-01-01&endDate=2026-01-31',
  {
    headers: {
      'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
  }
);

const statement = await response.json();
console.log(`Opening balance: ${statement.openingBalance}`);
console.log(`Closing balance: ${statement.closingBalance}`);
console.log(`Total credits: ${statement.totalCredits}`);
console.log(`Total debits: ${statement.totalDebits}`);
Statement Response:
Account Statement
{
  "accountId": "123e4567-e89b-12d3-a456-426614174000",
  "accountName": "user:cc82fa1d-fc7a-478c-a734-d3bce40464e7",
  "currency": "EUR",
  "periodStart": "2026-01-01",
  "periodEnd": "2026-01-31",
  "openingBalance": "1000.00",
  "closingBalance": "1500.50",
  "totalCredits": "600.50",
  "totalDebits": "100.00",
  "transactions": [
    {
      "date": "2026-01-15T10:00:00Z",
      "credit": "500.00",
      "balanceAfter": "1500.00",
      "reference": "DEPOSIT-001",
      "transactionId": "abc123..."
    }
  ],
  "generatedAt": "2026-01-31T12:00:00Z"
}

Error Handling

Transaction Failures

When a payout or deposit fails, you may see a failed transaction in your history:
Failed Transaction
{
  "id": "572e779e-6d71-4bd7-91f1-57109c562b56",
  "type": "debit",
  "amount": 100.00,
  "currency": "EUR",
  "balanceAfter": 1000.00,
  "status": "failed",
  "createdAt": "2026-01-12T12:50:48.792Z"
}
Failed transactions do not affect your available balance. The platform automatically handles any necessary reversals.

Common Error Scenarios

ErrorDescriptionResolution
Insufficient fundsNot enough balance to complete payoutAdd funds to your account
Invalid beneficiaryBeneficiary details are incorrectUpdate beneficiary information
Provider errorExternal provider issueRetry or contact support

Integration Guidelines

Ledger transactions are created automatically by the platform when you create payouts or receive deposits. You cannot create transactions directly - use the Payouts API to initiate transfers.

Best Practices

  • Check balances before initiating payouts to avoid failures
  • Set up webhooks for real-time balance notifications
  • Use the statement endpoint for reconciliation
  • Monitor pending amounts for cash flow planning
  • Generate statements periodically for your records
  • Match ledger transactions with your internal systems
  • Keep track of pending transactions for accurate cash positions
  • Export transaction history for accounting integration
  • Use pagination (limit and offset) for large transaction histories
  • Filter by currency when querying specific account activity
  • Cache balance responses where real-time accuracy isn’t required
  • Handle rate limits gracefully with exponential backoff

Next Steps

First Payout Guide

Send your first payout with the Zuba API

Webhooks

Set up real-time notifications for payment events

Batch Payouts

Process multiple payouts efficiently

API Reference

Complete API documentation