Wallet guide
The wallet is a server-managed financial account, not a client-side balance object. This guide explains how it connects to the marketplace account and how to integrate common flows without treating the SDK or browser as payment authority.
One marketplace account, one user wallet identity#
The signed-in marketplace account has a stable user ID (admin._id). The backend creates or resolves the user's wallet against that same identity. In the intended shared deployment, the marketplace and SDK backend point to the same MongoDB database, so the account, profile, subscriptions, and personal wallet are shared. Separate database targets mean separate identities and wallet records.
Use self-scoped calls for the signed-in user's wallet:
const wallet = await sdk.wallet.myWallet('ZAR');
const transactions = await sdk.wallet.myTransactions('ZAR');These calls do not take a userId; the backend reads the actor from the access token. Prefer this to threading a user ID through a client app. Do not confuse the user's wallet with a store wallet: store wallets are separately keyed to the store. Store-wallet reads require an active membership with VIEW_FINANCIALS permission or the owner role; platform admins retain their existing bypass. The backend checks this before resolving the wallet, and supplying a store ID alone does not grant access.
Wallets and subscriptions#
| Flow | Wallet owner | Notes |
|---|---|---|
| Personal balance, user API subscription | Marketplace user | A developer API plan is tied to the user account and uses the user's wallet for paid plans. |
| Store subscription | Store | Paid store plans must be paid by that same store's wallet; server checks store ownership. |
| Sales proceeds | Store or configured product owner | Payout can be held and released according to order/service receipt rules. |
Registering an account attempts to provision the personal wallet and default user subscription. That does not add funds. Use sdk.subscriptions.userContext(...) / sdk.apiSubscriptions.plans(...) to inspect subscription context and API plans; the backend enforces entitlements.
Balance model and currency#
Each wallet has one canonical spendable amount in its base currency. Transaction history retains the original request currency and conversion metadata for audit. A converted display balance is for presentation; it is not another independently spendable balance. ZAR is the platform's primary payment currency, while some operations support other currencies under backend conversion and policy.
Never calculate a charge, available balance, or payment result from frontend exchange rates. Ask the server for current fees, limits, minimums, and transaction status:
const wallet = await sdk.wallet.myWallet('ZAR');
const fee = await sdk.wallet.calculateFee({
transactionType: 'TRANSFER',
amount: 100,
currency: 'ZAR',
});
const minimum = await sdk.wallet.minimumAmount('TRANSFER', 'ZAR');
const dailyLimits = await sdk.wallet.dailyLimit('ZAR');Fee and limit configuration can change on the server; do not hard-code fee percentages into a standalone app.
Funding with a hosted Ozow flow#
For wallet deposits, the SDK requests server-generated payment data. Use Ozow's official hosted modal integration, not a custom payment iframe/form:
const started = await sdk.wallet.startOzowDepositModal(userId, { amount: 250 });
// On an explicit user action, pass the returned URL and post data to
// the official Ozow integration in your UI.
const ozow = new window.Ozow();
ozow.createPaymentModal(started.paymentUrl, started.postData);Keep the Ozow private key and hash generation on the backend. A browser return URL/callback is only a user-interface signal. Confirm payment through the backend/provider reconciliation before showing the deposit as available. Never credit a wallet based on a client callback.
Transfers#
const result = await sdk.wallet.transfer(userId, {
toUserId: 'recipient-account-number-phone-or-user-id',
amount: 50,
currency: 'ZAR',
description: 'Invoice 104',
});The SDK generates an idempotency key and reuses it for the same retry inside that SDK instance. If the app must retry after a restart, supply and persist a stable idempotencyKey for the same logical transfer. Never generate a new key just because the network timed out; that can turn a retry into a second financial action.
The backend validates the sender, recipient, amount, fees, limits, wallet freeze state, and records ledger activity. Atomic wallet transfers require MongoDB transaction support (a replica set or sharded cluster); a standalone MongoDB server is not sufficient for that flow.
Withdrawals#
The SDK exposes sdk.wallet.withdraw(userId, input, idempotencyKey?, currencyCode?). Treat this as a request into the backend's withdrawal workflow, not as proof that money reached a bank. Supply the DTO required by your deployed schema, preserve the same key across retries, and display the server's resulting status. Staff/provider processing can continue after the API call returns.
Never log bank details or use user-supplied account IDs as proof of ownership. Use only the signed-in account's own withdrawal requests and follow the masking/status behavior of the backend.
Holds, orders, and payout release#
Sales may pass through a store hold rather than immediately becoming withdrawable. Buyer receipt confirmation is the payout-release trigger for held store funds. Scheduled orders must follow their specific receipt-confirmation flow; the seven-day fallback is server-side safety behavior, not a client timer. Your UI may show server-provided held and available totals, but must not mark a payout released itself.
Frozen wallets, errors, and support#
Wallet freezes, fraud review, ledger recording, fees, and minimum/daily limits are server-owned. If a wallet is frozen or a transaction is declined, show a clear status and use the backend's safe error response. Do not retry indefinitely, unfreeze a wallet locally, or try a different identity to bypass the restriction.
For sensitive or high-risk payment flows, sdk.wallet.initiateSecurePayment(...) starts a multi-party verification request; it is not a card-gateway checkout or a completed payment. Continue only after querying trusted server status and receiving the appropriate terminal state.
Integration checklist#
- Send the API key required by the deployed server and the signed-in user's access token for authenticated wallet operations.
- Prefer
myWallet()andmyTransactions()for the current user. - Keep private API keys, access tokens, bank details, and provider secrets out of browser bundles and logs.
- Use stable idempotency keys and retry only the same logical operation.
- Let the backend calculate totals, currency conversion, fees, limits, and payment completion.
- Verify provider results server-side before updating balances or fulfilling an order.
- Read the standalone account setup, flow locks, and required/optional field reference alongside this guide.