Since the launch of Stripe as a payment system, the likelihood that you have used it to make a payment is high. Have you ever wondered how you can integrate Stripe into your own application? If the answer is yes, don’t worry, we’ve got you covered. In this tutorial, you will learn how to integrate Stripe in Spring Boot using Thymeleaf.
If you have searched the internet for this topic, you might have come across an article I wrote on the same topic. If you have read the article, you already know it has some missing information. As a result, this tutorial will be an updated version that provides every step on the way.
Prerequisites
- Knowledge of Java programming language
- Java development kit(JDK8) and above
- Stripe account – Create a Stripe account to get your own public and secret keys. You will use the keys in your application.
- IntelliJ IDEA community edition – Code editor for the Java programming language.
Create a new project
To create a new project, go to Spring initializr and provide the project details. Select Maven in the Project section, Java in the Language section, and 3.1.x in the Spring Boot section.
In the Project Metadata section enter the respective sections as shown below.
- Group – com.javawhizz
- Artifact – stripePayment
- Name – stripePayment, this section gets filled for you.
- Description – Integrate Stripe in Spring Boot.
- Package name – com.javawhizz.stripePayment, this section gets filled for you.
- Packaging – Jar
- Java – 17
To add the project dependencies, press the ADD DEPENDENCIES button in the Dependencies section. Add the dependencies as provided below.
- Spring Web – Create web applications by providing core web services.
- Lombok – Reduce boilerplate code by generating helper methods.
- Thymeleaf – Templating engine and acts as the view for the application.
- Validation – Add constraints to fields and validates them during form submission.
The following image shows the dependencies and the project structure of the application.

