Creating Separate Charges and Transfers Public Beta

    With Connect, you can make charges on your platform account on behalf of connected accounts, perform transfers separately, and even retain funds in the process.

    Connect supports three approaches for processing charges on behalf of a connected account. For Express and Custom accounts, the simplest route is to create the charge on your platform’s account and use the destination parameter to indicate the connected account that receives the funds.

    For platforms that desire more flexibility in how funds move, a more manual option is to create the charge on your platform account and separately transfer funds to the connected account. Using this approach, the platform is responsible for the cost of the Stripe fees, refunds, and chargebacks by default.

    Because of the additional programming and operations required, this approach is best left to business models with any of these qualities:

    • A one-to-many correlation between charges and transfers (e.g., a charge made to a delivery service needs to be split between the business—the source of the items being delivered—and the delivery person)
    • A many-to-one correlation between charges and transfers (e.g., a carpool trip on a ride-sharing service)
    • At the time the charge is made, it is not known which connected account should receive the funds (e.g., a cleaning service may process the charge immediately but not know which connected account to pay—who is actually doing the work—until later)
    • A transfer may need to be made before the actual charge is made (e.g., an ad network needs to purchase ad space before users pay for ads)
    • A transfer may be for more than the associated charge (e.g., a platform may discount a charge for the customer but still pay the connected account the full amount)

    As the range of possibilities demonstrate, creating separate charges and transfers provides the most flexibility for a platform.

    Grouping transactions

    Because this manual approach creates no direct link between the charge and the subsequent transfer, you must assign a transfer_group to the related charges and transfers when making the requests. This can be any unique value, such as a sale ID. The only restriction is the transfer_group must represent a single business action.

    # Create a Charge:
    curl https://api.stripe.com/v1/charges \
       -u sk_test_R0fjIhUlTQhmmiJeeG9bvhTz: \
       -d amount=10000 \
       -d currency=usd \
       -d source=tok_visa \
       -d transfer_group="{ORDER10}"
    # Create a Transfer to a connected account (later):
    curl https://api.stripe.com/v1/transfers \
       -u sk_test_R0fjIhUlTQhmmiJeeG9bvhTz: \
       -d amount=7000 \
       -d currency=usd \
       -d destination="{CONNECTED_STRIPE_ACCOUNT_ID}" \
       -d transfer_group="{ORDER10}"
    # Create a second Transfer to another connected account (later):
    curl https://api.stripe.com/v1/transfers \
       -u sk_test_R0fjIhUlTQhmmiJeeG9bvhTz: \
       -d amount=2000 \
       -d currency=usd \
       -d destination="{OTHER_CONNECTED_STRIPE_ACCOUNT_ID}" \
       -d transfer_group="{ORDER10}"
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    # Create a Charge:
    charge = Stripe::Charge.create({
      :amount => 10000,
      :currency => "usd",
      :source => "tok_visa",
      :transfer_group => "{ORDER10}",
    })
    
    # Create a Transfer to a connected account (later):
    transfer = Stripe::Transfer.create({
      :amount => 7000,
      :currency => "usd",
      :destination => "{CONNECTED_STRIPE_ACCOUNT_ID}",
      :transfer_group => "{ORDER10}",
    })
    
    # Create a second Transfer to another connected account (later):
    transfer = Stripe::Transfer.create({
      :amount => 2000,
      :currency => "usd",
      :destination => "{OTHER_CONNECTED_STRIPE_ACCOUNT_ID}",
      :transfer_group => "{ORDER10}",
    })
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.api_key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    # Create a Charge:
    charge = stripe.Charge.create(
      amount=10000,
      currency="usd",
      source="tok_visa",
      transfer_group="{ORDER10}",
    )
    
    # Create a Transfer to a connected account (later):
    transfer = stripe.Transfer.create(
      amount=7000,
      currency="usd",
      destination="{CONNECTED_STRIPE_ACCOUNT_ID}",
      transfer_group="{ORDER10}",
    )
    
    # Create a second Transfer to another connected account (later):
    transfer = stripe.Transfer.create(
      amount=2000,
      currency="usd",
      destination="{OTHER_CONNECTED_STRIPE_ACCOUNT_ID}",
      transfer_group="{ORDER10}",
    )
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    \Stripe\Stripe::setApiKey("sk_test_R0fjIhUlTQhmmiJeeG9bvhTz");
    
    // Create a Charge:
    $charge = \Stripe\Charge::create(array(
      "amount" => 10000,
      "currency" => "usd",
      "source" => "tok_visa",
      "transfer_group" => "{ORDER10}",
    ));
    
    // Create a Transfer to a connected account (later):
    $transfer = \Stripe\Transfer::create(array(
      "amount" => 7000,
      "currency" => "usd",
      "destination" => "{CONNECTED_STRIPE_ACCOUNT_ID}",
      "transfer_group" => "{ORDER10}",
    ));
    
    // Create a second Transfer to another connected account (later):
    $transfer = \Stripe\Transfer::create(array(
      "amount" => 2000,
      "currency" => "usd",
      "destination" => "{OTHER_CONNECTED_STRIPE_ACCOUNT_ID}",
      "transfer_group" => "{ORDER10}",
    ));
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.apiKey = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz";
    
    // Create a Charge:
    Map<String, Object> chargeParams = new HashMap<String, Object>();
    chargeParams.put("amount", 10000);
    chargeParams.put("currency", "usd");
    chargeParams.put("source", "tok_visa");
    chargeParams.put("transfer_group", "{ORDER10}");
    Charge charge = Charge.create(chargeParams);
    
    // Create a Transfer to a connected account (later):
    Map<String, Object> transferParams = new HashMap<String, Object>();
    transferParams.put("amount", 7000);
    transferParams.put("currency", "usd");
    transferParams.put("destination", "{CONNECTED_STRIPE_ACCOUNT_ID}");
    transferParams.put("transfer_group", "{ORDER10}");
    Transfer transfer = Transfer.create(transferParams);
    
    // Create a second Transfer to another connected account (later):
    Map<String, Object> secondTransferParams = new HashMap<String, Object>();
    secondTransferParams.put("amount", 2000);
    secondTransferParams.put("currency", "usd");
    secondTransferParams.put("destination", "{OTHER_CONNECTED_STRIPE_ACCOUNT_ID}");
    secondTransferParams.put("transfer_group", "{ORDER10}");
    Transfer secondTransfer = Transfer.create(secondTransferParams);
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    var stripe = require("stripe")("sk_test_R0fjIhUlTQhmmiJeeG9bvhTz");
    
    // Create a Charge:
    stripe.charges.create({
      amount: 10000,
      currency: "usd",
      source: "tok_visa",
      transfer_group: "{ORDER10}",
    }).then(function(charge) {
      // asynchronously called
    });
    
    // Create a Transfer to the connected account (later):
    stripe.transfers.create({
      amount: 7000,
      currency: "usd",
      destination: "{CONNECTED_STRIPE_ACCOUNT_ID}",
      transfer_group: "{ORDER10}",
    }).then(function(transfer) {
      // asynchronously called
    });
    
    // Create a second Transfer to another connected account (later):
    stripe.transfers.create({
      amount: 2000,
      currency: "usd",
      destination: "{OTHER_CONNECTED_STRIPE_ACCOUNT_ID}",
      transfer_group: "{ORDER10}",
    }).then(function(second_transfer) {
      // asynchronously called
    });
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.Key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    // Create a Charge:
    chargeParams := &stripe.ChargeParams{
      Amount: stripe.Int64(10000),
      Currency: stripe.String(string(stripe.CurrencyUSD)),
      TransferGroup: stripe.String("{ORDER10}"),
    }
    params.SetSource("tok_visa")
    charge, err := charge.New(chargeParams)
    
    // Create a Transfer to the connected account (later):
    transferParams := &stripe.TransferParams{
      Amount: stripe.Int64(7000),
      Currency: stripe.String(string(stripe.CurrencyUSD)),
      Destination: stripe.String("{CONNECTED_STRIPE_ACCOUNT_ID}"),
      TransferGroup: stripe.String("{ORDER10}"),
    }
    tr, err := transfer.New(transferParams)
    
    // Create a second Transfer to another connected account (later):
    secondTransferParams := &stripe.TransferParams{
      Amount: stripe.Int64(2000),
      Currency: stripe.String(string(stripe.CurrencyUSD)),
      Destination: stripe.String("{OTHER_CONNECTED_STRIPE_ACCOUNT_ID}"),
      TransferGroup: stripe.String("{ORDER10}"),
    }
    secondTransfer, err := transfer.New(secondTransferParams)
    

    The example uses a test tokentok_visa—but you could tokenize a test card using Stripe.js and Elements or Stripe Checkout instead.

    When using this approach, your platform:

    • Does not have to transfer the same amount as the original charge
    • Can only transfer up to the platform’s available account balance (with an exception when using source_transaction)
    • Must be configured to use manual payouts in order to ensure that the account has a balance of funds available for transfer

    But you can group together multiple charges with a single transfer, multiple transfers with a single charge, or even perform transfers and charges in any order.

    Collecting fees

    When creating charges on your platform and separately creating a transfer, the platform can earn money by allocating less of the charge amount to the destination Stripe account, as in the above code example. Assuming that represents a delivery service transaction, with a charge to the customer of $100, a transfer of $20 to the delivery person, and a transfer of $70 to the restaurant:

    1. The charge amount less the Stripe fees is added to the platform account’s pending balance
    2. When the platform’s available balance is sufficient (at least $90), the transfers can be made, reducing the platform’s available balance by the specified amounts and increasing both connected account’s available balances by that same amount
    3. The platform retains an additional $6.80 ($100.00 - $70.00 - $20.00 - $3.20, assuming standard U.S. Stripe fees).

    If you process payments in multiple currencies, you should also read how that is handled in Connect.

    Refunding charges

    Charges created on your platform can be refunded using your platform's secret key. However, refunding a charge has no impact on any associated transfers. It's up to your platform to reconcile any amount owed back to your platform by reducing subsequent transfer amounts or by reversing transfers (as explained next).

    curl https://api.stripe.com/v1/refunds \
       -u sk_test_R0fjIhUlTQhmmiJeeG9bvhTz: \
       -d charge="{CHARGE_ID}"
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    refund = Stripe::Refund.create({
      :charge => "{CHARGE_ID}",
    })
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.api_key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    refund = stripe.Refund.create(
      charge="{CHARGE_ID}",
    )
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    \Stripe\Stripe::setApiKey("sk_test_R0fjIhUlTQhmmiJeeG9bvhTz");
    
    $refund = \Stripe\Refund::create(array(
      "charge" => "{CHARGE_ID}",
    ));
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.apiKey = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz";
    
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("charge", "{CHARGE_ID}");
    Refund refund = Refund.create(params);
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    var stripe = require("stripe")("sk_test_R0fjIhUlTQhmmiJeeG9bvhTz");
    
    stripe.refunds.create({
      charge: "{CHARGE_ID}",
    }).then(function(refund) {
      // asynchronously called
    });
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.Key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    params := &stripe.RefundParams{
      Charge: stripe.String("{CHARGE_ID}"),
    }
    refund, err := refund.New(params)
    

    Reversing transfers

    Connect supports the ability to reverse transfers made to connected accounts, either entirely or partially (by setting an amount value):

    curl https://api.stripe.com/v1/transfers/{TRANSFER_ID}/reversals \
       -u {PLATFORM_SECRET_KEY}: \
       -d amount=500
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    transfer = Stripe::Transfer.retrieve("{TRANSFER_ID}")
    transfer.reversals.create({
      :amount => 500,
    })
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.api_key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    transfer = stripe.Transfer.retrieve("{TRANSFER_ID}")
    transfer.reversals.create()
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    \Stripe\Stripe::setApiKey("sk_test_R0fjIhUlTQhmmiJeeG9bvhTz");
    
    $transfer = \Stripe\Transfer::retrieve("{TRANSFER_ID}");
    $transfer->reversals->create(
      amount=500,
    );
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.apiKey = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz";
    
    Transfer transfer = Transfer.retrieve("{TRANSFER_ID}");
    
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("amount", 500);
    
    transfer.getReversals().create(params);
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    var stripe = require("stripe")("sk_test_R0fjIhUlTQhmmiJeeG9bvhTz");
    
    stripe.transfers.createReversal(
      "{TRANSFER_ID}",
      {
        amount: 500,
      },
    ).then(function(reversal) {
      // asynchronously called
    });
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.Key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    params := &stripe.ReversalParams{
      Amount: stripe.Int64(500),
      Transfer: stripe.String("{TRANSFER_ID}"),
    }
    r, err := reversal.New(params)
    

    Transfer reversals add the specified (or entire) amount back to the platform’s available balance, reducing the connected account’s available balance accordingly. It is only possible to reverse a transfer if the connected account’s available balance is greater than the reversal amount or has connected reserves enabled.

    Using on_behalf_of

    When you create destination charges on the platform account, Stripe automatically:

    • Settles charges in the country of the specified account, thereby minimizing declines and avoiding currency conversions
    • Uses the fee structure for the connected account’s country
    • If the account is in a different country than the platform, the connected account’s address and phone number shows up on the customer’s credit card statement (as opposed to the platform’s)

    This functionality isn’t provided automatically if you’re creating separate charges and transfers. You can create charges using the on_behalf_of attribute to achieve similar functionality though. Setting on_behalf_of defines the business of record for the charge and funds are settled using that account. The difference from destination charges is that funds are available based on the platform’s payout schedule, not the connected account’s. This is especially important for international platforms who have connected accounts in different countries.

    curl https://api.stripe.com/v1/charges \
       -u sk_test_R0fjIhUlTQhmmiJeeG9bvhTz: \
       -d amount=1000 \
       -d currency=usd \
       -d source=tok_visa \
       -d on_behalf_of="{CONNECTED_STRIPE_ACCOUNT_ID}"
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    charge = Stripe::Charge.create({
      :amount => 1000,
      :currency => "usd",
      :source => "tok_visa",
      :on_behalf_of => "{CONNECTED_STRIPE_ACCOUNT_ID}"
      })
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.api_key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    charge = stripe.Charge.create(
      amount=1000,
      currency="usd",
      source="tok_visa",
      on_behalf_of="{CONNECTED_STRIPE_ACCOUNT_ID}"
    )
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    \Stripe\Stripe::setApiKey("sk_test_R0fjIhUlTQhmmiJeeG9bvhTz");
    
    $charge = \Stripe\Charge::create(array(
      "amount" => 1000,
      "currency" => "usd",
      "source" => "tok_visa",
      "on_behalf_of" => "{CONNECTED_STRIPE_ACCOUNT_ID}"
    ));
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.apiKey = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz";
    
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("amount", 1000);
    params.put("currency", "usd");
    params.put("source", "tok_visa");
    params.put("on_behalf_of", "{CONNECTED_STRIPE_ACCOUNT_ID}");
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    var stripe = require("stripe")("sk_test_R0fjIhUlTQhmmiJeeG9bvhTz");
    
    stripe.charges.create({
      amount: 1000,
      currency: "usd",
      source: "tok_visa",
      on_behalf_of: "{CONNECTED_STRIPE_ACCOUNT_ID}"
    }).then(function(charge) {
      // asynchronously called
    });
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.Key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    params := &stripe.ChargeParams{
      Amount: stripe.Int64(1000),
      Currency: stripe.String(string(stripe.CurrencyUSD)),
      OnBehalfOf: stripe.String("{CONNECTED_STRIPE_ACCOUNT_ID}"),
    }
    params.SetSource("tok_visa")
    
    charge, err := charge.New(params)
    

    Transfer availability

    When creating separate charges and transfers, your platform can inadvertently attempt a transfer without having a sufficient available balance. Doing so raises an error and the transfer attempt fails. If you’re commonly experiencing this problem, you can use the source_transaction parameter to tie a transfer to an existing charge. By using source_transaction, the transfer request succeeds regardless of your available balance and the transfer itself only occurs once the charge’s funds become available.

    curl https://api.stripe.com/v1/transfers \
       -u sk_test_R0fjIhUlTQhmmiJeeG9bvhTz: \
       -d amount=1000 \
       -d currency=usd \
       -d source_transaction="{CHARGE_ID}" \
       -d destination="{CONNECTED_STRIPE_ACCOUNT_ID}"
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    transfer = Stripe::Transfer.create({
      :amount => 900,
      :currency => "usd",
      :source_transaction => "{CHARGE_ID}",
      :destination => "{CONNECTED_STRIPE_ACCOUNT_ID}",
    })
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.api_key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    transfer = stripe.Transfer.create(
      amount=1000,
      currency="usd",
      source_transaction="{CHARGE_ID}",
      destination={CONNECTED_STRIPE_ACCOUNT_ID}",
    )
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    \Stripe\Stripe::setApiKey("sk_test_R0fjIhUlTQhmmiJeeG9bvhTz");
    
    $transfer = \Stripe\Transfer::create(array(
      "amount" => 1000,
      "currency" => "usd",
      "source_transaction" => "{CHARGE_ID}",
      "destination" => "{CONNECTED_STRIPE_ACCOUNT_ID}",
    ));
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.apiKey = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz";
    
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("amount", 1000);
    params.put("currency", "usd");
    params.put("source_transaction", "{CHARGE_ID}");
    params.put("destination", "{CONNECTED_STRIPE_ACCOUNT_ID}");
    Transfer transfer = Transfer.create(params);
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    var stripe = require("stripe")("sk_test_R0fjIhUlTQhmmiJeeG9bvhTz");
    
    stripe.transfers.create({
      amount: 1000,
      currency: "usd",
      source_transaction: "{CHARGE_ID}",
      destination: "{CONNECTED_STRIPE_ACCOUNT_ID}",
    }).then(function(transfer) {
      // asynchronously called
    });
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.Key = "sk_test_R0fjIhUlTQhmmiJeeG9bvhTz"
    
    params := &stripe.TransferParams{
      Amount: stripe.Int64(1000),
      Currency: stripe.String(string(stripe.CurrencyUSD)),
      SourceTransaction: stripe.String("{CHARGE_ID}"),
      Destination: stripe.String("{CONNECTED_STRIPE_ACCOUNT_ID}"),
    }
    tr, err := transfer.New(params)
    

    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
    • The transfer takes on the pending status of the associated charge: if the funds from the charge become available in N days, the payment that the destination Stripe account receives from the transfer also becomes available in N days
    • Stripe automatically creates a transfer_group for you

    Further reading

    Discover what other Connect functionality is available.