# Fulfill orders Learn how to fulfill payments received with the Checkout Sessions API. # Stripe-hosted page > This is a Stripe-hosted page for when payment-ui is stripe-hosted. View the original doc at https://docs.stripe.com/checkout/fulfillment?payment-ui=stripe-hosted. When you receive a payment with the Checkout Sessions API (including Payment Links), you might need to take action to provide your customer with what they paid for. For example, you might need to grant them access to a service, or you might need to ship them physical goods. This process is known as fulfillment, and you have two ways to handle this process: - **Manually**: You can manually fulfill orders using information that Stripe makes available to you. For example, you can monitor the [Dashboard](https://docs.stripe.com/dashboard/basics.md), check payment notification emails, or look at reports and then fulfill orders. - **Automatically**: You can build an automated fulfillment system. The first option works for low volume or experimental ventures, but for most situations we recommend automating fulfillment. The rest of this guide shows you how to build an automatic fulfillment system. ## Automatic fulfillment The automatic fulfillment system outlined below uses a combination of *webhooks* and a redirect to your website to trigger fulfillment. You must use webhooks to make sure fulfillment happens for every payment, and redirects let your customers access services or fulfillment details immediately after paying. Payment Links use Checkout, so all of the information below applies to both Payment Links and Checkout unless otherwise noted. ## Create a fulfillment function Create a function on your server to fulfill successful payments. Webhooks trigger this function, and it’s called when customers are sent to your website after completing checkout. This guide refers to this function as `fulfill_checkout`, but you can name the function whatever you wish. Perform fulfillment only once per payment. Because of how this integration and the internet work, your `fulfill_checkout` function might be called multiple times, possibly concurrently, for the same Checkout Session. Performing checkout only once ensures this won’t cause undesired behavior. Your `fulfill_checkout` function must: 1. Correctly handle being called multiple times with the same Checkout Session ID. 1. Accept a *Checkout Session* ID as an argument. 1. Retrieve the Checkout Session from the API with the [line_items](https://docs.stripe.com/api/checkout/sessions/object.md#checkout_session_object-line_items) property [expanded](https://docs.stripe.com/api/expanding_objects.md). 1. Check the [payment_status](https://docs.stripe.com/api/checkout/sessions/object.md#checkout_session_object-payment_status) property to determine if it requires fulfillment. 1. Perform fulfillment of the line items. 1. Record fulfillment status for the provided Checkout Session. Use the code below as a starting point for your `fulfill_checkout` function. The `TODO` comments indicate any functionality you must implement. The code snippets below might name the `fulfill_checkout` function `fulfillCheckout` or `FulfillCheckout` depending on the language selected, but they all represent the same function. ```ruby def fulfill_checkout(session_id)<> puts "Fullfilling Checkout Session #{session_id}" # TODO: Make this function safe to run multiple times, # even concurrently, with the same session ID # TODO: Make sure fulfillment hasn't already been # performed for this Checkout Session # Retrieve the Checkout Session from the API with line_items expanded checkout_session = Stripe::Checkout::Session.retrieve({ id: session_id, expand: ['line_items'], }) # Check the Checkout Session's payment_status property # to determine if fulfillment should be performed if checkout_session.payment_status != 'unpaid' # TODO: Perform fulfillment of the line items # TODO: Record/save fulfillment status for this # Checkout Session end end ``` ```python <> def fulfill_checkout(session_id) print("Fulfilling Checkout Session", session_id) # TODO: Make this function safe to run multiple times, # even concurrently, with the same session ID # TODO: Make sure fulfillment hasn't already been # performed for this Checkout Session # Retrieve the Checkout Session from the API with line_items expanded checkout_session = stripe.checkout.Session.retrieve( session_id, expand=['line_items'], ) # Check the Checkout Session's payment_status property # to determine if fulfillment should be performed if checkout_session.payment_status != 'unpaid': # TODO: Perform fulfillment of the line items # TODO: Record/save fulfillment status for this # Checkout Session ``` ```php function fulfill_checkout($session_id) {<> // TODO: Log the string "Fulfilling Checkout Session $session_id" // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded $checkout_session = $stripe->checkout->sessions->retrieve($session_id, [ 'expand' => ['line_items'], ]); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if ($checkout_session->payment_status != 'unpaid') { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```java public void fulfillCheckout(String sessionId) {<> System.out.println("Fulfilling Checkout Session " + sessionId); // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded SessionRetrieveParams params = SessionRetrieveParams.builder() .addExpand("line_items") .build(); Session checkoutSession = Session.retrieve(sessionId, params, null); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (checkoutSession.getPaymentStatus() != 'unpaid') { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```node async function fulfillCheckout(sessionId) {<> console.log('Fulfilling Checkout Session ' + sessionId); // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded const checkoutSession = await stripe.checkout.sessions.retrieve(sessionId, { expand: ['line_items'], }); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (checkoutSession.payment_status !== 'unpaid') { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```go func FulfillCheckout(sessionId string) {<> fmt.Println("Fulfilling Checkout Session " + sessionId) // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded params := &stripe.CheckoutSessionParams{} params.AddExpand("line_items") cs, _ := session.Get(sessionId, params) // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (cs.PaymentStatus != stripe.CheckoutSessionPaymentStatusUnpaid) { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```dotnet public void FulfillCheckout(String sessionId) {<> Console.WriteLine("Fulfilling Checkout Session " + sessionId); // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded var options = new SessionGetOptions { Expand = new List { "line_items" }, }; var service = new SessionService(); var checkoutSession = service.Get(sessionId, options); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (checkoutSession.PaymentStatus != "unpaid") { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` If a Checkout Session has many line items, use [auto-pagination](https://docs.stripe.com/api/pagination/auto.md) with the [API for Checkout line items](https://docs.stripe.com/api/checkout/sessions/line_items.md) to retrieve all of them. Depending on the payment methods you accept and your business needs, you might want to have your `fulfill_checkout` function do the following: - Provision access to services. - Trigger shipment of goods. - Save a copy of the payment details and line items in your own database. - Send the customer a custom receipt email if you don’t have [Stripe’s receipts](https://docs.stripe.com/receipts.md) enabled. - Reconcile line items and quantities purchased if you allow customers to adjust quantities in Checkout. - Update inventory or stock records. ## Create a payment event handler To trigger fulfillment, create a webhook event handler to listen for payment events and trigger your `fulfill_checkout` function. When someone pays you, it creates a `checkout.session.completed` event. Set up an endpoint on your server to accept, process, and confirm receipt of these events. ### Immediate versus delayed payment methods Some payment methods aren’t [instant](https://docs.stripe.com/payments/payment-methods.md#payment-notification), such as [ACH direct debit](https://docs.stripe.com/payments/ach-direct-debit.md) and other bank transfers. This means, funds won’t be immediately available when Checkout completes. Delayed payment methods generate a [checkout.session.async_payment_succeeded](https://docs.stripe.com/api/events/types.md#event_types-checkout.session.async_payment_succeeded) event when payment succeeds later. The status of the object is in processing until the payment status either succeeds or fails. The webhook secret (`whsec_...`) shown in the code below comes from either the Stripe CLI or your webhook endpoint. You can use the Stripe CLI for local testing, and Stripe uses a webhook endpoint to send events to your handler when it’s running on a server. See the next section for more details. ```ruby require 'sinatra' # Use the secret provided by Stripe CLI for local testing # or your webhook endpoint's secret. endpoint_secret = 'whsec_...' post '/webhook' do event = nil # Verify webhook signature and extract the event # See https://stripe.com/docs/webhooks#verify-events for more information. begin sig_header = request.env['HTTP_STRIPE_SIGNATURE'] payload = request.body.read event = Stripe::Webhook.construct_event(payload, sig_header, endpoint_secret) rescue JSON::ParserError => e # Invalid payload return status 400 rescue Stripe::SignatureVerificationError => e # Invalid signature return status 400 end if event['type'] == 'checkout.session.completed' || event['type'] == 'checkout.session.async_payment_succeeded' fulfill_checkout(event['data']['object']['id']) end status 200 end ``` ```python \# Using Django from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt # Use the secret provided by Stripe CLI for local testing # or your webhook endpoint's secret. endpoint_secret = 'whsec_...' @csrf_exempt def my_webhook_view(request): payload = request.body sig_header = request.META['HTTP_STRIPE_SIGNATURE'] event = None try: event = stripe.Webhook.construct_event( payload, sig_header, endpoint_secret ) except ValueError as e: # Invalid payload return HttpResponse(status=400) except stripe.error.SignatureVerificationError as e: # Invalid signature return HttpResponse(status=400) if ( event['type'] == 'checkout.session.completed' or event['type'] == 'checkout.session.async_payment_succeeded' ): fulfill_checkout(event['data']['object']['id']) return HttpResponse(status=200) ``` ```php // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. $endpoint_secret = 'whsec_...'; $payload = @file_get_contents('php://input'); $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE']; $event = null; try { $event = \Stripe\Webhook::constructEvent( $payload, $sig_header, $endpoint_secret ); } catch(\UnexpectedValueException $e) { // Invalid payload http_response_code(400); exit(); } catch(\Stripe\Exception\SignatureVerificationException $e) { // Invalid signature http_response_code(400); exit(); } if ( $event->type == 'checkout.session.completed' || $event->type == 'checkout.session.async_payment_succeeded' ) { fulfill_checkout($event->data->object->id); } http_response_code(200); ``` ```java // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. String endpointSecret = "whsec_..."; // Using the Spark framework public Object handle(Request request, Response response) { String payload = request.body(); String sigHeader = request.headers("Stripe-Signature"); Event event = null; try { event = Webhook.constructEvent(payload, sigHeader, endpointSecret); } catch (JsonSyntaxException e) { // Invalid payload response.status(400); return ""; } catch (SignatureVerificationException e) { // Invalid signature response.status(400); return ""; } if ( "checkout.session.completed".equals(event.getType()) || "checkout.session.async_payment_succeeded".equals(event.getType()) ) { Session sessionEvent= (Session) event.getDataObjectDeserializer().getObject(); fulfillCheckout(sessionEvent.getId()); } response.status(200); return ""; } ``` ```javascript // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. const endpointSecret = 'whsec_...'; // Using Express const app = require('express')(); // Use body-parser to retrieve the raw body as a buffer const bodyParser = require('body-parser'); app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (request, response) => { const payload = request.body; const sig = request.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(payload, sig, endpointSecret); } catch (err) { return response.status(400).send(`Webhook Error: ${err.message}`); } if ( event.type === 'checkout.session.completed' || event.type === 'checkout.session.async_payment_succeeded' ) { fulfillCheckout(event.data.object.id); } response.status(200).end(); }); app.listen(4242, () => console.log('Running on port 4242')); ``` ```go func main() { http.HandleFunc("/webhook", handleWebhook) addr := "localhost:4242" log.Printf("Listening on %s", addr) log.Fatal(http.ListenAndServe(addr, nil)) } func handleWebhook(w http.ResponseWriter, req *http.Request) { const MaxBodyBytes = int64(65536) req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes) body, err := ioutil.ReadAll(req.Body) if err != nil { fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err) w.WriteHeader(http.StatusServiceUnavailable) return } // Pass the request body and Stripe-Signature header to ConstructEvent, along with the webhook signing key // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. endpointSecret := "whsec_..."; event, err := webhook.ConstructEvent(body, req.Header.Get("Stripe-Signature"), endpointSecret) if err != nil { fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err) w.WriteHeader(http.StatusBadRequest) // Return a 400 error on a bad signature return } if event.Type == stripe.EventTypeCheckoutSessionCompleted || event.Type == stripe.EventTypeCheckoutSessionAsyncPaymentSucceeded { var cs stripe.CheckoutSession err := json.Unmarshal(event.Data.Raw, &cs) if err != nil { fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) w.WriteHeader(http.StatusBadRequest) return } FulfillCheckout(cs.ID) } w.WriteHeader(http.StatusOK) } ``` ```dotnet namespace workspace.Controllers { [Route("webhook")] [ApiController] public class WebhookController : Controller { // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. const string secret = "whsec_..."; [HttpPost] public async Task Index() { var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); try { var stripeEvent = EventUtility.ConstructEvent( json, Request.Headers["Stripe-Signature"], secret ); // If on SDK version < 46, use class Events instead of EventTypes if ( stripeEvent.Type == EventTypes.CheckoutSessionCompleted || stripeEvent.Type == EventTypes.CheckoutSessionAsyncPaymentSucceeded ) { var session = stripeEvent.Data.Object as Session; FulfillCheckout(session.Id); } return Ok(); } catch (StripeException) { return BadRequest(); } } } } ``` You might also want to listen for and handle `checkout.session.async_payment_failed` events. For example, you can send an email to your customer when a delayed payment fails. ## Test your event handler locally The quickest way to develop and test your webhook event handler is with the [Stripe CLI](https://docs.stripe.com/stripe-cli.md). If you don’t have the Stripe CLI, follow the [install guide](https://docs.stripe.com/stripe-cli.md#install) to get started. When the Stripe CLI is installed, you can test your event handler locally. Run your server (for example, on `localhost:4242`), then run the [stripe listen](https://docs.stripe.com/cli/listen) command to have the Stripe CLI forward events to your local server: ```bash stripe listen --forward-to localhost:4242/webhook Ready! Your webhook signing secret is 'whsec_' (^C to quit) ``` Add the webhook secret (`whsec_...`) to your event handling code, then test fulfillment by going through Checkout as a customer: - Press the checkout button that takes you to Checkout, or visit your Payment Link - Provide the following test data in Checkout: - Enter `4242 4242 4242 4242` as the card number - Enter any future date for card expiry - Enter any 3-digit number for CVV - Enter any billing postal code (`90210`) - Press the **Pay** button When the payment completes, verify the following: - On your command line, where `stripe listen` is running, it shows a `checkout.session.completed` event forwarded to your local server. - Your server logs show the expected output from your `fulfill_checkout` function. ## Create a webhook endpoint After testing locally, get your webhook event handler up and running on your server. Next, [create a webhook endpoint](https://docs.stripe.com/webhooks.md#register-webhook) to send `checkout.session.completed` events to your server, then test the Checkout flow again. ## Configure a landing page URL Configure Checkout to send your customer to a page on your website after they complete Checkout. Include the `{CHECKOUT_SESSION_ID}` placeholder in your page’s URL, which is replaced with the Checkout Session ID when your customer is redirected from Checkout. ### Hosted Checkout For Checkout Sessions with the default [ui_mode](https://docs.stripe.com/api/checkout/sessions/create.md#create_checkout_session-ui_mode) of `hosted`, set the `success_url`. ```dotnet StripeConfiguration.ApiKey = "<>"; var options = new Stripe.Checkout.SessionCreateOptions { LineItems = new List { new Stripe.Checkout.SessionLineItemOptions { Price = "<>", Quantity = 1 }, }, Mode = "payment", SuccessUrl = "https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}", }; var service = new Stripe.Checkout.SessionService(); Stripe.Checkout.Session session = service.Create(options); ``` ```go stripe.Key = "<>" params := &stripe.CheckoutSessionParams{ LineItems: []*stripe.CheckoutSessionLineItemParams{ &stripe.CheckoutSessionLineItemParams{ Price: stripe.String("<>"), Quantity: stripe.Int64(1), }, }, Mode: stripe.String(string(stripe.CheckoutSessionModePayment)), SuccessURL: stripe.String("https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}"), }; result, err := session.New(params); ``` ```java Stripe.apiKey = "<>"; SessionCreateParams params = SessionCreateParams.builder() .addLineItem( SessionCreateParams.LineItem.builder().setPrice("<>").setQuantity(1L).build() ) .setMode(SessionCreateParams.Mode.PAYMENT) .setSuccessUrl("https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}") .build(); Session session = Session.create(params); ``` ```node const stripe = require('stripe')('<>'); const session = await stripe.checkout.sessions.create({ line_items: [ { price: '<>', quantity: 1, }, ], mode: 'payment', success_url: 'https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}', }); ``` ```python import stripe stripe.api_key = "<>" session = stripe.checkout.Session.create( line_items=[{"price": "<>", "quantity": 1}], mode="payment", success_url="https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}", ) ``` ```php $stripe = new \Stripe\StripeClient('<>'); $session = $stripe->checkout->sessions->create([ 'line_items' => [ [ 'price' => '<>', 'quantity' => 1, ], ], 'mode' => 'payment', 'success_url' => 'https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}', ]); ``` ```ruby Stripe.api_key = '<>' session = Stripe::Checkout::Session.create({ line_items: [ { price: '<>', quantity: 1, }, ], mode: 'payment', success_url: 'https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}', }) ``` When you have a webhook endpoint set up to listen for `checkout.session.completed` events and you set a `success_url`, Checkout waits up to 10 seconds for your server to respond to the webhook event delivery before redirecting your customer. If you use this approach, make sure your server responds to `checkout.session.completed` events as quickly as possible. This behavior isn’t supported for webhook endpoints registered in an[organization](https://docs.stripe.com/get-started/account/orgs.md) account. Stripe doesn’t wait for organization webhook endpoints that listen to `checkout.sessions.completed` to respond when redirecting Checkout customers. ### Payment Links For Payment Links you create with the API, set the [after_completion.redirect.url](https://docs.stripe.com/api/payment_links/payment_links/create.md#create_payment_link-after_completion-redirect-url). ```dotnet StripeConfiguration.ApiKey = "<>"; var options = new PaymentLinkCreateOptions { LineItems = new List { new PaymentLinkLineItemOptions { Price = "<>", Quantity = 1 }, }, AfterCompletion = new PaymentLinkAfterCompletionOptions { Type = "redirect", Redirect = new PaymentLinkAfterCompletionRedirectOptions { Url = "https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}", }, }, }; var service = new PaymentLinkService(); PaymentLink paymentLink = service.Create(options); ``` ```go stripe.Key = "<>" params := &stripe.PaymentLinkParams{ LineItems: []*stripe.PaymentLinkLineItemParams{ &stripe.PaymentLinkLineItemParams{Price: stripe.String("<>"), Quantity: stripe.Int64(1)}, }, AfterCompletion: &stripe.PaymentLinkAfterCompletionParams{ Type: stripe.String(string(stripe.PaymentLinkAfterCompletionTypeRedirect)), Redirect: &stripe.PaymentLinkAfterCompletionRedirectParams{ URL: stripe.String("https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}"), }, }, }; result, err := paymentlink.New(params); ``` ```java Stripe.apiKey = "<>"; PaymentLinkCreateParams params = PaymentLinkCreateParams.builder() .addLineItem( PaymentLinkCreateParams.LineItem.builder().setPrice("<>").setQuantity(1L).build() ) .setAfterCompletion( PaymentLinkCreateParams.AfterCompletion.builder() .setType(PaymentLinkCreateParams.AfterCompletion.Type.REDIRECT) .setRedirect( PaymentLinkCreateParams.AfterCompletion.Redirect.builder() .setUrl("https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}") .build() ) .build() ) .build(); PaymentLink paymentLink = PaymentLink.create(params); ``` ```node const stripe = require('stripe')('<>'); const paymentLink = await stripe.paymentLinks.create({ line_items: [ { price: '<>', quantity: 1, }, ], after_completion: { type: 'redirect', redirect: { url: 'https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}', }, }, }); ``` ```python import stripe stripe.api_key = "<>" payment_link = stripe.PaymentLink.create( line_items=[{"price": "<>", "quantity": 1}], after_completion={ "type": "redirect", "redirect": {"url": "https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}"}, }, ) ``` ```php $stripe = new \Stripe\StripeClient('<>'); $paymentLink = $stripe->paymentLinks->create([ 'line_items' => [ [ 'price' => '<>', 'quantity' => 1, ], ], 'after_completion' => [ 'type' => 'redirect', 'redirect' => ['url' => 'https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}'], ], ]); ``` ```ruby Stripe.api_key = '<>' payment_link = Stripe::PaymentLink.create({ line_items: [ { price: '<>', quantity: 1, }, ], after_completion: { type: 'redirect', redirect: {url: 'https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}'}, }, }) ``` For Payment Links you [create in the Dashboard](https://dashboard.stripe.com/payment-links/create): 1. Go to the **After Payment** tab. 1. Select **Don’t show confirmation page**. 1. Provide the URL to your landing page that includes the `{CHECKOUT_SESSION_ID}` placeholder (for example, `https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}`) ## Trigger fulfillment on your landing page [Listening to webhooks](#create-payment-event-handler) is required to make sure you always trigger fulfillment for every payment, but webhooks can sometimes be delayed. To optimize your payment flow and guarantee immediate fulfillment when your customer is present, trigger fulfillment from your landing page as well. Use the Checkout Session ID from the URL you specified in the previous step to do the following: 1. When your server receives a request for your Checkout landing page, extract the Checkout Session ID from the URL. 1. Run your `fulfill_checkout` function with the ID provided. 1. Render the page after the fulfillment attempt is complete. When you render your landing page you can display the following: - Details from the fulfillment process. - Links or information about services the customer now has access to. - Shipping or logistical details for physical goods. You can’t rely on triggering fulfillment only from your Checkout landing page, because your customers aren’t guaranteed to visit that page. For example, someone can pay successfully in Checkout and then lose their connection to the internet before your landing page loads. [Set up a webhook event handler](#create-payment-event-handler) so Stripe can send payment events directly to your server, bypassing the client entirely. Webhooks provide the most reliable way to confirm when you get paid. If webhook event delivery fails, Stripe [retries multiple times](https://docs.stripe.com/webhooks.md#automatic-retries). # Embedded form > This is a Embedded form for when payment-ui is embedded-form. View the original doc at https://docs.stripe.com/checkout/fulfillment?payment-ui=embedded-form. When you receive a payment with the Checkout Sessions API (including Payment Links), you might need to take action to provide your customer with what they paid for. For example, you might need to grant them access to a service, or you might need to ship them physical goods. This process is known as fulfillment, and you have two ways to handle this process: - **Manually**: You can manually fulfill orders using information that Stripe makes available to you. For example, you can monitor the [Dashboard](https://docs.stripe.com/dashboard/basics.md), check payment notification emails, or look at reports and then fulfill orders. - **Automatically**: You can build an automated fulfillment system. The first option works for low volume or experimental ventures, but for most situations we recommend automating fulfillment. The rest of this guide shows you how to build an automatic fulfillment system. ## Automatic fulfillment The automatic fulfillment system outlined below uses a combination of *webhooks* and a redirect to your website to trigger fulfillment. You must use webhooks to make sure fulfillment happens for every payment, and redirects let your customers access services or fulfillment details immediately after paying. ## Create a fulfillment function Create a function on your server to fulfill successful payments. Webhooks trigger this function, and it’s called when customers are sent to your website after completing checkout. This guide refers to this function as `fulfill_checkout`, but you can name the function whatever you wish. Perform fulfillment only once per payment. Because of how this integration and the internet work, your `fulfill_checkout` function might be called multiple times, possibly concurrently, for the same Checkout Session. Performing checkout only once ensures this won’t cause undesired behavior. Your `fulfill_checkout` function must: 1. Correctly handle being called multiple times with the same Checkout Session ID. 1. Accept a *Checkout Session* ID as an argument. 1. Retrieve the Checkout Session from the API with the [line_items](https://docs.stripe.com/api/checkout/sessions/object.md#checkout_session_object-line_items) property [expanded](https://docs.stripe.com/api/expanding_objects.md). 1. Check the [payment_status](https://docs.stripe.com/api/checkout/sessions/object.md#checkout_session_object-payment_status) property to determine if it requires fulfillment. 1. Perform fulfillment of the line items. 1. Record fulfillment status for the provided Checkout Session. Use the code below as a starting point for your `fulfill_checkout` function. The `TODO` comments indicate any functionality you must implement. The code snippets below might name the `fulfill_checkout` function `fulfillCheckout` or `FulfillCheckout` depending on the language selected, but they all represent the same function. ```ruby def fulfill_checkout(session_id)<> puts "Fullfilling Checkout Session #{session_id}" # TODO: Make this function safe to run multiple times, # even concurrently, with the same session ID # TODO: Make sure fulfillment hasn't already been # performed for this Checkout Session # Retrieve the Checkout Session from the API with line_items expanded checkout_session = Stripe::Checkout::Session.retrieve({ id: session_id, expand: ['line_items'], }) # Check the Checkout Session's payment_status property # to determine if fulfillment should be performed if checkout_session.payment_status != 'unpaid' # TODO: Perform fulfillment of the line items # TODO: Record/save fulfillment status for this # Checkout Session end end ``` ```python <> def fulfill_checkout(session_id) print("Fulfilling Checkout Session", session_id) # TODO: Make this function safe to run multiple times, # even concurrently, with the same session ID # TODO: Make sure fulfillment hasn't already been # performed for this Checkout Session # Retrieve the Checkout Session from the API with line_items expanded checkout_session = stripe.checkout.Session.retrieve( session_id, expand=['line_items'], ) # Check the Checkout Session's payment_status property # to determine if fulfillment should be performed if checkout_session.payment_status != 'unpaid': # TODO: Perform fulfillment of the line items # TODO: Record/save fulfillment status for this # Checkout Session ``` ```php function fulfill_checkout($session_id) {<> // TODO: Log the string "Fulfilling Checkout Session $session_id" // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded $checkout_session = $stripe->checkout->sessions->retrieve($session_id, [ 'expand' => ['line_items'], ]); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if ($checkout_session->payment_status != 'unpaid') { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```java public void fulfillCheckout(String sessionId) {<> System.out.println("Fulfilling Checkout Session " + sessionId); // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded SessionRetrieveParams params = SessionRetrieveParams.builder() .addExpand("line_items") .build(); Session checkoutSession = Session.retrieve(sessionId, params, null); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (checkoutSession.getPaymentStatus() != 'unpaid') { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```node async function fulfillCheckout(sessionId) {<> console.log('Fulfilling Checkout Session ' + sessionId); // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded const checkoutSession = await stripe.checkout.sessions.retrieve(sessionId, { expand: ['line_items'], }); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (checkoutSession.payment_status !== 'unpaid') { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```go func FulfillCheckout(sessionId string) {<> fmt.Println("Fulfilling Checkout Session " + sessionId) // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded params := &stripe.CheckoutSessionParams{} params.AddExpand("line_items") cs, _ := session.Get(sessionId, params) // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (cs.PaymentStatus != stripe.CheckoutSessionPaymentStatusUnpaid) { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```dotnet public void FulfillCheckout(String sessionId) {<> Console.WriteLine("Fulfilling Checkout Session " + sessionId); // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded var options = new SessionGetOptions { Expand = new List { "line_items" }, }; var service = new SessionService(); var checkoutSession = service.Get(sessionId, options); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (checkoutSession.PaymentStatus != "unpaid") { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` If a Checkout Session has many line items, use [auto-pagination](https://docs.stripe.com/api/pagination/auto.md) with the [API for Checkout line items](https://docs.stripe.com/api/checkout/sessions/line_items.md) to retrieve all of them. Depending on the payment methods you accept and your business needs, you might want to have your `fulfill_checkout` function do the following: - Provision access to services. - Trigger shipment of goods. - Save a copy of the payment details and line items in your own database. - Send the customer a custom receipt email if you don’t have [Stripe’s receipts](https://docs.stripe.com/receipts.md) enabled. - Reconcile line items and quantities purchased if you allow customers to adjust quantities in Checkout. - Update inventory or stock records. ## Create a payment event handler To trigger fulfillment, create a webhook event handler to listen for payment events and trigger your `fulfill_checkout` function. When someone pays you, it creates a `checkout.session.completed` event. Set up an endpoint on your server to accept, process, and confirm receipt of these events. ### Immediate versus delayed payment methods Some payment methods aren’t [instant](https://docs.stripe.com/payments/payment-methods.md#payment-notification), such as [ACH direct debit](https://docs.stripe.com/payments/ach-direct-debit.md) and other bank transfers. This means, funds won’t be immediately available when Checkout completes. Delayed payment methods generate a [checkout.session.async_payment_succeeded](https://docs.stripe.com/api/events/types.md#event_types-checkout.session.async_payment_succeeded) event when payment succeeds later. The status of the object is in processing until the payment status either succeeds or fails. The webhook secret (`whsec_...`) shown in the code below comes from either the Stripe CLI or your webhook endpoint. You can use the Stripe CLI for local testing, and Stripe uses a webhook endpoint to send events to your handler when it’s running on a server. See the next section for more details. ```ruby require 'sinatra' # Use the secret provided by Stripe CLI for local testing # or your webhook endpoint's secret. endpoint_secret = 'whsec_...' post '/webhook' do event = nil # Verify webhook signature and extract the event # See https://stripe.com/docs/webhooks#verify-events for more information. begin sig_header = request.env['HTTP_STRIPE_SIGNATURE'] payload = request.body.read event = Stripe::Webhook.construct_event(payload, sig_header, endpoint_secret) rescue JSON::ParserError => e # Invalid payload return status 400 rescue Stripe::SignatureVerificationError => e # Invalid signature return status 400 end if event['type'] == 'checkout.session.completed' || event['type'] == 'checkout.session.async_payment_succeeded' fulfill_checkout(event['data']['object']['id']) end status 200 end ``` ```python \# Using Django from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt # Use the secret provided by Stripe CLI for local testing # or your webhook endpoint's secret. endpoint_secret = 'whsec_...' @csrf_exempt def my_webhook_view(request): payload = request.body sig_header = request.META['HTTP_STRIPE_SIGNATURE'] event = None try: event = stripe.Webhook.construct_event( payload, sig_header, endpoint_secret ) except ValueError as e: # Invalid payload return HttpResponse(status=400) except stripe.error.SignatureVerificationError as e: # Invalid signature return HttpResponse(status=400) if ( event['type'] == 'checkout.session.completed' or event['type'] == 'checkout.session.async_payment_succeeded' ): fulfill_checkout(event['data']['object']['id']) return HttpResponse(status=200) ``` ```php // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. $endpoint_secret = 'whsec_...'; $payload = @file_get_contents('php://input'); $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE']; $event = null; try { $event = \Stripe\Webhook::constructEvent( $payload, $sig_header, $endpoint_secret ); } catch(\UnexpectedValueException $e) { // Invalid payload http_response_code(400); exit(); } catch(\Stripe\Exception\SignatureVerificationException $e) { // Invalid signature http_response_code(400); exit(); } if ( $event->type == 'checkout.session.completed' || $event->type == 'checkout.session.async_payment_succeeded' ) { fulfill_checkout($event->data->object->id); } http_response_code(200); ``` ```java // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. String endpointSecret = "whsec_..."; // Using the Spark framework public Object handle(Request request, Response response) { String payload = request.body(); String sigHeader = request.headers("Stripe-Signature"); Event event = null; try { event = Webhook.constructEvent(payload, sigHeader, endpointSecret); } catch (JsonSyntaxException e) { // Invalid payload response.status(400); return ""; } catch (SignatureVerificationException e) { // Invalid signature response.status(400); return ""; } if ( "checkout.session.completed".equals(event.getType()) || "checkout.session.async_payment_succeeded".equals(event.getType()) ) { Session sessionEvent= (Session) event.getDataObjectDeserializer().getObject(); fulfillCheckout(sessionEvent.getId()); } response.status(200); return ""; } ``` ```javascript // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. const endpointSecret = 'whsec_...'; // Using Express const app = require('express')(); // Use body-parser to retrieve the raw body as a buffer const bodyParser = require('body-parser'); app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (request, response) => { const payload = request.body; const sig = request.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(payload, sig, endpointSecret); } catch (err) { return response.status(400).send(`Webhook Error: ${err.message}`); } if ( event.type === 'checkout.session.completed' || event.type === 'checkout.session.async_payment_succeeded' ) { fulfillCheckout(event.data.object.id); } response.status(200).end(); }); app.listen(4242, () => console.log('Running on port 4242')); ``` ```go func main() { http.HandleFunc("/webhook", handleWebhook) addr := "localhost:4242" log.Printf("Listening on %s", addr) log.Fatal(http.ListenAndServe(addr, nil)) } func handleWebhook(w http.ResponseWriter, req *http.Request) { const MaxBodyBytes = int64(65536) req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes) body, err := ioutil.ReadAll(req.Body) if err != nil { fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err) w.WriteHeader(http.StatusServiceUnavailable) return } // Pass the request body and Stripe-Signature header to ConstructEvent, along with the webhook signing key // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. endpointSecret := "whsec_..."; event, err := webhook.ConstructEvent(body, req.Header.Get("Stripe-Signature"), endpointSecret) if err != nil { fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err) w.WriteHeader(http.StatusBadRequest) // Return a 400 error on a bad signature return } if event.Type == stripe.EventTypeCheckoutSessionCompleted || event.Type == stripe.EventTypeCheckoutSessionAsyncPaymentSucceeded { var cs stripe.CheckoutSession err := json.Unmarshal(event.Data.Raw, &cs) if err != nil { fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) w.WriteHeader(http.StatusBadRequest) return } FulfillCheckout(cs.ID) } w.WriteHeader(http.StatusOK) } ``` ```dotnet namespace workspace.Controllers { [Route("webhook")] [ApiController] public class WebhookController : Controller { // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. const string secret = "whsec_..."; [HttpPost] public async Task Index() { var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); try { var stripeEvent = EventUtility.ConstructEvent( json, Request.Headers["Stripe-Signature"], secret ); // If on SDK version < 46, use class Events instead of EventTypes if ( stripeEvent.Type == EventTypes.CheckoutSessionCompleted || stripeEvent.Type == EventTypes.CheckoutSessionAsyncPaymentSucceeded ) { var session = stripeEvent.Data.Object as Session; FulfillCheckout(session.Id); } return Ok(); } catch (StripeException) { return BadRequest(); } } } } ``` You might also want to listen for and handle `checkout.session.async_payment_failed` events. For example, you can send an email to your customer when a delayed payment fails. ## Test your event handler locally The quickest way to develop and test your webhook event handler is with the [Stripe CLI](https://docs.stripe.com/stripe-cli.md). If you don’t have the Stripe CLI, follow the [install guide](https://docs.stripe.com/stripe-cli.md#install) to get started. When the Stripe CLI is installed, you can test your event handler locally. Run your server (for example, on `localhost:4242`), then run the [stripe listen](https://docs.stripe.com/cli/listen) command to have the Stripe CLI forward events to your local server: ```bash stripe listen --forward-to localhost:4242/webhook Ready! Your webhook signing secret is 'whsec_' (^C to quit) ``` Add the webhook secret (`whsec_...`) to your event handling code, then test fulfillment by going through Checkout as a customer: - Press the checkout button that takes you to Checkout, or visit your Payment Link - Provide the following test data in Checkout: - Enter `4242 4242 4242 4242` as the card number - Enter any future date for card expiry - Enter any 3-digit number for CVV - Enter any billing postal code (`90210`) - Press the **Pay** button When the payment completes, verify the following: - On your command line, where `stripe listen` is running, it shows a `checkout.session.completed` event forwarded to your local server. - Your server logs show the expected output from your `fulfill_checkout` function. ## Create a webhook endpoint After testing locally, get your webhook event handler up and running on your server. Next, [create a webhook endpoint](https://docs.stripe.com/webhooks.md#register-webhook) to send `checkout.session.completed` events to your server, then test the Checkout flow again. ## Configure a landing page URL Configure Checkout to send your customer to a page on your website after they complete Checkout. Include the `{CHECKOUT_SESSION_ID}` placeholder in your page’s URL, which is replaced with the Checkout Session ID when your customer completes the checkout process. ```dotnet StripeConfiguration.ApiKey = "<>"; var options = new Stripe.Checkout.SessionCreateOptions { LineItems = new List { new Stripe.Checkout.SessionLineItemOptions { Price = "<>", Quantity = 1 }, }, Mode = "payment", UiMode = "embedded", ReturnUrl = "https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}", }; var service = new Stripe.Checkout.SessionService(); Stripe.Checkout.Session session = service.Create(options); ``` ```go stripe.Key = "<>" params := &stripe.CheckoutSessionParams{ LineItems: []*stripe.CheckoutSessionLineItemParams{ &stripe.CheckoutSessionLineItemParams{ Price: stripe.String("<>"), Quantity: stripe.Int64(1), }, }, Mode: stripe.String(string(stripe.CheckoutSessionModePayment)), UIMode: stripe.String(string(stripe.CheckoutSessionUIModeEmbedded)), ReturnURL: stripe.String("https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}"), }; result, err := session.New(params); ``` ```java Stripe.apiKey = "<>"; SessionCreateParams params = SessionCreateParams.builder() .addLineItem( SessionCreateParams.LineItem.builder().setPrice("<>").setQuantity(1L).build() ) .setMode(SessionCreateParams.Mode.PAYMENT) .setUiMode(SessionCreateParams.UiMode.EMBEDDED) .setReturnUrl("https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}") .build(); Session session = Session.create(params); ``` ```node const stripe = require('stripe')('<>'); const session = await stripe.checkout.sessions.create({ line_items: [ { price: '<>', quantity: 1, }, ], mode: 'payment', ui_mode: 'embedded', return_url: 'https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}', }); ``` ```python import stripe stripe.api_key = "<>" session = stripe.checkout.Session.create( line_items=[{"price": "<>", "quantity": 1}], mode="payment", ui_mode="embedded", return_url="https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}", ) ``` ```php $stripe = new \Stripe\StripeClient('<>'); $session = $stripe->checkout->sessions->create([ 'line_items' => [ [ 'price' => '<>', 'quantity' => 1, ], ], 'mode' => 'payment', 'ui_mode' => 'embedded', 'return_url' => 'https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}', ]); ``` ```ruby Stripe.api_key = '<>' session = Stripe::Checkout::Session.create({ line_items: [ { price: '<>', quantity: 1, }, ], mode: 'payment', ui_mode: 'embedded', return_url: 'https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}', }) ``` ## Trigger fulfillment on your landing page [Listening to webhooks](#create-payment-event-handler) is required to make sure you always trigger fulfillment for every payment, but webhooks can sometimes be delayed. To optimize your payment flow and guarantee immediate fulfillment when your customer is present, trigger fulfillment from your landing page as well. Use the Checkout Session ID from the URL you specified in the previous step to do the following: 1. When your server receives a request for your Checkout landing page, extract the Checkout Session ID from the URL. 1. Run your `fulfill_checkout` function with the ID provided. 1. Render the page after the fulfillment attempt is complete. When you render your landing page you can display the following: - Details from the fulfillment process. - Links or information about services the customer now has access to. - Shipping or logistical details for physical goods. You can’t rely on triggering fulfillment only from your Checkout landing page, because your customers aren’t guaranteed to visit that page. For example, someone can pay successfully in Checkout and then lose their connection to the internet before your landing page loads. [Set up a webhook event handler](#create-payment-event-handler) so Stripe can send payment events directly to your server, bypassing the client entirely. Webhooks provide the most reliable way to confirm when you get paid. If webhook event delivery fails, Stripe [retries multiple times](https://docs.stripe.com/webhooks.md#automatic-retries). # Embedded components > This is a Embedded components for when payment-ui is embedded-components. View the original doc at https://docs.stripe.com/checkout/fulfillment?payment-ui=embedded-components. When you receive a payment with the Checkout Sessions API, you might need to take action to provide your customer with what they paid for. For example, you might need to grant them access to a service, or you might need to ship them physical goods. This process is known as fulfillment, and you have two ways to handle this process: - **Manually**: You can manually fulfill orders using information that Stripe makes available to you. For example, you can monitor the [Dashboard](https://docs.stripe.com/dashboard/basics.md), check payment notification emails, or look at reports and then fulfill orders. - **Automatically**: You can build an automated fulfillment system. The first option works for low volume or experimental ventures, but for most situations we recommend automating fulfillment. The rest of this guide shows you how to build an automatic fulfillment system. ## Automatic fulfillment The automatic fulfillment system outlined below uses a combination of *webhooks* and a redirect to your website to trigger fulfillment. You must use webhooks to make sure fulfillment happens for every payment, and redirects let your customers access services or fulfillment details immediately after paying. ## Create a fulfillment function Create a function on your server to fulfill successful payments. Webhooks trigger this function, and it’s called when customers are sent to your website after completing checkout. This guide refers to this function as `fulfill_checkout`, but you can name the function whatever you wish. Perform fulfillment only once per payment. Because of how this integration and the internet work, your `fulfill_checkout` function might be called multiple times, possibly concurrently, for the same Checkout Session. Performing checkout only once ensures this won’t cause undesired behavior. Your `fulfill_checkout` function must: 1. Correctly handle being called multiple times with the same Checkout Session ID. 1. Accept a *Checkout Session* ID as an argument. 1. Retrieve the Checkout Session from the API with the [line_items](https://docs.stripe.com/api/checkout/sessions/object.md#checkout_session_object-line_items) property [expanded](https://docs.stripe.com/api/expanding_objects.md). 1. Check the [payment_status](https://docs.stripe.com/api/checkout/sessions/object.md#checkout_session_object-payment_status) property to determine if it requires fulfillment. 1. Perform fulfillment of the line items. 1. Record fulfillment status for the provided Checkout Session. Use the code below as a starting point for your `fulfill_checkout` function. The `TODO` comments indicate any functionality you must implement. The code snippets below might name the `fulfill_checkout` function `fulfillCheckout` or `FulfillCheckout` depending on the language selected, but they all represent the same function. ```ruby def fulfill_checkout(session_id)<> puts "Fullfilling Checkout Session #{session_id}" # TODO: Make this function safe to run multiple times, # even concurrently, with the same session ID # TODO: Make sure fulfillment hasn't already been # performed for this Checkout Session # Retrieve the Checkout Session from the API with line_items expanded checkout_session = Stripe::Checkout::Session.retrieve({ id: session_id, expand: ['line_items'], }) # Check the Checkout Session's payment_status property # to determine if fulfillment should be performed if checkout_session.payment_status != 'unpaid' # TODO: Perform fulfillment of the line items # TODO: Record/save fulfillment status for this # Checkout Session end end ``` ```python <> def fulfill_checkout(session_id) print("Fulfilling Checkout Session", session_id) # TODO: Make this function safe to run multiple times, # even concurrently, with the same session ID # TODO: Make sure fulfillment hasn't already been # performed for this Checkout Session # Retrieve the Checkout Session from the API with line_items expanded checkout_session = stripe.checkout.Session.retrieve( session_id, expand=['line_items'], ) # Check the Checkout Session's payment_status property # to determine if fulfillment should be performed if checkout_session.payment_status != 'unpaid': # TODO: Perform fulfillment of the line items # TODO: Record/save fulfillment status for this # Checkout Session ``` ```php function fulfill_checkout($session_id) {<> // TODO: Log the string "Fulfilling Checkout Session $session_id" // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded $checkout_session = $stripe->checkout->sessions->retrieve($session_id, [ 'expand' => ['line_items'], ]); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if ($checkout_session->payment_status != 'unpaid') { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```java public void fulfillCheckout(String sessionId) {<> System.out.println("Fulfilling Checkout Session " + sessionId); // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded SessionRetrieveParams params = SessionRetrieveParams.builder() .addExpand("line_items") .build(); Session checkoutSession = Session.retrieve(sessionId, params, null); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (checkoutSession.getPaymentStatus() != 'unpaid') { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```node async function fulfillCheckout(sessionId) {<> console.log('Fulfilling Checkout Session ' + sessionId); // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded const checkoutSession = await stripe.checkout.sessions.retrieve(sessionId, { expand: ['line_items'], }); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (checkoutSession.payment_status !== 'unpaid') { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```go func FulfillCheckout(sessionId string) {<> fmt.Println("Fulfilling Checkout Session " + sessionId) // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded params := &stripe.CheckoutSessionParams{} params.AddExpand("line_items") cs, _ := session.Get(sessionId, params) // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (cs.PaymentStatus != stripe.CheckoutSessionPaymentStatusUnpaid) { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` ```dotnet public void FulfillCheckout(String sessionId) {<> Console.WriteLine("Fulfilling Checkout Session " + sessionId); // TODO: Make this function safe to run multiple times, // even concurrently, with the same session ID // TODO: Make sure fulfillment hasn't already been // performed for this Checkout Session // Retrieve the Checkout Session from the API with line_items expanded var options = new SessionGetOptions { Expand = new List { "line_items" }, }; var service = new SessionService(); var checkoutSession = service.Get(sessionId, options); // Check the Checkout Session's payment_status property // to determine if fulfillment should be performed if (checkoutSession.PaymentStatus != "unpaid") { // TODO: Perform fulfillment of the line items // TODO: Record/save fulfillment status for this // Checkout Session } } ``` If a Checkout Session has many line items, use [auto-pagination](https://docs.stripe.com/api/pagination/auto.md) with the [API for Checkout line items](https://docs.stripe.com/api/checkout/sessions/line_items.md) to retrieve all of them. Depending on the payment methods you accept and your business needs, you might want to have your `fulfill_checkout` function do the following: - Provision access to services. - Trigger shipment of goods. - Save a copy of the payment details and line items in your own database. - Send the customer a custom receipt email if you don’t have [Stripe’s receipts](https://docs.stripe.com/receipts.md) enabled. - Reconcile line items and quantities purchased if you allow customers to adjust quantities in Checkout. - Update inventory or stock records. ## Create a payment event handler To trigger fulfillment, create a webhook event handler to listen for payment events and trigger your `fulfill_checkout` function. When someone pays you, it creates a `checkout.session.completed` event. Set up an endpoint on your server to accept, process, and confirm receipt of these events. ### Immediate versus delayed payment methods Some payment methods aren’t [instant](https://docs.stripe.com/payments/payment-methods.md#payment-notification), such as [ACH direct debit](https://docs.stripe.com/payments/ach-direct-debit.md) and other bank transfers. This means, funds won’t be immediately available when Checkout completes. Delayed payment methods generate a [checkout.session.async_payment_succeeded](https://docs.stripe.com/api/events/types.md#event_types-checkout.session.async_payment_succeeded) event when payment succeeds later. The status of the object is in processing until the payment status either succeeds or fails. The webhook secret (`whsec_...`) shown in the code below comes from either the Stripe CLI or your webhook endpoint. You can use the Stripe CLI for local testing, and Stripe uses a webhook endpoint to send events to your handler when it’s running on a server. See the next section for more details. ```ruby require 'sinatra' # Use the secret provided by Stripe CLI for local testing # or your webhook endpoint's secret. endpoint_secret = 'whsec_...' post '/webhook' do event = nil # Verify webhook signature and extract the event # See https://stripe.com/docs/webhooks#verify-events for more information. begin sig_header = request.env['HTTP_STRIPE_SIGNATURE'] payload = request.body.read event = Stripe::Webhook.construct_event(payload, sig_header, endpoint_secret) rescue JSON::ParserError => e # Invalid payload return status 400 rescue Stripe::SignatureVerificationError => e # Invalid signature return status 400 end if event['type'] == 'checkout.session.completed' || event['type'] == 'checkout.session.async_payment_succeeded' fulfill_checkout(event['data']['object']['id']) end status 200 end ``` ```python \# Using Django from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt # Use the secret provided by Stripe CLI for local testing # or your webhook endpoint's secret. endpoint_secret = 'whsec_...' @csrf_exempt def my_webhook_view(request): payload = request.body sig_header = request.META['HTTP_STRIPE_SIGNATURE'] event = None try: event = stripe.Webhook.construct_event( payload, sig_header, endpoint_secret ) except ValueError as e: # Invalid payload return HttpResponse(status=400) except stripe.error.SignatureVerificationError as e: # Invalid signature return HttpResponse(status=400) if ( event['type'] == 'checkout.session.completed' or event['type'] == 'checkout.session.async_payment_succeeded' ): fulfill_checkout(event['data']['object']['id']) return HttpResponse(status=200) ``` ```php // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. $endpoint_secret = 'whsec_...'; $payload = @file_get_contents('php://input'); $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE']; $event = null; try { $event = \Stripe\Webhook::constructEvent( $payload, $sig_header, $endpoint_secret ); } catch(\UnexpectedValueException $e) { // Invalid payload http_response_code(400); exit(); } catch(\Stripe\Exception\SignatureVerificationException $e) { // Invalid signature http_response_code(400); exit(); } if ( $event->type == 'checkout.session.completed' || $event->type == 'checkout.session.async_payment_succeeded' ) { fulfill_checkout($event->data->object->id); } http_response_code(200); ``` ```java // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. String endpointSecret = "whsec_..."; // Using the Spark framework public Object handle(Request request, Response response) { String payload = request.body(); String sigHeader = request.headers("Stripe-Signature"); Event event = null; try { event = Webhook.constructEvent(payload, sigHeader, endpointSecret); } catch (JsonSyntaxException e) { // Invalid payload response.status(400); return ""; } catch (SignatureVerificationException e) { // Invalid signature response.status(400); return ""; } if ( "checkout.session.completed".equals(event.getType()) || "checkout.session.async_payment_succeeded".equals(event.getType()) ) { Session sessionEvent= (Session) event.getDataObjectDeserializer().getObject(); fulfillCheckout(sessionEvent.getId()); } response.status(200); return ""; } ``` ```javascript // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. const endpointSecret = 'whsec_...'; // Using Express const app = require('express')(); // Use body-parser to retrieve the raw body as a buffer const bodyParser = require('body-parser'); app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (request, response) => { const payload = request.body; const sig = request.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(payload, sig, endpointSecret); } catch (err) { return response.status(400).send(`Webhook Error: ${err.message}`); } if ( event.type === 'checkout.session.completed' || event.type === 'checkout.session.async_payment_succeeded' ) { fulfillCheckout(event.data.object.id); } response.status(200).end(); }); app.listen(4242, () => console.log('Running on port 4242')); ``` ```go func main() { http.HandleFunc("/webhook", handleWebhook) addr := "localhost:4242" log.Printf("Listening on %s", addr) log.Fatal(http.ListenAndServe(addr, nil)) } func handleWebhook(w http.ResponseWriter, req *http.Request) { const MaxBodyBytes = int64(65536) req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes) body, err := ioutil.ReadAll(req.Body) if err != nil { fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err) w.WriteHeader(http.StatusServiceUnavailable) return } // Pass the request body and Stripe-Signature header to ConstructEvent, along with the webhook signing key // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. endpointSecret := "whsec_..."; event, err := webhook.ConstructEvent(body, req.Header.Get("Stripe-Signature"), endpointSecret) if err != nil { fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err) w.WriteHeader(http.StatusBadRequest) // Return a 400 error on a bad signature return } if event.Type == stripe.EventTypeCheckoutSessionCompleted || event.Type == stripe.EventTypeCheckoutSessionAsyncPaymentSucceeded { var cs stripe.CheckoutSession err := json.Unmarshal(event.Data.Raw, &cs) if err != nil { fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) w.WriteHeader(http.StatusBadRequest) return } FulfillCheckout(cs.ID) } w.WriteHeader(http.StatusOK) } ``` ```dotnet namespace workspace.Controllers { [Route("webhook")] [ApiController] public class WebhookController : Controller { // Use the secret provided by Stripe CLI for local testing // or your webhook endpoint's secret. const string secret = "whsec_..."; [HttpPost] public async Task Index() { var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); try { var stripeEvent = EventUtility.ConstructEvent( json, Request.Headers["Stripe-Signature"], secret ); // If on SDK version < 46, use class Events instead of EventTypes if ( stripeEvent.Type == EventTypes.CheckoutSessionCompleted || stripeEvent.Type == EventTypes.CheckoutSessionAsyncPaymentSucceeded ) { var session = stripeEvent.Data.Object as Session; FulfillCheckout(session.Id); } return Ok(); } catch (StripeException) { return BadRequest(); } } } } ``` You might also want to listen for and handle `checkout.session.async_payment_failed` events. For example, you can send an email to your customer when a delayed payment fails. ## Test your event handler locally The quickest way to develop and test your webhook event handler is with the [Stripe CLI](https://docs.stripe.com/stripe-cli.md). If you don’t have the Stripe CLI, follow the [install guide](https://docs.stripe.com/stripe-cli.md#install) to get started. When the Stripe CLI is installed, you can test your event handler locally. Run your server (for example, on `localhost:4242`), then run the [stripe listen](https://docs.stripe.com/cli/listen) command to have the Stripe CLI forward events to your local server: ```bash stripe listen --forward-to localhost:4242/webhook Ready! Your webhook signing secret is 'whsec_' (^C to quit) ``` Add the webhook secret (`whsec_...`) to your event handling code, then test fulfillment by going through your checkout as a customer. When the payment completes, verify the following: - On your command line, where `stripe listen` is running, it shows a `checkout.session.completed` event forwarded to your local server. - Your server logs show the expected output from your `fulfill_checkout` function. ## Create a webhook endpoint After testing locally, get your webhook event handler up and running on your server. Next, [create a webhook endpoint](https://docs.stripe.com/webhooks.md#register-webhook) to send `checkout.session.completed` events to your server, then test the checkout flow again. ## Trigger fulfillment on your landing page [Listening to webhooks](#create-payment-event-handler) is required to make sure you always trigger fulfillment for every payment, but webhooks can sometimes be delayed. To optimize your payment flow and guarantee immediate fulfillment when your customer is present, trigger fulfillment from your landing page as well. You can can configure the landing page by passing [return_url](https://docs.stripe.com/api/checkout/sessions/create.md#create_checkout_session-return_url) when creating the Checkout Session, or passing [returnUrl](https://docs.stripe.com/js/custom_checkout/confirm#custom_checkout_session_confirm-options-returnUrl) to `confirm` on the front end. Use the Checkout Session ID from the URL you specified to do the following: 1. When your server receives a request for your checkout landing page, extract the Checkout Session ID from the URL. 1. Run your `fulfill_checkout` function with the ID provided. 1. Render the page after the fulfillment attempt is complete. When you render your landing page you can display the following: - Details from the fulfillment process. - Links or information about services the customer now has access to. - Shipping or logistical details for physical goods. You can’t rely on triggering fulfillment only from your checkout landing page, because your customers aren’t guaranteed to visit that page. For example, someone can pay successfully and then lose their connection to the internet before your landing page loads. [Set up a webhook event handler](#create-payment-event-handler) so Stripe can send payment events directly to your server, bypassing the client entirely. Webhooks provide the most reliable way to confirm when you get paid. If webhook event delivery fails, Stripe [retries multiple times](https://docs.stripe.com/webhooks.md#automatic-retries).