Balance checks
When you process refunds for destination charges or separate charges and transfers, Stripe debits your platform balance first, then attempts to recover funds from the connected account. If the connected account lacks sufficient funds:
- The connected account goes into negative balance
- You may not recover the refunded amount
- Stripe may hold reserves on the connected account’s future payouts
import stripe
stripe.api_key = "sk_test_51Sy8NyJHoIadtaBidwXWWi0ikBTmlTEnZYMq3usCGvkl918mcw4GvFM7GrdGtmgDBudXGuPmmqEm5GOZ5TcYvAeJ00Di5CuUbD"
def process_refund_with_balance_check(payment_intent_id, refund_amount, connected_account_id):
"""Process refund only if connected account has sufficient balance"""
try:
# Check connected account balance
balance = stripe.Balance.retrieve(
stripe_account=connected_account_id
)
# Get available balance in the charge currency
available_balance = 0
for balance_item in balance['available']:
if balance_item['currency'] == 'usd': # Match your currency
available_balance = balance_item['amount']
break
# Check if sufficient funds available
if available_balance >= refund_amount:
# Proceed with refund
refund = stripe.Refund.create(
payment_intent=payment_intent_id,
amount=refund_amount,
reverse_transfer=True,
refund_application_fee=True
)
return {'success': True, 'refund': refund}
else:
# Insufficient funds - handle accordingly
return {
'success': False,
'error': 'Insufficient balance',
'available': available_balance,
'required': refund_amount
}
except stripe.error.StripeError as e:
return {'success': False, 'error': str(e)}
# Usage
result = process_refund_with_balance_check(
payment_intent_id="pi_123456789",
refund_amount=5000, # $50.00
connected_account_id="acct_connected_account"
)
if result['success']:
print("Refund processed successfully")
else:
print(f"Refund failed: {result['error']}")
def process_refund_with_auto_topup(payment_intent_id, refund_amount, connected_account_id):
"""Process refund with automatic account top-up if insufficient funds"""
try:
# Check balance first
balance = stripe.Balance.retrieve(stripe_account=connected_account_id)
available_balance = 0
currency = 'usd'
for balance_item in balance['available']:
if balance_item['currency'] == currency:
available_balance = balance_item['amount']
break
# Calculate shortfall
shortfall = refund_amount - available_balance
if shortfall > 0:
# Attempt to charge stored payment method to cover shortfall
success = charge_connected_account_for_shortfall(
connected_account_id,
shortfall,
currency
)
if not success:
return {
'success': False,
'error': 'Insufficient balance and unable to charge payment method',
'shortfall': shortfall
}
# Proceed with refund
refund = stripe.Refund.create(
payment_intent=payment_intent_id,
amount=refund_amount,
reverse_transfer=True,
refund_application_fee=True
)
return {'success': True, 'refund': refund}
except Exception as e:
return {'success': False, 'error': str(e)}
def charge_connected_account_for_shortfall(account_id, amount, currency):
"""Charge connected account's stored payment method to cover refund"""
try:
# Get the connected account's default payment method
account = stripe.Account.retrieve(account_id)
# Check if account has external accounts (bank accounts/cards)
external_accounts = stripe.Account.list_external_accounts(account_id)
if not external_accounts['data']:
return False
# For accounts with debit_negative_balances enabled,
# Stripe will automatically attempt to debit external account
# You can also manually create a charge if needed
# Create a charge to the connected account
charge = stripe.Charge.create(
amount=amount,
currency=currency,
source=external_accounts['data'][0]['id'], # Use first available source
description=f"Top-up for refund processing",
stripe_account=account_id
)
return charge['paid']
except stripe.error.StripeError:
return False
def refund_with_balance_validation(self, payment_intent_id, refund_amount=None):
"""Enhanced refund method with balance checks"""
# Get the original payment intent to find connected account
intent = self.client.v1.payment_intents.retrieve(
payment_intent_id,
options=self._default_stripe_options
)
connected_account = intent.get('on_behalf_of')
if not connected_account:
# Direct charge - no balance check needed
return self._process_direct_refund(payment_intent_id, refund_amount)
# For destination charges - check connected account balance
charge_currency = intent['currency']
refund_amt = refund_amount or intent['amount']
balance_check = self._check_connected_account_balance(
connected_account,
refund_amt,
charge_currency
)
if not balance_check['sufficient']:
# Handle insufficient balance
return self._handle_insufficient_balance(
connected_account,
balance_check,
payment_intent_id,
refund_amt
)
# Proceed with refund
return self.client.v1.refunds.create(
params={
"payment_intent": payment_intent_id,
"amount": refund_amt,
"reverse_transfer": True,
"refund_application_fee": True,
},
options=self._default_stripe_options
)
def _check_connected_account_balance(self, account_id, required_amount, currency):
"""Check if connected account has sufficient balance"""
balance = self.client.v1.balance.retrieve(
options={"stripe_account": account_id}
)
available_amount = 0
for balance_item in balance['available']:
if balance_item['currency'] == currency:
available_amount = balance_item['amount']
break
return {
'sufficient': available_amount >= required_amount,
'available': available_amount,
'required': required_amount,
'shortfall': max(0, required_amount - available_amount)
}
def _handle_insufficient_balance(self, account_id, balance_check, payment_intent_id, refund_amount):
"""Handle cases where connected account has insufficient balance"""
# Option 1: Delay refund until account receives more funds
# Option 2: Attempt to charge account's external payment method
# Option 3: Return error and require manual intervention
return {
'success': False,
'error': 'Connected account has insufficient balance for refund',
'balance_info': balance_check,
'suggested_actions': [
'Wait for account to receive more funds',
'Contact connected account to add funds',
'Process partial refund if applicable'
]
}
Proactive measures:
- Check balances before processing refunds
- Set up automatic external account debiting for Express accounts
- Maintain minimum balance requirements for connected accounts
- Implement delayed payouts for high-risk accounts
Error handling:
- Gracefully handle insufficient balance scenarios
- Provide clear error messages with next steps
- Log balance check failures for manual review
- Offer alternative refund timelines when funds are insufficient
This approach prevents refund failures and ensures smooth financial operations for your platform.
User: @stripe how does checking the balance prevent “You may not recover the refunded amount”
Stripe: Checking the balance doesn’t actually prevent you from not recovering the refunded amount - you’re right to question this. Let me clarify what balance checks actually accomplish:
Balance checks help you identify risk but don’t eliminate it. Here’s what happens:
# You process refund blindly
refund = stripe.Refund.create(
payment_intent="pi_123456789",
amount=5000,
reverse_transfer=True,
)
# Result:
# - Your platform balance: -$50 (you paid the refund)
# - Connected account balance: -$50 (Stripe created negative balance)
# - You discover the problem AFTER the refund is processed
# Check first
balance = stripe.Balance.retrieve(stripe_account="acct_connected")
available = balance['available'][0]['amount'] # $10 available
if available < 5000: # Need $50, only have $10
# You can choose what to do BEFORE processing refund:
return {
'error': 'Connected account has insufficient funds',
'available': 10, # $0.10
'shortfall': 4990 # $49.90 shortfall
}
1. Risk-based decision making:
def smart_refund_processing(payment_intent_id, refund_amount, account_id):
balance_info = check_balance(account_id, refund_amount)
if balance_info['sufficient']:
# Safe to process - high chance of recovery
return process_refund_immediately()
elif balance_info['shortfall'] < 1000: # Less than $10 short
# Small risk - process and monitor
refund = process_refund_immediately()
alert_finance_team(f"Small shortfall: ${balance_info['shortfall']}")
return refund
else:
# High risk - require manual approval or alternative action
return delay_refund_until_sufficient_balance()
2. Alternative recovery strategies:
def handle_insufficient_balance(account_id, shortfall):
# Option 1: Wait for incoming payments
pending_balance = get_pending_balance(account_id)
if pending_balance >= shortfall:
return schedule_refund_after_settlement()
# Option 2: Charge external account (if enabled)
if can_debit_external_account(account_id):
return charge_external_account_for_shortfall(shortfall)
# Option 3: Negotiate payment plan
return create_recovery_plan(account_id, shortfall)
3. Proactive communication:
if balance_info['shortfall'] > 0:
# Contact connected account before processing refund
notify_connected_account({
'message': f'Refund requires ${shortfall/100} additional balance',
'options': ['Add funds', 'Delay refund', 'Partial refund'],
'deadline': datetime.now() + timedelta(days=7)
})
You’re absolutely correct - balance checks don’t guarantee recovery because:
Connected accounts can still go negative:
- Stripe allows negative balances and will attempt future recovery
- Account holders might close accounts or stop transacting
- External payment methods might fail when Stripe tries to debit
Platform still pays refunds upfront:
- Your platform balance is always debited first
- Recovery happens separately and might fail