Bacs Direct Debit payments

Learn how to use Checkout to accept Bacs Direct Debit payments.

Stripe users in the UK can use Checkout in payment mode to accept Bacs Direct Debit payments.

A Checkout Session represents the details of your customer’s intent to purchase. You create a Session when your customer wants to pay for something. After redirecting your customer to a Checkout Session, Stripe presents a payment form where your customer can complete their purchase. Once your customer has completed a purchase, they are redirected back to your site.

1 Set up Stripe Server-side

First, you need a Stripe account. Register now.

Use our official libraries for access to the Stripe 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'
# Install through pip pip install --upgrade stripe
# Or find the Stripe package on http://pypi.python.org/pypi/stripe/
# Find the version you want to pin: # https://github.com/stripe/stripe-python/blob/master/CHANGELOG.md # Specify that version in your requirements.txt file stripe>=2.48.0,<3.0
# Install the PHP library via Composer composer require stripe/stripe-php
# Or download the source directly: https://github.com/stripe/stripe-php/releases
/* For Gradle, add the following dependency to your build.gradle and replace {VERSION} with the version number you want to use from - https://mvnrepository.com/artifact/com.stripe/stripe-java or - https://github.com/stripe/stripe-java/releases/latest */ implementation "com.stripe:stripe-java:{VERSION}"
<!-- For Maven, add the following dependency to your POM and replace {VERSION} with the version number you want to use from - https://mvnrepository.com/artifact/com.stripe/stripe-java or - https://github.com/stripe/stripe-java/releases/latest --> <dependency> <groupId>com.stripe</groupId> <artifactId>stripe-java</artifactId> <version>{VERSION}</version> </dependency>
# For other environments, manually install the following JARs: # - The Stripe JAR from https://github.com/stripe/stripe-java/releases/latest # - Google Gson from https://github.com/google/gson
# Install via npm npm install --save stripe
# Install stripe-go go get -u github.com/stripe/stripe-go
// Then import the package import ( "github.com/stripe/stripe-go/v71" )
# Install via dotnet dotnet add package Stripe.net dotnet restore
# Or install via NuGet PM> Install-Package Stripe.net

2 Create products and prices

To use Checkout, you first need to create a Product and a Price. Different physical goods or levels of service should be represented by products. Each product’s pricing is represented by one or more prices.

For example, you can create a T-shirt product that has 2 prices for different currencies: £20 GBP and €25 Euro. This allows you to change and add prices without needing to change the details of your underlying products. You can either create a product and price through the API or through the Stripe Dashboard.

If your price is determined at checkout (e.g. the customer sets a donation amount) or you prefer not to create prices upfront, you may also create ad-hoc prices at Checkout Session creation using an existing product.

Make sure you are in test mode by toggling the View test data button at the bottom of the Stripe Dashboard. Next, define the items you want to sell. To create a new product and price:

  • Navigate to the Products section in the Dashboard
  • Click Add product
  • Select One time when setting the price

The product name, description, and image that you supply are displayed to customers in Checkout.

To create a Product with the API, only a name is required. The product name, description, and images that you supply are displayed to customers on Checkout.

curl https://api.stripe.com/v1/products \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d name=T-shirt
product = Stripe::Product.create( { name: 'T-shirt', } )
product = stripe.Product.create( name='T-shirt', )
$product = \Stripe\Product::create([ 'name' => 'T-shirt', ]);
ProductCreateParams params = ProductCreateParams.builder() .setName("T-shirt") .build(); Product product = Product.create(params);
const product = await stripe.products.create({ name: 'T-shirt', });
params := &stripe.ProductParams{ Name: stripe.String("T-shirt"), } p, _ := product.New(params)
var productService = new ProductService(); var options = new ProductCreateOptions { Name = "T-shirt", }; var product = productService.Create(options);

Next, create a Price to define how much to charge for your product. This includes how much the product costs and what currency to use.

curl https://api.stripe.com/v1/prices \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d product="{{PRODUCT_ID}}" \ -d unit_amount=2000 \ -d currency=gbp
price = Stripe::Price.create( { product: '{{PRODUCT_ID}}', unit_amount: 2000, currency: 'gbp', } )
price = stripe.Price.create( product='{{PRODUCT_ID}}', unit_amount=2000, currency='gbp', )
$price = \Stripe\Price::create([ 'product' => '{{PRODUCT_ID}}', 'unit_amount' => 2000, 'currency' => 'gbp', ]);
PriceCreateParams params = PriceCreateParams.builder() .setProduct("{{PRODUCT_ID}}") .setUnitAmount(2000) .setCurrency("gbp") .build(); Price price = Price.create(params);
const price = await stripe.prices.create({ product: '{{PRODUCT_ID}}', unit_amount: 2000, currency: 'gbp', });
params := &stripe.PriceParams{ Product: stripe.String("{{PRODUCT_ID}}"), UnitAmount: stripe.Int64(2000), Currency: stripe.String("gbp"), } p, _ := price.New(params)
var priceService = new PriceService(); var options = new PriceCreateOptions { Product = "{{PRODUCT_ID}}", UnitAmount = 2000, Currency = "gbp", }; var price = priceService.Create(options);

3 Create a Checkout Session Server-side

Create a Session with line_items. Line items represent a list of items the customer is purchasing.

When your customer successfully completes their payment, they are redirected to the success_url, a page on your website that informs the customer that their payment details have been successfully collected and their payment is being processed.

When your customer clicks on your logo in a Checkout Session without completing a payment, Checkout redirects them back to your website by navigating to the cancel_url. Typically, this is the page on your website that the customer viewed prior to redirecting to Checkout.

Checkout can accept a payment and save the payment method for future use. Payment methods saved this way can be used for future payments using a PaymentIntent.

curl https://api.stripe.com/v1/checkout/sessions \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d "payment_method_types[]"=bacs_debit \ -d "line_items[][price]"="{{PRICE_ID}}" \ -d "line_items[][quantity]"=1 \ -d mode=payment \ -d customer="{{CUSTOMER_ID}}" \ -d "payment_intent_data[setup_future_usage]"=off_session \ -d success_url="https://example.com/success?session_id={CHECKOUT_SESSION_ID}" \ -d cancel_url="https://example.com/cancel"
# 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_4eC39HqLyjWDarjtT1zdp7dc' session = Stripe::Checkout::Session.create({ payment_method_types: ['bacs_debit'], line_items: [{ price: '{{PRICE_ID}}', quantity: 1, }], mode: 'payment', customer: customer.id, payment_intent_data: { setup_future_usage: 'off_session', }, success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}', cancel_url: 'https://example.com/cancel', })
# 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_4eC39HqLyjWDarjtT1zdp7dc' session = stripe.checkout.Session.create( payment_method_types=['bacs_debit'], line_items=[{ 'price': '{{PRICE_ID}}', 'quantity': 1, }], mode='payment', customer=customer.id, payment_intent_data={ 'setup_future_usage': 'off_session', }, success_url='https://example.com/success?session_id={CHECKOUT_SESSION_ID}', cancel_url='https://example.com/cancel', )
// 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\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); $session = \Stripe\Checkout\Session::create([ 'payment_method_types' => ['bacs_debit'], 'line_items' => [[ 'price' => '{{PRICE_ID}}', 'quantity' => 1, ]], 'mode' => 'payment', 'customer' => $customer->id, 'payment_intent_data' => [ 'setup_future_usage' => 'off_session', ], 'success_url' => 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}', 'cancel_url' => 'https://example.com/cancel', ]);
// 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.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; SessionCreateParams params = SessionCreateParams.builder() .addPaymentMethodType(SessionCreateParams.PaymentMethodType.BACS_DEBIT) .addLineItem( SessionCreateParams.LineItem.builder() .setPrice("{{PRICE_ID}}") .setQuantity(1L) .build()) .setMode("payment") .setCustomer(customer.getId()) .setPaymentIntentData( SessionCreateParams.PaymentIntentData.builder() .setSetupFutureUsage(SessionCreateParams.PaymentIntentData.SetupFutureUsage.OFF_SESSION) .build()) .setSuccessUrl("https://example.com/success?session_id={CHECKOUT_SESSION_ID}") .setCancelUrl("https://example.com/cancel") .build(); Session session = Session.create(params);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); const session = await stripe.checkout.sessions.create({ payment_method_types: ['bacs_debit'], line_items: [{ price: '{{PRICE_ID}}', quantity: 1, }], mode: 'payment', customer: customer.id, payment_intent_data: { setup_future_usage: 'off_session', }, success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}', cancel_url: 'https://example.com/cancel', });
// 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.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" params := &stripe.CheckoutSessionParams{ PaymentMethodTypes: stripe.StringSlice([]string{ "bacs_debit", }), LineItems: []*stripe.CheckoutSessionLineItemParams{ &stripe.CheckoutSessionLineItemParams{ Price: stripe.String("{{PRICE_ID}}"), Quantity: stripe.Int64(1), }, }, Mode: stripe.String("payment"), Customer: stripe.String(c.ID), PaymentIntentData: &stripe.CheckoutSessionPaymentIntentDataParams{ SetupFutureUsage: stripe.String("off_session"), }, SuccessURL: stripe.String("https://example.com/success?session_id={CHECKOUT_SESSION_ID}"), CancelURL: stripe.String("https://example.com/cancel"), } session, err := session.New(params)
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; var options = new SessionCreateOptions { PaymentMethodTypes = new List<string> { "bacs_debit", }, LineItems = new List<SessionLineItemOptions> { new SessionLineItemOptions { Price = "{{PRICE_ID}}", Quantity = 1, }, }, Mode = "payment", Customer = customer.Id, PaymentIntentData = new SessionPaymentIntentDataOptions { SetupFutureUsage = "off_session", }, SuccessUrl = "https://example.com/success?session_id={CHECKOUT_SESSION_ID}", CancelUrl = "https://example.com/cancel", }; var service = new SessionService(); Session session = service.Create(options);

