original source : https://stripe.com/docs/connect/collect-then-transfer-guide
Collect payments from customers and pay them out to sellers or service providers.
이글에서는 이해를 돕기 위해 집 renter와 rentee를 연결해주는 플랫폼을 만든다고 가정했다.
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.
1. set up stripe library -server side
# Install through pip
pip install --upgrade stripe
2. create 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 user in Stripe’s API and help facilitate the collection of onboarding requirements so Stripe can verify the user’s identity. In our home-rental example, the connected account represents the homeowner.
connected account를 만들수 있게 아래와 같은 버튼을 만들어 준다.

클리하면 아래와 같은 페이지로 이동하게 된다.

2-1 Add an authentication button -client side
Add an OAuth link to your website so the user can sign up for your platform:(위위 그림같은 버튼에 아래링크 넣는다.)
https://connect.stripe.com/express/oauth/authorize?client_id=ca_32D88BD1qLklliziD7gYQvctJIhWBSQ7&state={STATE_VALUE}&suggested_capabilities[]=transfers&stripe_user[email]=user@example.com
client_id
– This can be found in your platform settings. If you’re testing your integration, enable View test data in the Dashboard. (개발자만의 id라고 생각할수 있다. stripe가 connected account를 만들때 개발자가 누군지 알아야 하므로)state
– Use this argument to prevent CSRF attacks. It should be a unique, not guessable value that’s generated and saved on your server. Stripe passes it back to your redirect after the user finishes the onboarding flow. (개발자가 임의로 만들어 제공하는 csrf token이며 stripe account생성작업을 마치고 돌아오는 user인지를 확인할때 사용한다.)suggested_capabilities
– Use this argument only if you want to customize the capabilities for certain subsets of your connected accounts. Capabilities define what a connected account is allowed to do. In our home-rental example, we want the ability to process credit card and ACH payments directly from within the platform, then transfer some percentage of the funds to the homeowner. For this, we want the homeowner to have the transfers capability so they can receive transfers after fulfilling their onboarding requirements.stripe_user
– You can use this argument to prefill any fields you already have so the user won’t need to provide that information again. In our home-rental example, we collected the homeowner’s email address when they signed up so this piece of information can be passed in the OAuth link. (위 그림 form 입력란에 미리 얻은 정보로 pre fill 할수 있게 한다.)
2-2 Save the account ID -SERVER-SIDE
In platform settings, specify where the user should be redirected to after they’ve completed the OAuth flow with redirect_uri
.

For example, if your redirect URI is https://www.example.com/connect/oauth
, Stripe might redirect to https://www.example.com/connect/oauth?state=s_987654321&code=ac_123456789
.
stripe connected account 를 만드는 작업을 한후 되돌아 올때 state (개발자가 만들어서 보낸 csrf token이 되돌아온다.)
If there’s an error, the URI contains the error
and error_description
parameters instead.
Write a GET handler to extract the parameters from the URI, verify the state
against the state you saved on your server, and send the token to Stripe’s API:
import stripe
import json
# Using Flask.
from flask import (
Flask,
render_template,
request,
)
app = Flask(__name__, static_folder=".",
static_url_path="", template_folder=".")
@app.route('/', methods=['GET'])
def get_example():
# Display landing page.
return render_template('index.html')
# 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 = 'sk_test_4G17rywGpaql6aqpnHVjEdFc'
@app.route("/connect/oauth", methods=["GET"])
def handle_oauth_redirect():
# Assert the state matches the state you provided in the OAuth link (optional).
state = request.args.get("state")
if not state_matches(state):
return json.dumps({"error": "Incorrect state parameter: " + state}), 403
# Send the authorization code to Stripe's API.
code = request.args.get("code")
try:
response = stripe.OAuth.token(grant_type="authorization_code", code=code,)
except stripe.oauth_error.OAuthError as e:
return json.dumps({"error": "Invalid authorization code: " + code}), 400
except Exception as e:
return json.dumps({"error": "An unknown error occurred."}), 500
connected_account_id = response["stripe_user_id"]
save_account_id(connected_account_id)
# Render some HTML or redirect to a different page.
return json.dumps({"success": True}), 200
def state_matches(state_parameter):
# Load the same state value that you randomly generated for your OAuth link.
saved_state = "{{ STATE }}"
return saved_state == state_parameter
def save_account_id(id):
# Save the connected account ID from the response to your database.
print("Connected account ID: ", id)
if __name__ == "__main__":
app.run(port=4242)
response = stripe.OAuth.token(grant_type=“authorization_code”, code=code,)
response[“stripe_user_id”]
위 코드와 살펴본다. 위 과정을 통해 stripe_user_id 를 얻을수 있고 나중을 위해 이를 database에 저장한다. stripe_user_id 는 acct_ 로 시작한다. stripe_user_id 는 나중에 account로 자금을 이동시킬때 사용된다.
Make sure to save the stripe_user_id
parameter that’s returned to your database. This is your connected account’s ID and begins with acct_
. You will need it later to transfer funds to the account.
Step 2.3: Customize your signup form
By default, your Express signup form might look like this:

위와 같은 기본 폼을 변경하기 위해서는 platform settings 에서 아래와 같은 폼을 통해 한다.

