Overview
The Store Wallet API manages financial operations for store owners including balance tracking, payout requests, transaction history, and earnings management. This system automatically processes order earnings and handles marketplace commission deductions.
Location: convex/stores/wallet.ts
Wallet Structure
Available Balance Funds available for immediate payout requests
Pending Balance Earnings from recent orders (held for security period)
Total Earnings Lifetime earnings from all completed orders
Payout History Complete record of all payout transactions
Get Store Wallet Balance
Retrieve comprehensive wallet information and statistics for a store.
Store ID to get wallet information for
TypeScript
JavaScript
Python
const wallet = await convex . query ( api . stores . wallet . getStoreWalletBalance , {
storeId: "j123456789"
});
{
"_id" : "w123456789" ,
"storeId" : "j123456789" ,
"availableBalance" : 1250.75 ,
"pendingBalance" : 150.25 ,
"totalEarnings" : 5000.00 ,
"totalOrders" : 42 ,
"totalPayouts" : 3500.00 ,
"totalCommissionPaid" : 245.50 ,
"minPayoutAmount" : 50.00 ,
"maxPayoutAmount" : 2000.00 ,
"lastOrderDate" : 1640995800000 ,
"lastPayoutDate" : 1640990000000 ,
"_creationTime" : 1640980000000
}
Request Payout
Request a payout from the store wallet to the registered bank account.
Store ID requesting the payout
Payout amount (must be within available balance and limits)
TypeScript
JavaScript
Python
const payoutId = await convex . mutation ( api . stores . wallet . requestPayout , {
storeId: "j123456789" ,
amount: 500.00
});
Get Payout History
Retrieve payout-related transactions by filtering wallet transactions.
Maximum number of payout records to return
const payoutHistory = await convex . query ( api . stores . wallet . getWalletTransactionHistory , {
storeId: "j123456789" ,
type: "payout" ,
limit: 20
});
[
{
"_id" : "wt987654321" ,
"storeWalletId" : "w123456789" ,
"type" : "payout" ,
"amount" : -500.00 ,
"description" : "Payout to bank account" ,
"payoutId" : "p123456789" ,
"balanceAfter" : 750.75 ,
"_creationTime" : 1640990000000
}
]
Get Wallet Transaction History
Retrieve detailed transaction history for a store wallet.
Maximum number of transaction records
Filter by transaction type: order_funding, commission_deduction, payout, payout_fee, refund, adjustment, penalty, bonus
// Get all transactions
const allTransactions = await convex . query ( api . stores . wallet . getWalletTransactionHistory , {
storeId: "j123456789" ,
limit: 50
});
// Get only order funding transactions
const orderTransactions = await convex . query ( api . stores . wallet . getWalletTransactionHistory , {
storeId: "j123456789" ,
type: "order_funding" ,
limit: 30
});
[
{
"_id" : "wt123456789" ,
"storeWalletId" : "w123456789" ,
"type" : "order_funding" ,
"amount" : 45.50 ,
"description" : "Order #o123456789 earnings" ,
"orderId" : "o123456789" ,
"balanceAfter" : 1250.75 ,
"metadata" : {
"orderTotal" : 50.97 ,
"platformCommission" : 5.47 ,
"storeEarnings" : 45.50
},
"_creationTime" : 1640995800000
},
{
"_id" : "wt987654321" ,
"storeWalletId" : "w123456789" ,
"type" : "commission_deduction" ,
"amount" : -5.47 ,
"description" : "Platform commission for order #o123456789" ,
"orderId" : "o123456789" ,
"balanceAfter" : 1205.25 ,
"_creationTime" : 1640995800000
},
{
"_id" : "wt555666777" ,
"storeWalletId" : "w123456789" ,
"type" : "payout" ,
"amount" : -500.00 ,
"description" : "Payout to bank account" ,
"payoutId" : "p123456789" ,
"balanceAfter" : 750.75 ,
"_creationTime" : 1640990000000
}
]
Wallet Dashboard Component
Complete wallet management interface:
import { useState } from 'react' ;
import { useQuery , useMutation } from 'convex/react' ;
import { api } from './convex/_generated/api' ;
import { LineChart , Line , XAxis , YAxis , CartesianGrid , Tooltip , ResponsiveContainer } from 'recharts' ;
interface WalletDashboardProps {
storeId : string ;
}
function WalletDashboard ({ storeId } : WalletDashboardProps ) {
const [ payoutAmount , setPayoutAmount ] = useState < string >( '' );
const [ showPayoutForm , setShowPayoutForm ] = useState ( false );
const wallet = useQuery ( api . stores . wallet . getStoreWalletBalance , { storeId });
const payoutHistory = useQuery ( api . stores . wallet . getWalletTransactionHistory , {
storeId ,
type: 'payout' ,
limit: 10
});
const transactions = useQuery ( api . stores . wallet . getWalletTransactionHistory , {
storeId ,
limit: 20
});
const requestPayout = useMutation ( api . stores . wallet . requestPayout );
const handlePayoutRequest = async () => {
if ( ! wallet || ! payoutAmount ) return ;
const amount = parseFloat ( payoutAmount );
if ( amount < wallet . minPayoutAmount ) {
alert ( `Minimum payout amount is $ ${ wallet . minPayoutAmount } ` );
return ;
}
if ( amount > wallet . availableBalance ) {
alert ( 'Insufficient available balance' );
return ;
}
try {
const payoutId = await requestPayout ({ storeId , amount });
console . log ( 'Payout requested:' , payoutId );
setPayoutAmount ( '' );
setShowPayoutForm ( false );
} catch ( error ) {
console . error ( 'Payout request failed:' , error );
alert ( 'Payout request failed. Please try again.' );
}
};
if ( ! wallet ) {
return < div > Loading wallet ...</ div > ;
}
// Prepare chart data
const chartData = transactions ?. filter ( t => t . type === 'order_funding' )
. slice ( 0 , 10 )
. reverse ()
. map ( t => ({
date: new Date ( t . _creationTime ). toLocaleDateString (),
earnings: t . amount ,
balance: t . balanceAfter
})) || [];
return (
< div className = "space-y-6" >
{ /* Wallet Overview */ }
< div className = "grid grid-cols-1 md:grid-cols-4 gap-4" >
< div className = "bg-white p-6 rounded-lg shadow" >
< div className = "flex items-center" >
< div className = "p-2 bg-green-100 rounded-lg" >
< svg className = "w-6 h-6 text-green-600" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</ svg >
</ div >
< div className = "ml-4" >
< p className = "text-sm font-medium text-gray-600" > Available Balance </ p >
< p className = "text-2xl font-bold text-green-600" > $ {wallet.availableBalance.toFixed( 2 ) } </ p >
</ div >
</ div >
</ div >
< div className = "bg-white p-6 rounded-lg shadow" >
< div className = "flex items-center" >
< div className = "p-2 bg-yellow-100 rounded-lg" >
< svg className = "w-6 h-6 text-yellow-600" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</ svg >
</ div >
< div className = "ml-4" >
< p className = "text-sm font-medium text-gray-600" > Pending Balance </ p >
< p className = "text-2xl font-bold text-yellow-600" > $ {wallet.pendingBalance.toFixed( 2 ) } </ p >
</ div >
</ div >
</ div >
< div className = "bg-white p-6 rounded-lg shadow" >
< div className = "flex items-center" >
< div className = "p-2 bg-blue-100 rounded-lg" >
< svg className = "w-6 h-6 text-blue-600" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</ svg >
</ div >
< div className = "ml-4" >
< p className = "text-sm font-medium text-gray-600" > Total Earnings </ p >
< p className = "text-2xl font-bold text-blue-600" > $ {wallet.totalEarnings.toFixed( 2 ) } </ p >
</ div >
</ div >
</ div >
< div className = "bg-white p-6 rounded-lg shadow" >
< div className = "flex items-center" >
< div className = "p-2 bg-purple-100 rounded-lg" >
< svg className = "w-6 h-6 text-purple-600" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
</ svg >
</ div >
< div className = "ml-4" >
< p className = "text-sm font-medium text-gray-600" > Total Payouts </ p >
< p className = "text-2xl font-bold text-purple-600" > $ {wallet.totalPayouts.toFixed( 2 ) } </ p >
</ div >
</ div >
</ div >
</ div >
{ /* Payout Request Section */ }
< div className = "bg-white rounded-lg shadow p-6" >
< div className = "flex justify-between items-center mb-4" >
< h2 className = "text-lg font-semibold text-gray-900" > Request Payout </ h2 >
{! showPayoutForm && (
< button
onClick = {() => setShowPayoutForm ( true )}
disabled = {wallet.availableBalance < wallet. minPayoutAmount }
className = "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Request Payout
</ button >
)}
</ div >
{ wallet . availableBalance < wallet . minPayoutAmount && (
< div className = "bg-yellow-50 p-4 rounded-lg mb-4" >
< p className = "text-yellow-800 text-sm" >
⚠️ Minimum payout amount is $ { wallet . minPayoutAmount . toFixed (2)}.
You need $ {( wallet . minPayoutAmount - wallet . availableBalance ). toFixed (2)} more to request a payout .
</ p >
</ div >
)}
{ showPayoutForm && (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-2" >
Payout Amount ( $ )
</ label >
< input
type = "number"
step = "0.01"
min = {wallet. minPayoutAmount }
max = {wallet. availableBalance }
value = { payoutAmount }
onChange = {(e) => setPayoutAmount (e.target.value)}
placeholder = { `Min: $ ${ wallet . minPayoutAmount } • Max: $ ${ wallet . availableBalance . toFixed ( 2 ) } ` }
className = "w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
< p className = "text-xs text-gray-500 mt-1" >
Available balance : $ { wallet . availableBalance . toFixed (2)}
</ p >
</ div >
< div className = "bg-gray-50 p-4 rounded" >
< h3 className = "font-medium text-gray-900 mb-2" > Payout Details </ h3 >
< div className = "text-sm space-y-1" >
< div className = "flex justify-between" >
< span > Requested Amount :</ span >
< span > $ { parseFloat ( payoutAmount || '0'). toFixed (2)}</ span >
</ div >
< div className = "flex justify-between" >
< span > Processing Fee :</ span >
< span > $5 . 00 </ span >
</ div >
< div className = "flex justify-between font-medium" >
< span > Net Amount :</ span >
< span > $ { Math . max (0, parseFloat ( payoutAmount || '0') - 5). toFixed (2)}</ span >
</ div >
</ div >
</ div >
< div className = "flex space-x-3" >
< button
onClick = { handlePayoutRequest }
disabled = {!payoutAmount || parseFloat ( payoutAmount ) < wallet . minPayoutAmount }
className = "flex-1 bg-green-600 text-white py-2 px-4 rounded hover:bg-green-700 disabled:opacity-50"
>
Request Payout
</ button >
< button
onClick = {() => {
setShowPayoutForm ( false );
setPayoutAmount ( '' );
}}
className = "px-4 py-2 text-gray-600 hover:text-gray-800"
>
Cancel
</ button >
</ div >
</ div >
)}
</ div >
{ /* Earnings Chart */ }
{ chartData . length > 0 && (
< div className = "bg-white rounded-lg shadow p-6" >
< h2 className = "text-lg font-semibold text-gray-900 mb-4" > Earnings Trend </ h2 >
< ResponsiveContainer width = "100%" height = { 300 } >
< LineChart data = { chartData } >
< CartesianGrid strokeDasharray = "3 3" />
< XAxis dataKey = "date" />
< YAxis />
< Tooltip />
< Line
type = "monotone"
dataKey = "balance"
stroke = "#3B82F6"
strokeWidth = { 2 }
dot = {{ fill : '#3B82F6' }}
/>
</ LineChart >
</ ResponsiveContainer >
</ div >
)}
{ /* Recent Payouts */ }
{ payoutHistory && payoutHistory . length > 0 && (
< div className = "bg-white rounded-lg shadow p-6" >
< h2 className = "text-lg font-semibold text-gray-900 mb-4" > Recent Payouts </ h2 >
< div className = "space-y-3" >
{ payoutHistory . map (( payout ) => (
< div key = {payout. _id } className = "flex items-center justify-between p-4 bg-gray-50 rounded" >
< div >
< p className = "font-medium text-gray-900" >
$ {payout.amount.toFixed( 2 ) }
</ p >
< p className = "text-sm text-gray-600" >
{ new Date ( payout . requestedAt ). toLocaleDateString ()}
{ payout . transactionReference && ` • $ { payout . transactionReference }`}
</ p >
</ div >
< span className = { `px-3 py-1 rounded-full text-sm font-medium ${
payout . status === 'completed' ? 'bg-green-100 text-green-800' :
payout . status === 'processing' ? 'bg-blue-100 text-blue-800' :
payout . status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
} ` } >
{ payout . status . charAt (0). toUpperCase () + payout . status . slice (1)}
</ span >
</ div >
))}
</ div >
</ div >
)}
{ /* Transaction History */ }
{ transactions && transactions . length > 0 && (
< div className = "bg-white rounded-lg shadow p-6" >
< h2 className = "text-lg font-semibold text-gray-900 mb-4" > Transaction History </ h2 >
< div className = "space-y-2" >
{ transactions . map (( transaction ) => (
< div key = {transaction. _id } className = "flex items-center justify-between p-3 border-b last:border-b-0" >
< div >
< p className = "font-medium text-gray-900" > {transaction. description } </ p >
< p className = "text-sm text-gray-600" >
{ new Date ( transaction . _creationTime ). toLocaleString ()}
</ p >
</ div >
< div className = "text-right" >
< p className = { `font-medium ${
transaction . amount > 0 ? 'text-green-600' : 'text-red-600'
} ` } >
{ transaction . amount > 0 ? '+' : '' } $ {transaction.amount.toFixed( 2 ) }
</ p >
< p className = "text-sm text-gray-500" >
Balance : $ {transaction.balanceAfter.toFixed( 2 ) }
</ p >
</ div >
</ div >
))}
</ div >
</ div >
)}
</ div >
);
}
export default WalletDashboard ;
Payout Processing Timeline
Request Submitted
Store owner submits payout request through dashboard
Admin Review
Platform admin reviews request and bank account details
Processing
Payment is initiated to store’s registered bank account
Completed
Funds transferred successfully, confirmation sent to store
Transaction Types
Order Funding Type : order_funding
Description : Earnings from completed orders
Effect : Increases wallet balance
Commission Deduction Type : commission_deduction
Description : Platform commission on orders
Effect : Decreases wallet balance
Payout Type : payout
Description : Funds transferred to bank account
Effect : Decreases wallet balance
Payout Fee Type : payout_fee
Description : Processing fee for payouts
Effect : Decreases wallet balance
Refund Type : refund
Description : Order refunds processed
Effect : Decreases wallet balance
Adjustment Type : adjustment
Description : Manual balance adjustments
Effect : Can increase or decrease balance
Error Handling
Status Code : 400{
"error" : "Insufficient available balance" ,
"requested" : 1000.00 ,
"available" : 750.50
}
Status Code : 400{
"error" : "Amount below minimum payout threshold" ,
"requested" : 25.00 ,
"minimum" : 50.00
}
Status Code : 400{
"error" : "Bank account details required for payout" ,
"message" : "Please add your bank account information in store settings"
}
Status Code : 404{
"error" : "Store wallet not found" ,
"storeId" : "j123456789"
}
Wallet balances are updated in real-time as orders are processed. Pending balances are held for a security period before becoming available for payout.
Set up bank account details before requesting payouts. Processing typically takes 1-3 business days depending on the bank and amount.