Creating a Checkout Session returns a Session ID which is important for the following steps. Make the Session ID available on your success page by including the {CHECKOUT_SESSION_ID} template variable in the success_url as in the above example.

4 Redirect the customer to collect payment Client-side

To use Checkout on your website, you must add a snippet of code that includes the id field from the session that you created in the previous step.

Checkout relies on Stripe.js. To get started, include the following script tag on your website—always load it directly from https://js.stripe.com:

<script src="https://js.stripe.com/v3/"></script>
npm install @stripe/stripe-js

Next, create an instance of the Stripe object by providing your publishable API key as the first parameter:

var stripe = Stripe('pk_test_TYooMQauvdEDq54NiTphI7jx');
import {loadStripe} from '@stripe/stripe-js'; const stripe = await loadStripe('pk_test_TYooMQauvdEDq54NiTphI7jx');

When your customer is ready to save or update their payment method, call redirectToCheckout to begin the setup process. This is where you provide the id from session creation as a parameter to Checkout.

var checkoutButton = document.getElementById('checkout-button'); checkoutButton.addEventListener('click', function() { stripe.redirectToCheckout({ // Make the id field from the Checkout Session creation API response // available to this file, so you can provide it as argument here // instead of the {{CHECKOUT_SESSION_ID}} placeholder. sessionId: '{{CHECKOUT_SESSION_ID}}' }).then(function (result) { // If `redirectToCheckout` fails due to a browser or network // error, display the localized error message to your customer // using `result.error.message`. }); });
const checkoutButton = document.getElementById('checkout-button'); checkoutButton.addEventListener('click', () => { stripe.redirectToCheckout({ // Make the id field from the Checkout Session creation API response // available to this file, so you can provide it as argument here // instead of the {{CHECKOUT_SESSION_ID}} placeholder. sessionId: '{{CHECKOUT_SESSION_ID}}' }) // If `redirectToCheckout` fails due to a browser or network // error, display the localized error message to your customer // using `error.message`. });

This code is typically invoked from an event handler that triggers in response to an action taken by your customer, such as clicking on a setup button.

5 Handle post-payment events Server-side

When your customer completes a payment, Stripe redirects them to the URL that you specified in the success_url parameter. Typically, this is a page on your website that informs your customer that their payment was successful.

However, Bacs Direct Debit is a delayed notification payment method, which means that funds are not immediately available. A Bacs Direct Debit payment typically takes 3 business days to make the funds available. Because of this, you’ll want to delay order fulfillment until the funds are available. Once the payment succeeds, the underlying PaymentIntent status changes from processing to succeeded.

The following Checkout events are sent when the payment status changes:

Event Name Description Next steps
checkout.session.completed The customer has successfully authorized the debit payment by submitting the Checkout form. Wait for the payment to succeed or fail.
checkout.session.async_payment_succeeded The customer’s payment succeeded. Fulfill the goods or services that the customer purchased.
checkout.session.async_payment_failed The customer’s payment was declined, or failed for some other reason. Contact the customer via email and request that they place a new order.

Your webhook code will need to handle all 3 of these Checkout events.

Each Checkout webhook payload includes the Checkout Session object, which contains information about the Customer and PaymentIntent.

The checkout.session.completed webhook is sent to your server before your customer is redirected. Your webhook acknowledgement (any 2xx status code) triggers the customer’s redirect to the success_url. If Stripe doesn’t receive successful acknowledgement within 10 seconds of a successful payment, your customer is automatically redirected to the success_url page.

On your success_url page, you’ll want to show a success message to your customer, and let them know that fulfillment of the order will take a few days as the Bacs Direct Debit payment method is not instant.

When accepting instant payments (such as credit cards) in addition to delayed notification payments, you’ll need to update your webhook endpoint to handle both kinds of payments when receiving a checkout.session.completed event.

# 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_4eC39HqLyjWDarjtT1zdp7dc' # You can find your endpoint's secret in your webhook settings endpoint_secret = 'whsec_...' # Using Sinatra post '/webhook' do payload = request.body.read event = nil # Verify webhook signature and extract the event # See https://stripe.com/docs/webhooks/signatures for more information. sig_header = request.env['HTTP_STRIPE_SIGNATURE'] begin event = Stripe::Webhook.construct_event( payload, sig_header, endpoint_secret ) rescue JSON::ParserError => e # Invalid payload status 400 return rescue Stripe::SignatureVerificationError => e # Invalid signature status 400 return end case event['type'] when 'checkout.session.completed' session = event['data']['object'] # Check if the order is paid (e.g. from a card payment) payment_intent = Stripe::PaymentIntent.retrieve(session.payment_intent) # A delayed notification payment will have the status 'processing' order_paid = payment_intent.status == "succeeded" # Save an order in your database, marked as 'awaiting payment' create_order(session) if order_paid fulfill_order(session) end when 'checkout.session.async_payment_succeeded' session = event['data']['object'] # Fulfill the purchase... fulfill_order(session) when 'checkout.session.async_payment_failed' session = event['data']['object'] # Send an email to the customer asking them to retry their order email_customer_about_failed_payment(session) end status 200 end
# 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_4eC39HqLyjWDarjtT1zdp7dc' # Using Django from django.http import HttpResponse # You can find your endpoint's secret in your webhook settings 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) # Handle the checkout.session.completed event if event['type'] == 'checkout.session.completed': session = event['data']['object'] # Check if the order is paid (e.g. from a card payment) payment_intent = stripe.PaymentIntent.retrieve(session.payment_intent) order_paid = payment_intent.status == "succeeded" # Save an order in your database, marked as 'awaiting payment' create_order(session) if order_paid: # Fulfill the purchase fulfill_order(session) elif event['type'] == 'checkout.session.async_payment_succeeded': session = event['data']['object'] # Fulfill the purchase fulfill_order(session) elif event['type'] == 'checkout.session.async_payment_failed': session = event['data']['object'] # Send an email to the customer asking them to retry their order email_customer_about_failed_payment(session) return HttpResponse(status=200)
// 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\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); // You can find your endpoint's secret in your webhook settings $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(); } switch ($event->type) { case 'checkout.session.completed': $session = $event->data->object; // Check if the order is paid (e.g. from a card payment) $payment_intent = \Stripe\PaymentIntent::retrieve($session->payment_intent) $order_paid = $payment_intent->status == 'succeeded' // Save an order in your database, marked as 'awaiting payment' create_order($session); if ($order_paid) { // Fulfill the purchase fulfill_order($session); } break; case 'checkout.session.async_payment_succeeded': $session = $event->data->object; // Fulfill the purchase fulfill_order($session); break; case 'checkout.session.async_payment_failed': $session = $event->data->object; // Send an email to the customer asking them to retry their order email_customer_about_failed_payment($session); break; } http_response_code(200);
// 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.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; // You can find your endpoint's secret in your webhook settings String endpointSecret = "whsec_..."; // Using the Spark framework (http://sparkjava.com) 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 ""; } switch(event.getType()) { case "checkout.session.completed": Session session = (Session) event.getDataObjectDeserializer().getObject(); // Check if the order is paid (e.g. from a card payment) PaymentIntent paymentIntent = PaymentIntent.retrieve(session.getPaymentIntent()); boolean orderPaid = paymentIntent.getStatus() == PaymentIntent.Status.Succeeded; // Save an order in your database, marked as 'awaiting payment' createOrder(session); if (orderPaid) { // Fulfill the purchase fulfillOrder(session); } break; case "checkout.session.async_payment_succeeded": Session session = (Session) event.getDataObjectDeserializer().getObject(); // Fulfill the purchase fulfillOrder(session); break; case "checkout.session.async_payment_failed": Session session = (Session) event.getDataObjectDeserializer().getObject(); // Send an email to the customer asking them to retry their order emailCustomerAboutFailedPayment(session); break; } response.status(200); return ""; }
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); // Find your endpoint's secret in your Dashboard's webhook settings 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'); // Match the raw body to content type application/json app.post('/webhook', bodyParser.raw({type: 'application/json'}), (request, response) => { const sig = request.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(request.rawBody, sig, endpointSecret); } catch (err) { return response.status(400).send(`Webhook Error: ${err.message}`); } switch (event.type) { case 'checkout.session.completed': { const session = event.data.object; // Check if the order is paid (e.g. from a card payment) stripe.paymentIntents.retrieve( session.payment_intent, (err, paymentIntent) => { if (err) { return console.error(err); } // A delayed notification payment will have the status 'processing' const orderPaid = paymentIntent.status === 'succeeded'; // Save an order in your database, marked as 'awaiting payment' createOrder(session); if (orderPaid) { fulfillOrder(session); } } ); } case 'checkout.session.async_payment_succeeded': { const session = event.data.object; // Fulfill the purchase... fulfillOrder(session); } case 'checkout.session.async_payment_failed': { const session = event.data.object; // Send an email to the customer asking them to retry their order emailCustomerAboutFailedPayment(session); } } // Return a response to acknowledge receipt of the event response.json({received: true}); }); app.listen(8000, () => console.log('Running on port 8000'));
// 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.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" http.HandleFunc("/webhook", func(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 & Stripe-Signature header to ConstructEvent, along with the webhook signing key // You can find your endpoint's secret in your webhook settings 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 } switch event.Type { case "checkout.session.completed": var session stripe.CheckoutSession err := json.Unmarshal(event.Data.Raw, &session) if err != nil { fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) w.WriteHeader(http.StatusBadRequest) return } // Check if the order is paid (e.g. from a card payment) intent, err := paymentintent.Get(session.PaymentIntent.ID, nil) if err != nil { fmt.Fprintf(os.Stderr, "Error retrieving PaymentIntent: %v\n", err) w.WriteHeader(http.StatusInternalServerError) return } orderPaid := intent.Status == stripe.PaymentIntentStatusSucceeded // Save an order in your database, marked as 'awaiting payment' createOrder(session) if orderPaid { // Fulfill the purchase fulfillorder(session) } case "checkout.session.async_payment_succeeded": var session stripe.CheckoutSession err := json.Unmarshal(event.Data.Raw, &session) if err != nil { fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) w.WriteHeader(http.StatusBadRequest) return } // Fulfill the purchase fulfillOrder(session) case "checkout.session.async_payment_failed": var session stripe.CheckoutSession err := json.Unmarshal(event.Data.Raw, &session) if err != nil { fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) w.WriteHeader(http.StatusBadRequest) return } // Send an email to the customer asking them to retry their order emailCustomerAboutFailedPayment(session) } w.WriteHeader(http.StatusOK) })
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; using System; using System.IO; using Microsoft.AspNetCore.Mvc; using Stripe; namespace workspace.Controllers { [Route("api/[controller]")] public class StripeWebHook : Controller { // You can find your endpoint's secret in your webhook settings const string secret = "whsec_..."; [HttpPost] public async Task<IActionResult> Index() { var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); try { var stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], secret); Checkout.Session session = null; switch (stripeEvent.Type) { case Events.CheckoutSessionCompleted: session = stripeEvent.Data.Object as Checkout.Session; // Check if the order is paid (e.g. from a card payment) var service = new PaymentIntentService(); var paymentIntent = service.Get(session.PaymentIntentId); var orderPaid = paymentIntent.Status == "succeeded" // Save an order in your database, marked as 'awaiting payment' CreateOrder(session); if (orderPaid) { // Fulfill the purchase FulfillOrder(session); } break; case Events.CheckoutSessionAsyncPaymentSucceeded: session = stripeEvent.Data.Object as Checkout.Session; // Fulfill the purchase FulfillOrder(session); break; case Events.CheckoutSessionAsyncPaymentFailed: session = stripeEvent.Data.Object as Checkout.Session; // Send an email to the customer asking them to retry their order EmailCustomerAboutFailedPayment(session); break; } } catch (StripeException e) { return BadRequest(); } return Ok(); } } }

You can get information about the customer and payment by retrieving the Customer or PaymentIntent objects referenced by the customer, payment_intent properties in the webhook payload.

Testing webhooks locally

To test webhooks locally, you can use Stripe CLI. Once you have it installed, you can forward events to your server:

stripe listen --forward-to localhost:4242/webhook Ready! Your webhook signing secret is '{{WEBHOOK_SIGNING_SECRET}}' (^C to quit)

Learn more about setting up webhooks.

6 Test the integration

By this point you should have a basic Bacs Direct Debit integration that collects bank account details and accepts a payment.

There are several test bank account numbers you can use in test mode to make sure this integration is ready.

Sort code Account number Description
10-88-00 00012345 The payment succeeds and the PaymentIntent transitions from processing to succeeded.
10-88-00 90012345 The payment succeeds after three minutes and the PaymentIntent transitions from processing to succeeded.
10-88-00 33333335 The payment fails with a debit_not_authorized failure code and the PaymentIntent transitions from processing to requires_payment_method. The Mandate becomes inactive and the PaymentMethod can not be used again.
10-88-00 93333335 The payment fails after three minutes with a debit_not_authorized failure code and the PaymentIntent transitions from processing to requires_payment_method. The Mandate becomes inactive and the PaymentMethod can not be used again.
10-88-00 22222227 The payment fails with an insufficient_funds failure code and the PaymentIntent transitions from processing to requires_payment_method. The Mandate remains active and the PaymentMethod can be used again.
10-88-00 92222227 The payment fails after three minutes with an insufficient_funds failure code and the PaymentIntent transitions from processing to requires_payment_method. The Mandate remains active and the PaymentMethod can be used again.
10-88-00 00033333 Payment detail collection succeeds, but the Mandate transitions immediately to inactive.
10-88-00 00044444 The request to set up Bacs Direct Debit fails immediately due to an invalid account number and the customer is prompted to update their information before submitting. Payment details are not collected.

You can test using any of the account numbers provided above. However, as Bacs Direct Debit payments take several days to process, you should use the test account numbers that operate on a three-minute delay to better simulate the behavior of live payments.

Payment failures

Payments can fail for a variety of reasons. The reason for a failure is available through charge.failure_code. Only payments with certain failure codes may be retried. If a payment cannot be retried, we recommend reaching out to the customer and asking them to pay again using a different bank account or a different payment method.

Below is a list of failure codes we currently send for Bacs Direct Debit. We may add more at any time, so in developing and maintaining your code, you should not assume that only these types exist.

Failure code Description Retryable
account_closed The bank account has been closed. No
bank_ownership_changed The account has been transferred to a new Payment Service Provider (PSP). Check if you have been notified of the new PSP’s details. If not, you must collect a new mandate from the customer. No
debit_not_authorized The customer has notified their bank that this payment was unauthorized or there is no mandate held by the paying bank. No
generic_could_not_process This payment could not be processed. Yes
insufficient_funds The customer’s account has insufficient funds to cover this payment. Yes
invalid_account_number The account number is not valid. This could mean it is not for a GBP account or that the account cannot process Direct Debit payments. No

To retry a payment, confirm the PaymentIntent again using the same PaymentMethod.

To ensure success, we recommend reaching out to the payer before retrying a payment.

See also

Was this page helpful?
Questions? Contact us.
Developer tutorials on YouTube.
You can unsubscribe at any time. Read our privacy policy.