Problem
When a Stripe Connect platform refunds a charge, the platform often assumes that the original transfer and the application fee are automatically undone. In practice, two boolean flags—refund_application_fee and reverse_transfer—control whether the fee and the transfer are reversed. Their defaults are non‑obvious (false for both), so a refund can leave the platform with a net loss that never appears in Stripe’s error logs. The loss shows up only as a discrepancy between the amount refunded to the customer and the amount that actually returns to the platform’s balance.
Mechanism
A Connect refund touches several objects:
| Object | Role in a refund | Default behavior |
|---|---|---|
Charge |
Original payment | N/A |
Transfer |
Moves funds from platform to connected account | Not reversed unless reverse_transfer: true
|
ApplicationFee |
Platform’s fee on the charge | Not refunded unless refund_application_fee: true
|
Refund |
Credits the customer | Always creates a credit on the original charge |
The two flags are independent:
refund_application_fee |
reverse_transfer |
Platform net outcome |
|---|---|---|
| false | false | Platform keeps the fee and the transfer stays settled → net loss = fee + any FX spread |
| false | true | Transfer is reversed but fee stays → net loss = fee |
| true | false | Fee is refunded but transfer stays → net loss = transferred amount (minus fee) |
| true | true | Both fee and transfer are undone → net zero (aside from timing/FX) |
Because the defaults are false, a typical “refund” only credits the customer; the platform’s money remains locked in the connected account and the fee stays on the platform’s ledger. This silent leak is why many platforms see “missing” revenue after a batch of refunds.
Detection
FeeGuard can audit the last 90 days (or any custom window) without any credentials—just a read‑only API token or a CSV export. The audit consists of three steps:
-
Export refunds – Pull all
Refundobjects for the target date range. -
Join to transfers and fees – For each refund, locate the associated
Transfer(transferfield on the originalCharge) andApplicationFee(application_feefield). - Apply the truth table – Compute the expected platform balance change based on the two flags. Compare it to the actual balance delta reported by Stripe.
# Python example (requires only read‑only Stripe secret)
import stripe
from datetime import datetime, timedelta
stripe.api_key = "sk_test_readonly_..."
def fetch_refunds(days=90):
start = int((datetime.utcnow() - timedelta(days=days)).timestamp())
refunds = stripe.Refund.list(created={'gte': start}, limit=100)
return refunds.auto_paging_iter()
def audit():
total_gap = 0
for r in fetch_refunds():
charge = stripe.Charge.retrieve(r.charge)
transfer = stripe.Transfer.retrieve(charge.transfer) if charge.transfer else None
fee = stripe.ApplicationFee.retrieve(charge.application_fee) if charge.application_fee else None
# Flags (defaults are false if not present)
rev_fee = r.refund_application_fee or False
rev_transfer = r.reverse_transfer or False
# Expected platform delta
fee_amount = fee.amount if fee else 0
transfer_amount = transfer.amount if transfer else 0
expected = 0
if not rev_fee:
expected -= fee_amount
if not rev_transfer:
expected -= transfer_amount
# Actual delta is the amount Stripe reports as returned to the platform
actual = r.amount # amount refunded to customer (negative for platform)
gap = expected - actual
total_gap += gap
print(f"Total unrecovered loss: {total_gap/100:.2f} USD")
Running the script (or using FeeGuard’s UI) yields the exact dollar amount that never made it back to the platform.
Fix
There are two complementary approaches:
-
Preventive configuration – When creating a refund via the API, explicitly set the flags to
trueif you want the fee and transfer reversed:
curl https://api.stripe.com/v1/refunds \
-u sk_test_readonly_: \
-d charge=ch_1ABC... \
-d amount=5000 \
-d refund_application_fee=true \
-d reverse_transfer=true
Tip: Wrap this call in a helper function in your backend so every refund uses the same parameters.
- Post‑refund recovery – If a batch of refunds has already been processed without the flags, use FeeGuard’s recovery flow:
- Run a read‑only audit to quantify the loss.
- For each affected refund, issue a separate “fee reversal” and “transfer reversal” using the
ApplicationFeeRefundandTransferReversalendpoints. Both are independent operations and can be performed after the original refund.
# Reverse fee
stripe.ApplicationFeeRefund.create(
application_fee=fee.id,
amount=fee.amount,
)
# Reverse transfer
stripe.TransferReversal.create(
transfer=transfer.id,
amount=transfer.amount,
)
Because FeeGuard’s pricing is “pay‑only‑on‑recovery,” you only incur cost when the above steps actually return money to the platform.
Caveats
| Issue | Detail | Mitigation |
|---|---|---|
| Timing | Transfer reversals can take up to a few business days to settle. | Track reversal status via TransferReversal objects. |
| FX exposure | If the original transfer involved currency conversion, the reversal may settle at a different rate, leaving a small residual. | Include FX variance in the audit’s “gap” calculation. |
| Partial refunds | When only part of a charge is refunded, you must decide whether to reverse the proportional fee/transfer or keep the remainder. | Use the same proportion (refund.amount / charge.amount) when creating ApplicationFeeRefund and TransferReversal. |
| Disconnected accounts | Some connected accounts may have custom payout schedules that block immediate reversals. | Verify the account’s payout_schedule before attempting a reversal; fallback to manual payout if needed. |
| Read‑only audit limits | The audit does not require any write credentials, but it cannot detect unreversed transfers that were never created (e.g., a missing transfer field). |
Ensure your platform always creates a Transfer for every Connect charge; otherwise, the audit will flag the missing link. |
Summary
Stripe Connect refunds can silently leak platform revenue because refund_application_fee and reverse_transfer default to false. By systematically joining refunds to their associated transfers and fees, applying the truth table, and either configuring refunds correctly up front or reversing the missed amounts afterward, a platform can recover the hidden loss. FeeGuard’s read‑only audit makes the detection step trivial, and its pay‑only‑on‑recovery model ensures you only pay when money is actually returned.
For a deeper dive, see the full blog post on the FeeGuard site.
Top comments (0)