Skip to content
ExponaBLOG
PAYMENT ENGINEERING & STRIPE INTEGRATION · DAY 032 / 100

Payouts explained: a practical Payments & Stripe guide

When money reaches your bank.

Expona Admin8 min read

Payouts explained: a practical Payments & Stripe guide

Every payment your platform receives doesn't instantly land in your bank account. Between customer card swipe and your morning bank balance sits a process that groups transactions, deducts fees, and moves money according to a schedule you configure. Understanding payouts is fundamental to building reliable payment systems and avoiding the cash-flow surprises that catch teams off guard.

We'll walk through how Stripe groups balance transactions into payouts, how fees reduce what you actually receive, why payout schedules matter for your business, and how to reconcile payments with bank statements so your accounting team sleeps soundly.

Balance transactions group into payouts

Every time a payment succeeds, a fee is charged, or a refund is issued, Stripe creates a balance transaction. Think of balance transactions as atomic ledger entries. They're immutable records that represent money moving in or out of your Stripe balance.

A payout is a collection of these balance transactions bundled together and moved to your bank account. Stripe groups transactions that settle within the same payout window. By default, payouts happen daily, but you can configure this schedule based on your business needs.

Here's a simplified day in the life of your Stripe balance:

  • 09:15 — Customer A pays $50. Stripe creates a balance transaction: +$50 (minus your 2.9% + $0.30 fee).
  • 14:42 — Customer B pays $120. Another balance transaction: +$120 (minus fees).
  • 16:30 — You refund Customer A $25. Stripe creates a balance transaction: -$25.
  • 18:00 — Stripe batches all transactions from that day and initiates a payout to your bank account.

Let's see how to retrieve balance transactions and group them by payout in code:

typescript
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

async function listBalanceTransactionsForPayout(payoutId: string) {
  const transactions = await stripe.balanceTransactions.list({
    payout: payoutId,
    limit: 100,
  });

  console.log(`Payout ${payoutId} contains ${transactions.data.length} transactions`);
  
  let totalAmount = 0;
  transactions.data.forEach((txn) => {
    console.log(`- Type: ${txn.type}, Amount: ${txn.amount}, Fee: ${txn.fee}`);
    totalAmount += txn.net;
  });

  console.log(`Net amount for payout: ${totalAmount}`);
  return transactions.data;
}

// Retrieve a recent payout
const payouts = await stripe.payouts.list({ limit: 1 });
if (payouts.data.length > 0) {
  await listBalanceTransactionsForPayout(payouts.data[0].id);
}

Notice the relationship: each balance transaction has a payout field that links it to the payout it belongs to. The net field on a balance transaction is the amount after fees—what actually contributes to your payout total.

Fees are deducted before payout

This is where money disappears if you're not paying attention. Stripe doesn't send you the full payment amount. It deducts processing fees, and sometimes other charges, before the payout leaves their system.

For card payments, the typical fee structure is 2.9% + $0.30 per transaction. If a customer pays $100, Stripe keeps $2.90 + $0.30 = $3.20, and you receive $96.80. That's the net amount that appears in your payout.

But fees aren't just card processing. Stripe also charges for:

  • Refund fees: Usually charged at the same rate as the original transaction fee.
  • Chargeback fees: $15 USD per chargeback (plus any amount lost to the customer).
  • ACH return fees: $1 USD for ACH returns (if you use bank transfers).
  • Connect application fees: If you're using Stripe Connect, platform fees reduce the payout.

Here's how to inspect fees on a balance transaction:

typescript
async function analyzeFeesInPayout(payoutId: string) {
  const transactions = await stripe.balanceTransactions.list({
    payout: payoutId,
    limit: 100,
  });

  let cardFees = 0;
  let refundFees = 0;
  let chargebackFees = 0;
  let grossAmount = 0;
  let netAmount = 0;

  transactions.data.forEach((txn) => {
    grossAmount += txn.amount;
    netAmount += txn.net;

    if (txn.type === 'charge') {
      cardFees += txn.fee;
    } else if (txn.type === 'adjustment' && txn.description?.includes('Refund')) {
      refundFees += Math.abs(txn.fee);
    } else if (txn.type === 'adjustment' && txn.description?.includes('Chargeback')) {
      chargebackFees += Math.abs(txn.fee);
    }
  });

  console.log(`Payout Analysis:`);
  console.log(`  Gross: $${(grossAmount / 100).toFixed(2)}`);
  console.log(`  Card Fees: $${(cardFees / 100).toFixed(2)}`);
  console.log(`  Refund Fees: $${(refundFees / 100).toFixed(2)}`);
  console.log(`  Chargeback Fees: $${(chargebackFees / 100).toFixed(2)}`);
  console.log(`  Net (to bank): $${(netAmount / 100).toFixed(2)}`);

  return { grossAmount, cardFees, refundFees, chargebackFees, netAmount };
}

The importance of understanding this: your accounting team is likely reconciling against expected revenue, and if they're not aware that fees come out of payouts, they'll think money is missing. Always communicate to finance that the payout amount is net of fees.

Payout schedules are configurable

Stripe's default payout schedule is daily, but you have choices. You can configure payouts to happen:

  • Daily (default): Payout initiated every day, usually in the afternoon in your local timezone.
  • Weekly: Payout every Monday (or a day of your choice).
  • Monthly: Payout once per month on a day you specify.
  • Manual: Payouts only when you explicitly request them.

