Collect payments then pay out
This guide demonstrates how to accept payments and move funds to the bank accounts of your sellers or service providers. For demonstration purposes, we’ll build a home-rental marketplace that connects homeowners to people looking for a place to rent. You can use the concepts covered in this guide in other applications as well.
You can see a complete onboarding flow in action in our sample end-to-end Express integration.
Prerequisites
- Register your platform.
- Activate your account.
- Fill out your platform profile.
- Customize your brand settings on the Connect settings page. This information is required for Connect Onboarding.
Set up StripeServer-side
Install Stripe’s official libraries so you can access the API from your application:
# Available as a gem sudo gem install stripe
# If you use bundler, you can add this line to your Gemfile gem 'stripe'
Create a connected account
When a user (seller or service provider) signs up on your platform, create a user Account (referred to as a connected account) so you can accept payments and move funds to their bank account. Connected accounts represent your users in the Stripe API and help collect the information required to verify the user’s identity. In our home-rental example, the connected account represents the homeowner.
Step 2.1: Create an Express account and prefill information
Use the /v1/accounts
API to create an Express account and set type
to express
in the account creation request.
curl https://api.stripe.com/v1/accounts \ -u
: \ -d "type"="express"sk_test_4eC39HqLyjWDarjtT1zdp7dc
If you already collected information for your connected accounts, you can prefill that information on the account object for the user and it won’t be collected again in the Connect Onboarding flow. Connect Onboarding only collects required information when you create or update an account.
Step 2.2: Create an account link
You can create an account link by calling the Account Links API with the following parameters:
account
refresh_url
return_url
type
=account_onboarding
curl https://api.stripe.com/v1/account_links \ -u
: \ -d "account"="acct_1032D82eZvKYlo2C" \ -d "refresh_url"="https://example.com/reauth" \ -d "return_url"="https://example.com/return" \ -d "type"="account_onboarding"sk_test_4eC39HqLyjWDarjtT1zdp7dc
Step 2.3: Redirect your user to the account link URL
The response to your Account Links request includes a value for the key url
. Redirect to this link to send your user into the flow. URLs from the Account Links API are temporary and can only be used once, because they grant access to the account holder’s personal information. Authenticate the user in your application before redirecting them to this URL. If you want to prefill information, you must do so before generating the account link. After you create the account link for an Express account, you can’t read or write information for the account.
Don’t email, text, or otherwise send account link URLs directly to your user. Instead, redirect the authenticated user to the account link URL from within your platform’s application.
Step 2.4: Handle the user returning to your platform
Connect Onboarding requires you to pass both a return_url
and refresh_url
to handle all cases where the user is redirected to your platform. It’s important that you implement these correctly to provide the best experience for your user.
You can use HTTP for your return_url
and refresh_url
while in test mode (e.g., to test with localhost), but in live mode only HTTPS is accepted. Be sure to swap testing URLs for HTTPS URLs before going live.
return_url
Stripe issues a redirect to this URL when the user completes the Connect Onboarding flow. This doesn’t mean that all information has been collected or that there are no outstanding requirements on the account. This only means the flow was entered and exited properly.
No state is passed through this URL. After a user is redirected to your return_url
, check the state of the details_submitted
parameter on their account by doing either of the following:
- Listening to
account.updated
webhooks - Calling the Accounts API and inspecting the returned object
refresh_url
Your user will be redirected to the refresh_url
in these cases:
- The link is expired (a few minutes went by since the link was created).
- The link was already visited (the user refreshed the page or clicked back or forward in the browser).
- Your platform is no longer able to access the account.
- The account has been rejected.
Your refresh_url
should trigger a method on your server to call Account Links again with the same parameters, and redirect the user to the Connect Onboarding flow to create a seamless experience.
Step 2.5: Handle users that haven’t completed onboarding
A user that’s redirected to your return_url
might not have completed the onboarding process. Use the /v1/accounts
endpoint to retrieve the user’s account and check for charges_enabled
. If the account isn’t fully onboarded, provide UI prompts to allow the user to continue onboarding later. The user can complete their account activation through a new account link (generated by your integration). You can check the state of the details_submitted
parameter on their account to see if they’ve completed the onboarding process.
Accept a payment
Stripe Elements is a set of prebuilt UI components, like inputs and buttons, for building your checkout flow. If you’d rather not build your own payment form, consider Checkout, a Stripe-hosted page to accept payments for one-time purchases and subscriptions.
Step 3.1: Create a PaymentIntent Server-side
Stripe uses a PaymentIntent object to represent your intent to collect payment from a customer, tracking charge attempts and payment state changes throughout the process.
Create a PaymentIntent on your server with an amount and currency. Always decide how much to charge on the server side, a trusted environment, as opposed to the client. This prevents malicious customers from being able to choose their own prices.
curl https://api.stripe.com/v1/payment_intents \ -u
: \ -d "payment_method_types[]"=card \ -d amount=1000 \ -d currency="usd" \ -d application_fee_amount="123" \ -d "transfer_data[destination]"="{{CONNECTED_STRIPE_ACCOUNT_ID}}" \sk_test_4eC39HqLyjWDarjtT1zdp7dc
In our home-rental example, we want to build an experience where customers pay for rentals by using our platform, and where we pay homeowners for renting to customers. To set this experience up:
- Indicate the rental is a destination charge with
transfer_data[destination]
. - Specify how much of the rental amount will go to the platform with
application_fee_amount
.
When a rental charge occurs, Stripe transfers the entire amount to the connected account’s pending balance (transfer_data[destination]
). Afterward, Stripe transfers the fee amount (application_fee_amount
) to the platform’s account, which is the share of the revenue for facilitating the rental. Then, Stripe deducts the Stripe fees from the platform’s fee amount. An illustration of this funds flow is below:
This PaymentIntent creates a destination charge. If you need to control the timing of transfers or need to transfer funds from a single payment to multiple parties, use separate charges and transfers instead.
Included in the returned PaymentIntent is a client secret, which is used on the client side to securely complete the payment process instead of passing the entire PaymentIntent object. There are different approaches that you can use to pass the client secret to the client side.
You can retrieve the client secret from an endpoint on your server using the browser’s fetch
function on the client side. This approach is generally most suitable when your client side is a single-page application, particularly one built with a modern frontend framework such as React. This example shows how to create the server endpoint that serves the client secret:
get '/secret' do intent = # ... Create or retrieve the PaymentIntent {client_secret: intent.client_secret}.to_json end
This example demonstrates how to fetch the client secret with JavaScript on the client side:
var response = fetch('/secret').then(function(response) { return response.json(); }).then(function(responseJson) { var clientSecret = responseJson.client_secret; // Call stripe.confirmCardPayment() with the client secret. });
Step 3.2: Collect card details Client-side
You’re ready to collect card information on the client with Stripe Elements. Elements is a set of prebuilt UI components for collecting and validating card number, ZIP code, and expiration date.
A Stripe Element contains an iframe that securely sends the payment information to Stripe over an HTTPS connection. The checkout page address must also start with https:// rather than http:// for your integration to work.
You can test your integration without using HTTPS. Enable it when you’re ready to accept live payments.
Set up Stripe Elements
Stripe Elements is automatically available as a feature of Stripe.js. Include the Stripe.js script on your checkout page by adding it to the head
of your HTML file. Always load Stripe.js directly from js.stripe.com to remain PCI compliant. Do not include the script in a bundle or host a copy of it yourself.
<head> <title>Checkout</title> <script src="https://js.stripe.com/v3/"></script> </head>
Create an instance of Elements with the following JavaScript on your checkout page:
// Set your publishable key: remember to change this to your live publishable key in production // See your keys here: https://dashboard.stripe.com/account/apikeys var stripe = Stripe(
); var elements = stripe.elements();'pk_test_TYooMQauvdEDq54NiTphI7jx'
Add Elements to your payment page
Elements needs a place to live in your payment form. Create empty DOM nodes (containers) with unique IDs in your payment form and then pass those IDs to Elements.
<form id="payment-form"> <div id="card-element"> <!-- Elements will create input elements here --> </div> <!-- We'll put the error messages in this element --> <div id="card-errors" role="alert"></div> <button id="submit">Pay</button> </form>
When the form above has loaded, create an instance of an Element and mount it to the Element container:
// Set up Stripe.js and Elements to use in checkout form var elements = stripe.elements(); var style = { base: { color: "#32325d", } }; var card = elements.create("card", { style: style }); card.mount("#card-element");
The card
Element simplifies the form and minimizes the number of required fields by inserting a single, flexible input field that securely collects all necessary card and billing details. Otherwise, combine cardNumber
, cardExpiry
, and cardCvc
Elements for a flexible, multi-input card form.
Always collect a postal code to increase card acceptance rates and reduce fraud.
The single input card
Element automatically collects and sends the customer’s postal code to Stripe. If you build your payment form with multi-input card Elements (cardNumber
, cardExpiry
, cardCvc
), add a separate input field for the customer’s postal code.
For a full list of supported Element types, refer to our Stripe.js reference documentation.
Elements validates user input as it is typed. To help your customers catch mistakes, listen to change
events on the card
Element and display any errors:
card.on('change', function(event) { var displayError = document.getElementById('card-errors'); if (event.error) { displayError.textContent = event.error.message; } else { displayError.textContent = ''; } });
Postal code validation depends on your customer’s billing country. Use our international test cards to experiment with other postal code formats.
Step 3.3: Submit the payment to Stripe Client-side
Rather than sending the entire PaymentIntent object to the client, use its client secret from Step 3.1. This is different from your API keys that authenticate Stripe API requests.
The client secret should still be handled carefully because it can complete the charge. Do not log it, embed it in URLs, or expose it to anyone but the customer.
To complete the payment when the user clicks, retrieve the client secret from the PaymentIntent you created in Step 3.1 and call stripe.confirmCardPayment with the client secret.
Pass additional billing details, such as the cardholder name and address, to the billing_details
hash. The card
Element automatically sends the customer’s postal code information. However, combining cardNumber
, cardCvc
, and cardExpiry
Elements requires you to pass the postal code to billing_details[address][postal_code]
.
var form = document.getElementById('payment-form'); form.addEventListener('submit', function(ev) { ev.preventDefault(); stripe.confirmCardPayment(clientSecret, { payment_method: { card: card, billing_details: { name: 'Jenny Rosen' } } }).then(function(result) { if (result.error) { // Show error to your customer (e.g., insufficient funds) console.log(result.error.message); } else { // The payment has been processed! if (result.paymentIntent.status === 'succeeded') { // Show a success message to your customer // There's a risk of the customer closing the window before callback // execution. Set up a webhook or plugin to listen for the // payment_intent.succeeded event that handles any business critical // post-payment actions. } } }); });
stripe.confirmCardPayment
may take several seconds to complete. During that time, disable your form from being resubmitted and show a waiting indicator like a spinner. If you receive an error, show it to the customer, re-enable the form, and hide the waiting indicator.
If the customer must authenticate the card, Stripe.js walks them through that process by showing them a modal. You can see an example of this modal experience by using the test card number with any CVC, future expiration date, and postal code in the demo at the top of the page.
When the payment completes successfully, the value of the returned PaymentIntent’s status
property is succeeded. Check the status of a PaymentIntent in the Dashboard or by inspecting the status property on the object. If the payment is not successful, inspect the returned error
to determine the cause.
Step 3.4: Fulfillment Server-side
After the payment is completed, you’ll need to handle any fulfillment necessary on your end. A home-rental company that requires payment upfront, for instance, would connect the homeowner with the renter after a successful payment.
Configure a webhook endpoint (for events from your account) in your dashboard.
Then create an HTTP endpoint on your server to monitor for completed payments to then enable your sellers or service providers to fulfill purchases.
# Using Sinatra. require 'sinatra' require 'stripe' set :port, 4242 # Set your secret key. Remember to switch to your live secret key in production. # See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.api_key =
# Uncomment and replace with a real secret. You can find your endpoint's # secret in your webhook settings. # webhook_secret = 'whsec_...' post '/webhook' do payload = request.body.read sig_header = request.env['HTTP_STRIPE_SIGNATURE'] event = nil # Verify webhook signature and extract the event. # See https://stripe.com/docs/webhooks/signatures for more information. begin event = Stripe::Webhook.construct_event( payload, sig_header, webhook_secret ) rescue JSON::ParserError => e # Invalid payload. status 400 return rescue Stripe::SignatureVerificationError => e # Invalid Signature. status 400 return end if event['type'] == 'payment_intent.succeeded' payment_intent = event['data']['object'] handle_successful_payment_intent(payment_intent) end status 200 end def handle_successful_payment_intent(payment_intent) # Fulfill the purchase. puts payment_intent.to_s end"sk_test_4eC39HqLyjWDarjtT1zdp7dc"
Learn more in our fulfillment guide for payments.
Testing webhooks locally
Testing webhooks locally is easy with the Stripe CLI.
First, install the Stripe CLI on your machine if you haven’t already.
Then, to log in run
stripe login
in the command line, and follow the instructions.Finally, to allow your local host to receive a simulated event on your connected account run
stripe listen --forward-to localhost:{PORT}/webhook
in one terminal window, and runstripe trigger --stripe-account={{CONNECTED_STRIPE_ACCOUNT_ID}} payment_intent.succeeded
(or trigger any other supported event) in another.
Step 3.5: Disputes
As the settlement merchant on charges, your platform is responsible for disputes. Make sure you understand the best practices for responding to disputes.
Complete and customize your integration
You now have a working integration. From your account dashboard, you can view an account and its balance.
You can review other Connect features that you might want to consider adding to the integration.
Payouts
By default, any charge that you transfer to a connected account accumulates in the connected account’s Stripe balance and is paid out on a daily rolling basis. But, you can change the payout schedule if needed.
Testing
The Connect-specific testing resource provides tokens to help simulate flows for accounts and onboarding, payouts, and top-ups. To test your payments and disputes flow, we provide a number of test cards available to simulate payment outcomes.