3. 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. (두가지 방법이 documentation 에 있다. 간단하게 stripe의 checkout를 이용하는 방법만을 여기서는 살펴본다. 다른 방법도 본래 doc에는 설명하고 있다. 아래와 같은 모양이 된다. 버튼을 클릭하면 stripe가 제공하는 page로 이동하게 된다.)

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. ( PaymentIntent 생성시에 명확하게 금액과 currency를 명시해야 한다.)
# 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 = 'sk_test_4G17rywGpaql6aqpnHVjEdFc'
payment_intent = stripe.PaymentIntent.create(
payment_method_types=['card'],
amount=1000,
currency='usd',
application_fee_amount=123,
transfer_data={
'destination': '{{CONNECTED_STRIPE_ACCOUNT_ID}}',
}
)
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:
transfer_data[destination] 는 connected account를 말한다. 위의 2-2에서 말한 stripe_user_id를 기입한다. destination 이라고는 하지만 실제 자금의 흐름은 connected account로 들어갔다가 개발자 account로 수수료가 오고 이 금액에서 다시 stripe가 본인들의 수수료를 가져간다.

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.
return 된 PaymentIntent 안에는 client secret 이 들어 있으며 이를 client side에서 사용 payment 과정을 마무리하게 된다. PaymentIntent_obj.client_secret 이와 같이 client_secret을 얻을수 있다.
single page application과 server side rendering을 이용하는 방법이 doc에는 나와 있는데 일단 server side rendering 방법을 사용하기로 정함
server side rendering
You can embed it in a data attribute or hidden HTML element and then extract it with JavaScript in order to use it to complete payment.
checkout.html
<input id="card-name" type="text">
<!-- placeholder for Elements -->
<div id="card-element"></div>
<button id="card-button" data-secret="{{ client_secret }}">
Submit Payment
</button>
app.py
@app.route('/checkout')
def checkout():
intent = # ... Fetch or create the PaymentIntent
return render_template('checkout.html', client_secret=intent.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 a 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. (실제에서는 https에서만 stripe element가 작동하게 된다. 즉 사용자가 카드번호넣고 클릭하는 버튼은 https에서만 작동 단 test시에는 상관없음)
checkout.html
<head>
<title>Checkout</title>
https://js.stripe.com/v3/
</head>
js 화일을 다운받아서 사용하면 안된다. 링크를 이용해야 한다.
script.js
// 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('pk_test_내테스트키');
var elements = stripe.elements();
Add Elements to your payment page
payment button Element 는 payment form 안에 넣는다. Create empty DOM nodes (containers) with unique IDs in your payment form and then pass those IDs to Elements. js 에서 unique ID element를 찾아서 입력값에 접근 이를 stripe에 전송하게 된다.
checkout.html
<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: mount()를 통해 입력값에 접근
client.js
// Set up Stripe.js and Elements to use in checkout form
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.
간단하게 위와 같이 card만 생성하는 경우 자동으로 필요한 fields가 만들어지게 된다. 개발자가 각 항목을 정해서 만들수도 있다. card만 생성하는 경우 postal code를 입력하는 입력란이 자동으로 만들어 지는 데 개발자가 각 항목을 따로 만드는 경우 postal code입력란도 만들어야 한다.
For a full list of supported Element types, refer to our Stripe.js reference documentation. (다양한 입력 element를 제공하는데 doc을 확인하면 알수 있다.)
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.addEventListener('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]
.
client.js
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.
stripe.confirmCardPayment 작업이 시간이 몇초 걸릴수 있으므로 기다리는 동안 payment클릭이 불가능하게 만들어서 사용자가 여러번 같은 payment를 하지 않게 한다
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 4000 0025 0000 3155
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
이 단계에서는 payment가 완료된 상황에서 수행할 작업을 수행한다.
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.
Do not rely on the redirect to the success_url
param alone for fulfilling purchases as: (success_url parameters를 믿지 않는다.)
- Malicious users could directly access the
success_url
without paying and gain access to your goods or services. - Customers may not always reach the
success_url
after a successful payment. It is possible they close their browser tab before the redirect occurs.
If you’re using Checkout, configure a webhook endpoint (for events from your account) in your dashboard.

server.py
import stripe
import json
# Using Flask.
from flask import (
Flask,
render_template,
request,
Response,
)
app = Flask(__name__, static_folder=".",
static_url_path="", template_folder=".")
# 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 = 'sk_test_4G17rywGpaql6aqpnHVjEdFc'
# Uncomment and replace with a real secret. You can find your endpoint's
# secret in your webhook settings.
# webhook_secret = 'whsec_...'
@app.route("/webhook", methods=["POST"])
def webhook_received():
request_data = json.loads(request.data)
signature = request.headers.get("stripe-signature")
# Verify webhook signature and extract the event.
# See https://stripe.com/docs/webhooks/signatures for more information.
try:
event = stripe.Webhook.construct_event(
payload=request.data, sig_header=signature, secret=webhook_secret
)
except ValueError as e:
# Invalid payload.
return Response(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid Signature.
return Response(status=400)
if event["type"] == "payment_intent.succeeded":
payment_intent = event["data"]["object"]
handle_successful_payment_intent(payment_intent)
return json.dumps({"success": True}), 200
def handle_successful_payment_intent(payment_intent):
# Fulfill the purchase.
print('PaymentIntent: ' + str(payment_intent))
if __name__ == "__main__":
app.run(port=4242)
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 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.
4 Complete and customize your integration
You now have a working integration. From your account dashboard, you can view an account and its balance.

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 there are a number of test cards available to simulate payment outcomes.
Other resources
You have completed the steps required to build a working Connect integration. Based on your business needs, you can also do the following:
Manage connected accounts