Your choice depends on your business model. SaaS platforms with hundreds of transactions per day often stick with daily to minimize cash-in-transit. Marketplaces with sellers might use weekly or monthly to reduce transaction costs and simplify reconciliation. Platforms that need careful control might use manual payouts.

You can change the payout schedule programmatically:

typescript
async function updatePayoutSchedule(schedule: 'daily' | 'weekly' | 'monthly' | 'manual', dayOfWeek?: number, dayOfMonth?: number) {
  const updatePayload: any = {
    payout_schedule: {
      interval: schedule,
    },
  };

  if (schedule === 'weekly' && dayOfWeek !== undefined) {
    // dayOfWeek: 1 = Monday, 7 = Sunday
    updatePayload.payout_schedule.weekly_anchor = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'][dayOfWeek - 1];
  }

  if (schedule === 'monthly' && dayOfMonth !== undefined) {
    updatePayload.payout_schedule.monthly_anchor = dayOfMonth;
  }

  try {
    const account = await stripe.account.update(updatePayload);
    console.log(`Payout schedule updated to ${schedule}`);
    console.log(`Current schedule:`, account.payout_schedule);
  } catch (error) {
    console.error('Failed to update payout schedule:', error);
  }
}

// Example: Switch to weekly payouts every Monday
await updatePayoutSchedule('weekly', 1);

Important note: if you're using Stripe Connect (platform mode), you update payout schedules for connected accounts differently—usually through the API by updating the connected account object, not your own account.

Reconcile payouts with bank statements

This is where theory meets reality. Your Stripe dashboard shows a payout of $5,000, but your bank received $4,950. What happened?

Reconciliation means matching what Stripe says it sent with what actually landed in your bank. Discrepancies happen because:

  • Processing delays: Payout initiated Monday might settle Tuesday or Wednesday.
  • Currency conversion: If you receive in a different currency than your Stripe account, rates apply.
  • Reversals: A payout can be reversed if the bank rejects it (e.g., account closed, insufficient routing info).
  • Multiple deposits: Stripe might batch multiple payouts into one bank deposit.

Here's a practical reconciliation workflow:

typescript
async function reconcilePayouts(startDate: Date, endDate: Date) {
  const payouts = await stripe.payouts.list({
    created: {
      gte: Math.floor(startDate.getTime() / 1000),
      lte: Math.floor(endDate.getTime() / 1000),
    },
    limit: 100,
  });

  const reconciliation: any[] = [];

  for (const payout of payouts.data) {
    reconciliation.push({
      payoutId: payout.id,
      amount: payout.amount,
      currency: payout.currency,
      status: payout.status,
      arrivalDate: new Date(payout.arrival_date * 1000),
      bankAccountLastFour: payout.destination_payment_method_details?.us_bank_account?.account_holder_name,
      automatic: payout.automatic,
    });

    // If payout is in review or failed, flag it
    if (payout.status !== 'paid') {
      console.warn(`⚠️ Payout ${payout.id} has status: ${payout.status}`);
    }
  }

  console.log(`Found ${reconciliation.length} payouts between ${startDate} and ${endDate}`);
  console.log(`Total amount: $${reconciliation.reduce((sum, p) => sum + p.amount, 0) / 100}`);

  return reconciliation;
}

// Check last week's payouts
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
const thisWeek = new Date();

await reconcilePayouts(lastWeek, thisWeek);

Once you have the payout list, cross-reference it with your bank's API or manual bank statements. Many teams export Stripe payouts to a CSV and match them against bank deposits using a reconciliation sheet. Stripe provides a payout ID in the bank deposit memo (if your bank captures it), making this easier.

For platforms handling significant volume, automate this by checking the arrival_date field and alerting your finance team if a payout hasn't settled within the expected window.

Common mistakes

  • Forgetting to subtract fees from expected payouts: If you've charged customers $10,000 in gross volume, your payout won't be $10,000. Budget for 2.9% + $0.30 per transaction in your financial forecasts.
  • Not monitoring payout reversals: A payout can be reversed if the bank rejects it. Set up webhooks for payout.paid and payout.failed events, and alert your team when a reversal occurs.
  • Reconciling against gross volume instead of net payouts: Finance teams sometimes reconcile AR against gross transaction volume, not payout amounts. This creates phantom discrepancies.
  • Ignoring currency and timezone nuances: If your bank is in one timezone and Stripe dashboard in another, payout dates can appear misaligned. Always use Unix timestamps.
  • Not handling manual hold periods: New accounts or high-risk businesses sometimes have payouts on manual hold. Check payout status programmatically rather than assuming all pending payouts will settle.

Key takeaways

  • Balance transactions are atomic ledger entries; payouts are collections of them bundled by schedule and sent to your bank.
  • Fees—typically 2.9% + $0.30 per card transaction—are deducted before payout, not added on top.
  • Configure your payout schedule (daily, weekly, monthly, or manual) based on your business's cash-flow and reconciliation needs.
  • Reconcile payouts against bank statements by matching payout IDs and arrival dates; flag reversals and mismatches immediately.
  • Use the Stripe API to automate payout monitoring, fee analysis, and reconciliation workflows so your finance team has real-time visibility.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles