Special-case Transfers

If you're a platform, you may occasionally find it useful to make direct transfers to and from your connected accounts. If you need help after reading this, check out our answers to common questions or chat live with other developers in #stripe on freenode.

Stripe provides the ability to move funds from your own Stripe balance into the Stripe balance of another account. However, this should not be the main way you pay out to your users, and is only permitted for specific scenarios.

When are these transfers permitted?

For compliance reasons, the vast majority of money moving around must be directly linked to an incoming charge (either by having been created on the connected account or via use of the destination parameter). When you use one of these methods, the funds are never available to you: they will end up either being refunded, or being paid out to the account holder’s bank account. This is the foundation of a compliant Stripe integration.

However, there are some situations where unlinked transfers are permitted. These generally can be described as “part of normal charge-based business operations”. A few examples of explicitly permitted uses of the transfers API:

  • A ride sharing service paying a bonus to drivers who work a certain number of hours in a week.
  • A home cleaning service offering a coupon or otherwise taking a loss on a cleaning, and needing to fully compensate the cleaner.

Another helpful rule of thumb is that, over time, the transfers API should be less than 10% of your overall net volume from all transactions performed. Being under this volume does not guarantee that you are compliant, but use cases significantly over that volume are unlikely to be permitted.

Need help figuring out if your use case makes sense? We’d be very happy to help.

Transfer scenarios

With the API, platforms can:

  • Transfer funds directly to another Stripe account.
  • Tie a transfer to an existing charge.
  • Take an application fee from a transfer.

The three new parameters are explained individually below but can, and normally will, be used in some combination.

Note that, to use this API, you’ll need to switch the platform account to manual transfers. This switch disables automatic transfers to your bank account, thereby leaving funds in the Stripe account that can be sent by request to either your bank account or to third-party accounts.

Transferring to another Stripe account

Through Connect, you can transfer funds from the platform’s Stripe account to a connected Stripe account (as opposed to a bank account or debit card). This is done by making a POST request to /v1/transfers, passing the connected account ID in the new destination parameter.

curl https://api.stripe.com/v1/transfers \
   -u {PLATFORM_SECRET_KEY}: \
   -d amount=1000 \
   -d currency=usd \
   -d destination="{CONNECTED_STRIPE_ACCOUNT_ID}"
Stripe.api_key = PLATFORM_SECRET_KEY
Stripe::Transfer.create(
  :amount => 1000,
  :currency => 'usd',
  :destination => {CONNECTED_STRIPE_ACCOUNT_ID}
)
stripe.api_key = PLATFORM_SECRET_KEY
stripe.Transfer.create(
  amount=1000,
  currency="usd",
  destination={CONNECTED_STRIPE_ACCOUNT_ID}
)
\Stripe\Stripe::setApiKey(PLATFORM_SECRET_KEY);
\Stripe\Transfer::create(array(
  'amount' => 1000,
  'currency' => "usd",
  'destination' => {CONNECTED_STRIPE_ACCOUNT_ID}
));
Stripe.apiKey = PLATFORM_SECRET_KEY;

Map<String, Object> transferParams = new HashMap<String, Object>();
transferParams.put("amount", 1000);
transferParams.put("currency", "usd");
transferParams.put("destination", {CONNECTED_STRIPE_ACCOUNT_ID});

Transfer.create(transferParams);
var stripe = require('stripe')(PLATFORM_SECRET_KEY);
stripe.transfers.create(
  {
    amount: 1000,
    currency: 'usd',
    destination: {CONNECTED_STRIPE_ACCOUNT_ID}
  }
);

The destination account will see the transfer as a charge, subject to the same payout schedule and currency handling as a regular charge. Note that it is only possible to transfer funds to a connected account. You cannot transfer funds to an account not connected to the platform (which means funds also cannot be transferred from a connected account to the platform account).

Associating a transfer with a charge

Through the source_transaction parameter, you can tie a transfer to an existing charge:

curl https://api.stripe.com/v1/transfers \
   -u {PLATFORM_SECRET_KEY}: \
   -d amount=1000 \
   -d currency=usd \
   -d destination="{CONNECTED_STRIPE_ACCOUNT_ID}" \
   -d source_transaction="{CHARGE_ID}"
Stripe.api_key = PLATFORM_SECRET_KEY
Stripe::Transfer.create(
  :amount => 1000,
  :currency => 'usd',
  :destination => {CONNECTED_STRIPE_ACCOUNT_ID},
  :source_transaction => {CHARGE_ID}
)
stripe.api_key = PLATFORM_SECRET_KEY
stripe.Transfer.create(
  amount=1000,
  currency="usd",
  destination={CONNECTED_STRIPE_ACCOUNT_ID}
  source_transaction={CHARGE_ID}
)
\Stripe\Stripe::setApiKey(PLATFORM_SECRET_KEY);
\Stripe\Transfer::create(array(
  'amount' => 1000,
  'currency' => "usd",
  'destination' => {CONNECTED_STRIPE_ACCOUNT_ID},
  'source_transaction' => {CHARGE_ID}
));
Stripe.apiKey = PLATFORM_SECRET_KEY;

Map<String, Object> transferParams = new HashMap<String, Object>();
transferParams.put("amount", 1000);
transferParams.put("currency", "usd");
transferParams.put("destination", {CONNECTED_STRIPE_ACCOUNT_ID});
transferParams.put("source_transaction", {CHARGE_ID});

Transfer.create(transferParams);
var stripe = require('stripe')(PLATFORM_SECRET_KEY);
stripe.transfers.create(
  {
    amount: 1000,
    currency: 'usd',
    destination: {CONNECTED_STRIPE_ACCOUNT_ID},
    source_transaction: {CHARGE_ID}
  }
);

By making this association, the transfer will take on the pending status of the charge: if the funds from the charge become available in N days, the payment that the destination account receives from the transfer will also become available in N days. (For most Stripe accounts in the US, charges become available in two days.)

When using this parameter, the amount of the transfer must not exceed the amount of the source charge. You may create multiple transfers with the same source_transaction, as long as the sum of the transfers doesn’t exceed the source charge amount.

By using source_transaction with a transfer, you aren’t required to have funds in your available balance to make the transfer request (unless the source transaction has already become available). However, when you do this, the source transaction will remain attributed to the platform for tax and compliance purposes. Most use cases require the charge to be attributed to the connected account, so you’ll almost always want to use the destination field on the charge (which results in a transfer) rather than manually creating a separate transfer in this manner.

An example case where you would use source_transaction instead of the destination field on the charge is a platform that sells virtual gift cards that can be used with any of its sellers. In that case, the charge is intended for the platform as a whole, and leaving off destination on the charge but separately transferring it to a connected account (representing the seller) would be appropriate. However, as mentioned above, such usage would still need to be a small percentage of charges on the platform.

Taking application fees from a transfer

Connect lets you take an application fee as part of a transfer (not just as part of a charge):

curl https://api.stripe.com/v1/transfers \
   -u {PLATFORM_SECRET_KEY}: \
   -d amount=1000 \
   -d currency=usd \
   -d destination="{CONNECTED_STRIPE_ACCOUNT_ID}" \
   -d application_fee=200
Stripe.api_key = PLATFORM_SECRET_KEY
Stripe::Transfer.create(
  :amount => 1000,
  :currency => 'usd',
  :destination => {CONNECTED_STRIPE_ACCOUNT_ID},
  :application_fee => 200
)
stripe.api_key = PLATFORM_SECRET_KEY
stripe.Transfer.create(
  amount=1000,
  currency="usd",
  destination={CONNECTED_STRIPE_ACCOUNT_ID}
  application_fee=200
)
\Stripe\Stripe::setApiKey(PLATFORM_SECRET_KEY);
\Stripe\Transfer::create(array(
  'amount' => 1000,
  'currency' => "usd",
  'destination' => {CONNECTED_STRIPE_ACCOUNT_ID},
  'application_fee' => 200
));
Stripe.apiKey = PLATFORM_SECRET_KEY;

Map<String, Object> transferParams = new HashMap<String, Object>();
transferParams.put("amount", 1000);
transferParams.put("currency", "usd");
transferParams.put("destination", {CONNECTED_STRIPE_ACCOUNT_ID});
transferParams.put("application_fee", 200);

Transfer.create(transferParams);
var stripe = require('stripe')(PLATFORM_SECRET_KEY);
stripe.transfers.create(
  {
    amount: 1000,
    currency: 'usd',
    destination: {CONNECTED_STRIPE_ACCOUNT_ID},
    application_fee: 200
  }
);

When this parameter is set, the payment on the destination account will be created with an application fee for this amount. This only affects accounting; a transfer with an amount of 1000 and an application_fee of 200 will result in the same net balance (for both the platform and the connected account) as a transfer with an amount of 800 and no application fee, assuming no currency conversion needs to happen.

Reversing a transfer

You may reverse a transfer made to a connected account, either entirely or partially. You may also specify whether or not to refund any related application fees.

curl https://api.stripe.com/v1/transfers/{TRANSFER_ID}/reversals \
   -u {PLATFORM_SECRET_KEY}: \
   -d amount=1000 \
   -d refund_application_fee=true
Stripe.api_key = PLATFORM_SECRET_KEY
tr = Stripe::Transfer.retrieve({TRANSFER_ID})
tr.reversals.create(
  :amount => 1000,
  :refund_application_fee => true
)
stripe.api_key = PLATFORM_SECRET_KEY
tr = stripe.Transfer.retrieve({TRANSFER_ID})
tr.reversals.create(
  amount=1000,
  refund_application_fee=True
)
\Stripe\Stripe::setApiKey(PLATFORM_SECRET_KEY);
$tr = \Stripe\Transfer::retrieve({TRANSFER_ID});
$tr->reversals->create(array(
  "amount" => 1000,
  "refund_application_fee" => true
));
Stripe.apiKey = PLATFORM_SECRET_KEY;

Transfer tr = Transfer.retrieve({TRANSFER_ID});

Map<String, Object> reversalParams = new HashMap<String, Object>();
reversalParams.put("amount", 1000);
reversalParams.put("refund_application_fee", true);
tr.getReversals().create(reversalParams);
var stripe = require('stripe')(PLATFORM_SECRET_KEY);
stripe.transfers.createReversal(
  {TRANSFER_ID},
  {
    amount: 1000,
    refund_application_fee: true
  }
);

Transfer reversals will add to your balance and subtract from the destination’s balance. It is only possible to reverse a special-case transfer if the destination has enough balance to cover the reversal.

Reversing a transfer that was made for a charge with a destination, is only allowed up to the amount that the charge has been refunded. The charge and the transfer can be refunded at the same time using the reverse_transfer parameter on the charge refund API.

Further reading

Keep reading to discover what other functionality is available for your users.