Click on the GENERATE button to download a zipped file of your project. After downloading the project, unzip it and import it into IntelliJ. Wait for a few seconds so that IntelliJ can download the Maven dependencies.
You also need to add the Stripe dependency to the project. Open the file named pom.xml and copy and paste the following XML into the file.
<dependency> <groupId>com.stripe</groupId> <artifactId>stripe-java</artifactId> <version>22.0.0</version> </dependency>
Press CTRL+SHIFT+O to load the Maven changes. This will download the Stripe API and add it to the project’s classpath.
Create application sub-packages
In IntelliJ, create the following sub-packages under src/main/java/com/javawhizz/stripePayment.
- config
- controller
- model
Add public and secret keys
If you did not keep a copy of the keys, go to the Stripe dashboard to get a copy. To add the keys, open the application.properties file and them as shown below.
stripe.api.publicKey=YOUR_PUBLIC_KEY stripe.api.secretKey=YOUR_SECRET_KEY
Load the secret key
Create a file named Config.java in the config package. Copy and paste the following code into the file.
package com.javawhizz.stripePayment.config; import com.stripe.Stripe; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @Configuration public class Config { @Value("${stripe.api.secretKey}") private String secretKey; @PostConstruct public void initSecretKey(){ Stripe.apiKey = secretKey; } }
To begin with, add the @Configuration
annotation to the class. This tells Spring that the class will contain @Bean
definitions.
Although you have no @Bean
definitions in the class, you can use this class to initialize the secret key. Use the @PostConstruct
annotation to initialize the secret key.
Since the @PostConstruct
annotation is usable in any class that supports dependency injection. This is the reason behind using it in the configuration class.
To inject the secret key from the properties file to a String variable, use the @Value
annotation. Next, call Stripe.apiKey
in the initSecretKey()
method and assign it to the String variable.
With this in place, the secret key gets loaded only once during the lifetime of the application.
Create request DTO
Create a file named Request.java under the model package. Copy and paste the following code into the file.
package com.javawhizz.stripePayment.model; import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Request { @NotNull @Min(4) private Long amount; @Email private String email; @NotBlank @Size(min = 5, max = 200) private String productName; }
To make a payment using Stripe, you need to have a product or service that is being charged. The Request
class in this case represents our product.
The product contains the amount, email, and product name. To begin with, add the @Getter
and @Setter
annotations to the class to generate the helper methods.
Additionally, you also need to add the @NoArgsConstructor
and @AllArgsContructor
. As a result, the first annotation will help you create an empty instance of the request object out of the box. The latter will help you create an instance that expects all the fields as arguments.
Next, add annotations to the fields for validation during form submission. Add @NotNull
and @Min
in the amount field, @Email
in the email field, and @NotBlank
and @Size
in the product name field.
- @NotNull – The field must not be null.
- @Min – The value of the field must be higher or equal to the specified value.
- @Email – Correct format of the email entered into the field.
- @NotBlank – The value must not be null and has no whitespace.
- @Size – The length of the value must be between the specified numbers.
Create response DTO
Create a file named Response.java in the model package. Copy and paste the following code into the file.
package com.javawhizz.stripePayment.model; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Response { private String intentID; private String clientSecret; }
Once the payment is successful Stripe returns a response containing the payment details. In this case, retrieve only the payment intent ID and client secret.
The class Response
will hold these properties. As you can see in the class, it only contains two fields named intent ID and client secret. The class also contains the annotations to generate helper methods and create objects.
Create payment intent controller
Create a file named PaymentIntentController.java in the controller package. Copy and paste the following code into the file.
package com.javawhizz.stripePayment.controller; import com.javawhizz.stripePayment.model.Request; import com.javawhizz.stripePayment.model.Response; import com.stripe.exception.StripeException; import com.stripe.model.PaymentIntent; import com.stripe.param.PaymentIntentCreateParams; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class PaymentIntentController { @PostMapping("/create-payment-intent") public Response createPaymentIntent(@RequestBody Request request) throws StripeException { PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() .setAmount(request.getAmount() * 100L) .putMetadata("productName", request.getProductName()) .setCurrency("usd") .setAutomaticPaymentMethods( PaymentIntentCreateParams .AutomaticPaymentMethods .builder() .setEnabled(true) .build() ) .build(); PaymentIntent intent = PaymentIntent.create(params); return new Response(intent.getId(), intent.getClientSecret()); } }
When you submit the products page, this controller gets called immediately by JavaScript. Don’t worry about the JavaScript code, you will write the code later. This create payment intent method gets called to create and returns a payment intent.
The payment intent helps you through the process of charging the customer. As a result, it will track the status of the payment from the payment method to a successful transaction.
Note that the method createPaymentIntent()
expects the Request as the argument. The amount and product name from the request gets passed to PaymentIntentCreateParams
. To set the currency, you should also pass the String USD.
Note that the email field is not part of the parameters. As a result, JavaScript loads the payment form already pre-filled with the email on the products page.
The role of the PaymentIntentCreateParams
is to define the parameters for the payment. To create a payment intent, Call the static method named create()
of PaymentIntent
. Next, pass the reference of the params object as the argument of the method.
Since the method returns the response DTO, return an instance of the class. Finally, pass the arguments intent ID and client secret as the arguments of the method. The payment intent created by Stripe contains these values.
Create a controller for the application
Create a file named AppController.java in the controller package. Copy and paste the following code into the file.
package com.javawhizz.stripePayment.controller; import com.javawhizz.stripePayment.model.Request; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; @Controller public class AppController { @Value("${stripe.api.publicKey}") private String publicKey; @GetMapping("/") public String home(Model model){ model.addAttribute("request", new Request()); return "index"; } @PostMapping("/") public String showCard(@ModelAttribute @Valid Request request, BindingResult bindingResult, Model model){ if (bindingResult.hasErrors()){ return "index"; } model.addAttribute("publicKey", publicKey); model.addAttribute("amount", request.getAmount()); model.addAttribute("email", request.getEmail()); model.addAttribute("productName", request.getProductName()); return "checkout"; } }
When you first access the application, it returns the product details page. The controller uses the home()
method to return the products page. You will create the page later.
The hasErrors()
method ensures validation of the fields before submitting the form. If there are no errors, the controller returns the checkout page. The showCard()
method is in charge of this functionality. Likewise, you will create the page later.
For the payment to work, you should delegate the public key and product details to JavaScript. This is because the JavaScript code is in charge of making a request to the server to create a payment intent.
To achieve this, add the data to the page as model attributes. As a result, we will later retrieve the data from the page using Thymeleaf expressions. Once you retrieve the data, you can use it in the JavaScript code.
Create the product details page
Create a file named index.html under the package src/main/resources/templates. Copy and paste the following code into the file.
<!doctype html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap demo</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> </head> <body> <div class="container"> <div class="row"> <div class="col-md-12"> <form th:action="@{/}" th:object="${request}" method="post"> <h2>Enter product details</h2> <div class="mb-3"> <label for="email" class="form-label">Email address</label> <input type="email" th:field="*{email}" required class="form-control" id="email" aria-describedby="emailHelp"> <div id="emailHelp" class="form-text"> We'll never share your email with anyone else. </div> <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" style="color: red"/> </div> <div class="mb-3"> <label for="amount" class="form-label">Amount</label> <input type="number" required th:field="*{amount}" class="form-control" id="amount"> <p th:if="${#fields.hasErrors('amount')}" th:errors="*{amount}" style="color: red"/> </div> <div class="mb-3"> <label for="productName" class="form-label" >Product Name</label> <input type="text" required th:field="*{productName}" class="form-control" id="productName"> <p th:if="${#fields.hasErrors('productName')}" th:errors="*{productName}" style="color: red"/> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script> </body> </html>
This is the first page that gets loaded to enter the product details. The product details include the amount, email, and product name.
All the fields in the form use the hasErrors()
method to show any errors when submitting the form. The data in this form get passed to the showCard()
method where they are then added to the checkout page.
Create the checkout page
Create a file named checkout.html under the package src/main/resources/templates. Copy and paste the following code into the file.
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8" /> <title>Accept a payment</title> <meta name="description" content="A demo of a payment on Stripe" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="/checkout.css" /> <script src="https://js.stripe.com/v3/"></script> <script th:inline="javascript"> /*<![CDATA[*/ var publicKey = /*[[${publicKey}]]*/ null; var amount = /*[[${amount}]]*/ null; var email = /*[[${email}]]*/ null; var productName = /*[[${productName}]]*/ null; /*]]>*/ </script> <script src="/checkout.js" defer></script> </head> <body> <!-- Display a payment form --> <form id="payment-form"> <h2>Like the content: Support JavaWhizz</h2> <span>You are about to make a payment of: </span> <span th:text="*{amount}"></span> <span>USD</span> <div id="link-authentication-element"> <!--Stripe.js injects the Link Authentication Element--> </div> <div id="payment-element"> <!--Stripe.js injects the Payment Element--> </div> <button id="submit"> <div class="spinner hidden" id="spinner"></div> <span id="button-text">Pay now</span> </button> <div id="payment-message" class="hidden"></div> </form> </body> </html>
Inside the head section of the checkout page, use inline JavaScript to retrieve the data. By using Thymeleaf expressions, you can add the data to JavaScript variables. As a result, the variables are available to every JavaScript file in the application.
Note that the checkout page has a link to a JavaScript file named checkout.js. This file uses the JavaScript variables to send a request to the server to create a payment intent.
After creating the payment intent, Stripe injects the card into the checkout page. To complete the payment, you only need to enter the card details and click the button labeled Pay Now.
Implement JavaScript code for the checkout page
Create a file named checkout.js under the package src/main/resources/static. Copy and paste the following code into the file.
// This is your test publishable API key. const stripe = Stripe(publicKey); // The items the customer wants to buy const request = { amount: amount, email: email, productName: productName } let elements; initialize(); checkStatus(); document .querySelector("#payment-form") .addEventListener("submit", handleSubmit); let emailAddress = ''; // Fetches a payment intent and captures the client secret let paymentIntentID = ''; async function initialize() { const response = await fetch("/create-payment-intent", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), }); const { intentID, clientSecret } = await response.json(); paymentIntentID = intentID; const appearance = { theme: 'stripe', }; elements = stripe.elements({ appearance, clientSecret }); const linkAuthenticationElement = elements.create("linkAuthentication"); linkAuthenticationElement.mount("#link-authentication-element"); linkAuthenticationElement.on('change', (event) => { emailAddress = event.value.email; }); const paymentElementOptions = { layout: "tabs", defaultValues: { billingDetails:{ email: request.email } } }; const paymentElement = elements.create("payment", paymentElementOptions); paymentElement.mount("#payment-element"); } console.log(paymentIntentID); async function handleSubmit(e) { e.preventDefault(); setLoading(true); const { error } = await stripe.confirmPayment({ elements, confirmParams: { // Make sure to change this to your payment completion page return_url: "https://dashboard.stripe.com/test/payments/"+paymentIntentID, receipt_email: emailAddress }, }); // This point will only be reached if there is an immediate error when // confirming the payment. Otherwise, your customer will be redirected to // your `return_url`. For some payment methods like iDEAL, your customer will // be redirected to an intermediate site first to authorize the payment, then // redirected to the `return_url`. if (error.type === "card_error" || error.type === "validation_error") { showMessage(error.message); } else { showMessage("An unexpected error occurred."); } setLoading(false); } // Fetches the payment intent status after payment submission async function checkStatus() { const clientSecret = new URLSearchParams(window.location.search).get( "payment_intent_client_secret" ); if (!clientSecret) { return; } const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret); switch (paymentIntent.status) { case "succeeded": showMessage("Payment succeeded!"); break; case "processing": showMessage("Your payment is processing."); break; case "requires_payment_method": showMessage("Your payment was not successful, please try again."); break; default: showMessage("Something went wrong."); break; } } // ------- UI helpers ------- function showMessage(messageText) { const messageContainer = document.querySelector("#payment-message"); messageContainer.classList.remove("hidden"); messageContainer.textContent = messageText; setTimeout(function () { messageContainer.classList.add("hidden"); messageContainer.textContent = ""; }, 4000); } // Show a spinner on payment submission function setLoading(isLoading) { if (isLoading) { // Disable the button and show a spinner document.querySelector("#submit").disabled = true; document.querySelector("#spinner").classList.remove("hidden"); document.querySelector("#button-text").classList.add("hidden"); } else { document.querySelector("#submit").disabled = false; document.querySelector("#spinner").classList.add("hidden"); document.querySelector("#button-text").classList.remove("hidden"); } }
Note that most of this code is available in the Stripe documentation. In spite of this, you need to make a few changes for the application to work.
For example, since we not using static data, start by creating a request object to send to the server. The request object should have the amount, email, and product name. Note that these are the JavaScript variables retrieved from the checkout page.
Since the payment form should be pre-populated with the email. Add the code for this behavior in the paymentElementOptions
object. Apart from this, the code has comments that explain how the JavaScript code works.
Create a stylesheet for the checkout page
If you were keen, you might have noticed the CSS stylesheet link on the checkout page in the head section. This file styles the checkout page including the payment form.
Create a file named checkout.css under the package src/main/resources/static. Copy and paste the following code into the file.
/* Variables */ * { box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; -webkit-font-smoothing: antialiased; display: flex; justify-content: center; align-content: center; height: 100vh; width: 100vw; } form { width: 30vw; min-width: 500px; align-self: center; box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1), 0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07); border-radius: 7px; padding: 40px; } .hidden { display: none; } #payment-message { color: rgb(105, 115, 134); font-size: 16px; line-height: 20px; padding-top: 12px; text-align: center; } #payment-element { margin-bottom: 24px; } /* Buttons and links */ button { background: #5469d4; font-family: Arial, sans-serif; color: #ffffff; border-radius: 4px; border: 0; padding: 12px 16px; font-size: 16px; font-weight: 600; cursor: pointer; display: block; transition: all 0.2s ease; box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07); width: 100%; } button:hover { filter: contrast(115%); } button:disabled { opacity: 0.5; cursor: default; } /* spinner/processing state, errors */ .spinner, .spinner:before, .spinner:after { border-radius: 50%; } .spinner { color: #ffffff; font-size: 22px; text-indent: -99999px; margin: 0px auto; position: relative; width: 20px; height: 20px; box-shadow: inset 0 0 0 2px; -webkit-transform: translateZ(0); -ms-transform: translateZ(0); transform: translateZ(0); } .spinner:before, .spinner:after { position: absolute; content: ""; } .spinner:before { width: 10.4px; height: 20.4px; background: #5469d4; border-radius: 20.4px 0 0 20.4px; top: -0.2px; left: -0.2px; -webkit-transform-origin: 10.4px 10.2px; transform-origin: 10.4px 10.2px; -webkit-animation: loading 2s infinite ease 1.5s; animation: loading 2s infinite ease 1.5s; } .spinner:after { width: 10.4px; height: 10.2px; background: #5469d4; border-radius: 0 10.2px 10.2px 0; top: -0.1px; left: 10.2px; -webkit-transform-origin: 0px 10.2px; transform-origin: 0px 10.2px; -webkit-animation: loading 2s infinite ease; animation: loading 2s infinite ease; } @-webkit-keyframes loading { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } @keyframes loading { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } @media only screen and (max-width: 600px) { form { width: 80vw; min-width: initial; } }
Run and test the application
To run the application, use the command SHIFT+F10. If the application runs without any errors, you should see the following page.

Enter the product details and press the Submit button. Stripe creates a payment intent and injects a payment form into the checkout page. As a result, you should see the following checkout page.

Since you don’t want to use your real card for the transaction. Stripe provides test cards that you can use to test the payments. Go to the Stripe documentation to get one of the test numbers to use in your application. In this example, use the number 4242 4242 4242 4242 as your card number.
In the remaining fields, you can enter random data. To complete the payment, press the Pay Now button. If the transaction is complete, you should see the following page.

Conclusion
In this tutorial, you have learned how to integrate Stripe in Spring Boot using Thymeleaf. If you want to transition the application from test mode to live mode, update your public and secret keys.
The keys used in test mode are free for any developer to use. As a result, you need to upgrade your account to get keys that you can use in production environments.
There are other payment alternatives for Stripe including PayPal, Cash App, and Venmo. In the next tutorial, you will learn how to integrate these payment providers.
Meanwhile, try to integrate Stripe into your application using a live account. You can also check out our blog to read more articles. If you are a visual learner, this tutorial is available on YouTube. The code for the application is also available on GitHub.
Happy Hacking!
0 Comments