Stripe is one of the most popular cloud-based payment services that allow businesses and freelancers to collect payments over the internet.
In this tutorial, I will show you how to use Stripe Java SDK to collect payments in a Spring Boot and Thymeleaf application.
We are going to create a sample Spring Boot web project that enables users to input a credit card to do the following:
- Subscribe to a monthly or yearly plan (recurring)
- Buy a single product (one-time payment)
- Use coupon codes for discounts
Create a Stripe Account
The first step is to create an account on stripe.com website and obtain public & secret API keys. These keys are required to communicate with Stripe and execute credit card charges.
After activating your account, log in to access the Stripe dashboard. Next go to Developers -> API keys on the left-side menu, and you will find Publishable key and Secret key. Copy these keys as we will be using them later to make requests.
Dependencies
Use Spring Initializr to create a new Gradle project with spring-boot-starter-web
and spring-boot-starter-thymeleaf
dependencies. We will be using Thymeleaf templating engine for our front-end part.
We also need to add the stripe-java
dependency to use Stripe in our application.
The final build.gradle
file will look the following:
build.gradle
plugins {
id 'org.springframework.boot' version '2.1.3.RELEASE'
id 'java'
}
apply plugin: 'io.spring.dependency-management'
group = 'com.attacomsian'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
// include stripe dependency
implementation 'com.stripe:stripe-java:7.26.0'
}
If you are using Maven, make sure that you add the following dependency to your project's pom.xml
file:
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>7.26.0</version>
</dependency>
Spring Service
The StripeService
service class will be responsible for communicating with Stripe API for creating customers, charging credit cards and validating coupon codes, etc.
StripeService.java
package com.attacomsian.stripe.services;
import com.stripe.Stripe;
import com.stripe.model.Charge;
import com.stripe.model.Coupon;
import com.stripe.model.Customer;
import com.stripe.model.Subscription;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class StripeService {
@Value("${stripe.keys.secret}")
private String API_SECRET_KEY;
public StripeService() {
}
public String createCustomer(String email, String token) {
String id = null;
try {
Stripe.apiKey = API_SECRET_KEY;
Map<String, Object> customerParams = new HashMap<>();
// add customer unique id here to track them in your web application
customerParams.put("description", "Customer for " + email);
customerParams.put("email", email);
customerParams.put("source", token); // ^ obtained with Stripe.js
//create a new customer
Customer customer = Customer.create(customerParams);
id = customer.getId();
} catch (Exception ex) {
ex.printStackTrace();
}
return id;
}
public String createSubscription(String customerId, String plan, String coupon) {
String id = null;
try {
Stripe.apiKey = API_SECRET_KEY;
Map<String, Object> item = new HashMap<>();
item.put("plan", plan);
Map<String, Object> items = new HashMap<>();
items.put("0", item);
Map<String, Object> params = new HashMap<>();
params.put("customer", customerId);
params.put("items", items);
//add coupon if available
if (!coupon.isEmpty()) {
params.put("coupon", coupon);
}
Subscription sub = Subscription.create(params);
id = sub.getId();
} catch (Exception ex) {
ex.printStackTrace();
}
return id;
}
public boolean cancelSubscription(String subscriptionId) {
boolean status;
try {
Stripe.apiKey = API_SECRET_KEY;
Subscription sub = Subscription.retrieve(subscriptionId);
sub.cancel(null);
status = true;
} catch (Exception ex) {
ex.printStackTrace();
status = false;
}
return status;
}
public Coupon retrieveCoupon(String code) {
try {
Stripe.apiKey = API_SECRET_KEY;
return Coupon.retrieve(code);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
public String createCharge(String email, String token, int amount) {
String id = null;
try {
Stripe.apiKey = API_SECRET_KEY;
Map<String, Object> chargeParams = new HashMap<>();
chargeParams.put("amount", amount);
chargeParams.put("currency", "usd");
chargeParams.put("description", "Charge for " + email);
chargeParams.put("source", token); // ^ obtained with Stripe.js
//create a charge
Charge charge = Charge.create(chargeParams);
id = charge.getId();
} catch (Exception ex) {
ex.printStackTrace();
}
return id;
}
}
We defined several methods for completing different tasks in StripeService
:
createCustomer()
- This method accepts user email and token obtained viastripe.js
and returns the Stripe customer ID.createSubscription()
- It is used for creating a recurring subscription against a Stripe customer. It accepts Stripe customer ID, unique plan ID (read more), and coupon code, if available. It returns the unique subscription ID generated by Stripe.cancelSubscription()
- This method accepts Stripe subscription ID and cancels it.retrieveCoupon()
- It retrieves the coupon code details from Stripe and returns Stripe'sCoupon
object.createCharge()
- This method is used for creating one-time standalone charges. It returns the Stripe charge ID.
For charges, Stripe requires the amount to be in cents and not in dollars. If you are charging $20, make sure to pass 2000 to Stripe in the
amount
field. Also, this value must an integer.
The API_SECRET_KEY
is populated from the stripe.keys.secret
environment variable that we got from the Stripe dashboard and declared in the application.properties
file.
application.properties
#Stripe keys
stripe.keys.public=pk_test_XXXXXXXXXXXXXX
stripe.keys.secret=sk_test_XXXXXXXXXXXXXX
#Don't cache thymeleaf files - FOR TEST PURPOSE ONLY
spring.thymeleaf.cache=false
Spring Controller
Finally, let us create a controller class called PaymentController
that handles our GET
and POST
web requests. @Controller
annotation is used by the Spring MVC framework to indicate that this class serves the role of the controller.
PaymentController.java
package com.attacomsian.stripe.controllers;
import com.attacomsian.stripe.commons.Response;
import com.attacomsian.stripe.services.StripeService;
import com.stripe.model.Coupon;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class PaymentController {
@Value("${stripe.keys.public}")
private String API_PUBLIC_KEY;
private StripeService stripeService;
public PaymentController(StripeService stripeService) {
this.stripeService = stripeService;
}
@GetMapping("/")
public String homepage() {
return "homepage";
}
@GetMapping("/subscription")
public String subscriptionPage(Model model) {
model.addAttribute("stripePublicKey", API_PUBLIC_KEY);
return "subscription";
}
@GetMapping("/charge")
public String chargePage(Model model) {
model.addAttribute("stripePublicKey", API_PUBLIC_KEY);
return "charge";
}
/*========== REST APIs for Handling Payments ===================*/
@PostMapping("/create-subscription")
public @ResponseBody
Response createSubscription(String email, String token, String plan, String coupon) {
//validate data
if (token == null || plan.isEmpty()) {
return new Response(false, "Stripe payment token is missing. Please, try again later.");
}
//create customer first
String customerId = stripeService.createCustomer(email, token);
if (customerId == null) {
return new Response(false, "An error occurred while trying to create a customer.");
}
//create subscription
String subscriptionId = stripeService.createSubscription(customerId, plan, coupon);
if (subscriptionId == null) {
return new Response(false, "An error occurred while trying to create a subscription.");
}
// Ideally you should store customerId and subscriptionId along with customer object here.
// These values are required to update or cancel the subscription at a later stage.
return new Response(true, "Success! Your subscription id is " + subscriptionId);
}
@PostMapping("/cancel-subscription")
public @ResponseBody
Response cancelSubscription(String subscriptionId) {
boolean status = stripeService.cancelSubscription(subscriptionId);
if (!status) {
return new Response(false, "Failed to cancel the subscription. Please, try later.");
}
return new Response(true, "Subscription cancelled successfully.");
}
@PostMapping("/coupon-validator")
public @ResponseBody
Response couponValidator(String code) {
Coupon coupon = stripeService.retrieveCoupon(code);
if (coupon != null && coupon.getValid()) {
String details = (coupon.getPercentOff() == null ? "$" + (coupon.getAmountOff() / 100) : coupon.getPercentOff() + "%") +
" OFF " + coupon.getDuration();
return new Response(true, details);
} else {
return new Response(false, "This coupon code is not available. This may be because it has expired or has " +
"already been applied to your account.");
}
}
@PostMapping("/create-charge")
public @ResponseBody
Response createCharge(String email, String token) {
//validate data
if (token == null) {
return new Response(false, "Stripe payment token is missing. Please, try again later.");
}
//create charge
String chargeId = stripeService.createCharge(email, token, 999); //$9.99 USD
if (chargeId == null) {
return new Response(false, "An error occurred while trying to create a charge.");
}
// You may want to store the charge id along with order information
return new Response(true, "Success! Your charge id is " + chargeId);
}
}
The above controller class provides the following end-points:
/
- displays the homepage/subscription
- displays a form to create a recurring subscription (with coupon)/charge
- displays a form to create a one-time charge/create-subscription
- handlesPOST
request for creating a new subscription/cancel-subscription
- handlesPOST
subscription cancellation request/create-charge
- handles one-time chargePOST
request/coupon-validator
- handlesPOST
request to validate a coupon code
Thymeleaf Front-end
Our front-end pages are basic HTML, Bootstrap 4 & Thymeleaf templates. We are using Stripe Elements, a front-end JavaScript library offered by Stripe, for creating a form on our site that submits the credit card information directly to Stripe. This also eliminates any credit card information flowing into our web application.
Homepage
Here is what our homepage looks like. It has two buttons to test both recurring and one-time Stripe charges.
homepage.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Homepage</title>
<!--Bootstrap 4 CSS-->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<!--Bootstrap 4 JavaScript-->
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</head>
<body class="bg-light pt-5">
<!--hero section-->
<section class="py-5">
<div class="container">
<div class="row">
<div class="col-lg-7 col-md-10 col-12 my-auto mx-auto text-center">
<h1>
Stripe Payment Examples
</h1>
<p class="lead mb-4">
What would you like to do?
</p>
<a class="btn btn-primary" th:href="@{/subscription}">Create Recurring Subscription</a>
<a class="btn btn-success" th:href="@{/charge}">Create One-Time Charge</a>
<p class="mt-5 text-muted">
<small>An example project by <a th:href="@{https://attacomsian.com}" target="_blank">Atta</a>.</small>
</p>
</div>
</div>
</div>
</section>
</body>
</html>
Subscription Page
The subscription page shows a form for the customer to fill out. The credit card form is generated by Stripe Elements.
4242 4242 4242 4242 is not a real credit card number. It is one of the several Stripe testing cards available online. These cards only work in testing mode.
subscription.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Subscription</title>
<!--Bootstrap 4 CSS-->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<!--Bootstrap 4 JavaScript-->
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<!--Stripe JavaScript Library-->
<script src="https://js.stripe.com/v3/"></script>
</head>
<body class="bg-light pt-5">
<!--hero section-->
<section class="py-5">
<div class="container">
<div class="row">
<div class="col-lg-6 col-md-8 col-12 my-auto mx-auto">
<h1>
Stripe Recurring Subscription
</h1>
<p class="lead mb-4">
Please fill the form below to complete the payment
</p>
<h5 class="mb-2">Choose your payment plan</h5>
<p class="text-muted">
60% OFF when you upgrade to annual plan.
</p>
<div class="py-2">
<div class="custom-control custom-radio">
<input class="custom-control-input" id="monthly-plan" name="premium-plan" type="radio"
value="monthly-subscription"/>
<label class="custom-control-label" for="monthly-plan">
<strong>Monthly $9.99</strong><br/>
<small class="text-muted">
Pay $9.99 every month and get access to all premium features.
</small>
</label>
</div>
<div class="custom-control custom-radio mt-3">
<input checked="" class="custom-control-input" id="annually-plan" name="premium-plan"
type="radio" value="annual-subscription"/>
<label class="custom-control-label" for="annually-plan">
<strong>Yearly $49.99</strong>
<span class="badge badge-primary ml-1">60% OFF</span>
<br/>
<small class="text-muted">
Pay $49.99 every year and get access to all premium features.
</small>
</label>
</div>
</div>
<form action="#" id="payment-form" method="post">
<input id="api-key" type="hidden" th:value="${stripePublicKey}">
<div class="form-group">
<label class="font-weight-medium" for="card-element">
Enter credit or debit card below
</label>
<div class="w-100" id="card-element">
<!-- A Stripe Element will be inserted here. -->
</div>
</div>
<div class="form-group">
<input class="form-control" id="email" name="email"
placeholder="Email Address" type="email" required>
</div>
<div class="form-group">
<input class="form-control" id="coupon" name="coupon"
placeholder="Coupon code (optional)" type="text">
</div>
<!-- Used to display Element errors. -->
<div class="text-danger w-100" id="card-errors" role="alert"></div>
<div class="form-group pt-2">
<button class="btn btn-primary btn-block" id="submitButton" type="submit">
Pay With Your Card
</button>
<div class="small text-muted mt-2">
Pay securely with Stripe. By clicking the button above, you agree
to our <a target="_blank" href="#">Terms of Service</a>,
<a target="_blank" href="#">Privacy</a> and
<a target="_blank" href="#">Refund</a> policies.
</div>
</div>
</form>
<p class="mt-5 text-muted">
<small>An example project by <a th:href="@{https://attacomsian.com}" target="_blank">Atta</a>.
</small>
</p>
</div>
</div>
</div>
</section>
<!--custom javascript for handling subscription-->
<script>
$(function () {
var API_KEY = $('#api-key').val();
// Create a Stripe client.
var stripe = Stripe(API_KEY);
// Create an instance of Elements.
var elements = stripe.elements();
// Create an instance of the card Element.
var card = elements.create('card');
// Add an instance of the card Element into the `card-element` <div>.
card.mount('#card-element');
// Handle real-time validation errors from the card Element.
card.addEventListener('change', function (event) {
var displayError = document.getElementById('card-errors');
if (event.error) {
displayError.textContent = event.error.message;
} else {
displayError.textContent = '';
}
});
// Handle form submission.
var form = document.getElementById('payment-form');
form.addEventListener('submit', function (event) {
event.preventDefault();
//validate coupon if any
var code = $('#coupon').val().trim();
if (code.length > 0) {
$.post(
"/coupon-validator",
{code: code},
function (data) {
if (data.status) {
handlePayments();
} else {
alert(data.details);
}
}, 'json');
} else {
handlePayments();
}
});
//handle card submission
function handlePayments() {
stripe.createToken(card).then(function (result) {
if (result.error) {
// Inform the user if there was an error.
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
// Send the token to your server.
var token = result.token.id;
var plan = $('input[name="premium-plan"]:checked').val();
var email = $('#email').val();
var coupon = $('#coupon').val();
$.post(
"/create-subscription",
{email: email, token: token, plan: plan, coupon: coupon},
function (data) {
alert(data.details);
}, 'json');
}
});
}
});
</script>
</body>
</html>
In the code above, we have a pair of radio boxes to select the payment plan. These radio boxes' values (
monthly-subscription
&annual-subscription
) are actual Stripe plan IDs. You must replace these values with your own product plan IDs.
Charge Page
This page is used for testing one-time Stripe payments.
charge.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Charge</title>
<!--Bootstrap 4 CSS-->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<!--Bootstrap 4 JavaScript-->
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<!--Stripe JavaScript Library-->
<script src="https://js.stripe.com/v3/"></script>
</head>
<body class="bg-light pt-5">
<!--hero section-->
<section class="py-5">
<div class="container">
<div class="row">
<div class="col-lg-6 col-md-8 col-12 my-auto mx-auto">
<h1>
Stripe One-Time Charge
</h1>
<p class="lead mb-4">
Please fill the form below to complete the order payment
</p>
<div class="card mb-4">
<div class="card-body">
<h5>Leather Bag</h5>
<p class="lead">USD 9.99</p>
</div>
</div>
<form action="#" id="payment-form" method="post">
<input id="api-key" type="hidden" th:value="${stripePublicKey}">
<div class="form-group">
<label class="font-weight-medium" for="card-element">
Enter credit or debit card below
</label>
<div class="w-100" id="card-element">
<!-- A Stripe Element will be inserted here. -->
</div>
</div>
<div class="form-group">
<input class="form-control" id="email" name="email"
placeholder="Email Address" type="email" required>
</div>
<!-- Used to display Element errors. -->
<div class="text-danger w-100" id="card-errors" role="alert"></div>
<div class="form-group pt-2">
<button class="btn btn-primary btn-block" id="submitButton" type="submit">
Pay With Your Card
</button>
<div class="small text-muted mt-2">
Pay securely with Stripe. By clicking the button above, you agree
to our <a target="_blank" href="#">Terms of Service</a>,
<a target="_blank" href="#">Privacy</a> and
<a target="_blank" href="#">Refund</a> policies.
</div>
</div>
</form>
<p class="mt-5 text-muted">
<small>An example project by <a th:href="@{https://attacomsian.com}" target="_blank">Atta</a>.
</small>
</p>
</div>
</div>
</div>
</section>
<!--custom javascript for handling subscription-->
<script>
$(function () {
var API_KEY = $('#api-key').val();
// Create a Stripe client.
var stripe = Stripe(API_KEY);
// Create an instance of Elements.
var elements = stripe.elements();
// Create an instance of the card Element.
var card = elements.create('card');
// Add an instance of the card Element into the `card-element` <div>.
card.mount('#card-element');
// Handle real-time validation errors from the card Element.
card.addEventListener('change', function (event) {
var displayError = document.getElementById('card-errors');
if (event.error) {
displayError.textContent = event.error.message;
} else {
displayError.textContent = '';
}
});
// Handle form submission.
var form = document.getElementById('payment-form');
form.addEventListener('submit', function (event) {
event.preventDefault();
// handle payment
handlePayments();
});
//handle card submission
function handlePayments() {
stripe.createToken(card).then(function (result) {
if (result.error) {
// Inform the user if there was an error.
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
// Send the token to your server.
var token = result.token.id;
var email = $('#email').val();
$.post(
"/create-charge",
{email: email, token: token},
function (data) {
alert(data.details);
}, 'json');
}
});
}
});
</script>
</body>
</html>
Once you submit the form (either subscription or a one-time charge), you should get an alert message that displays the subscription ID or charge ID, depending on what you are testing.
Furthermore, you can open your Stripe account, go to the Billing -> Subscriptions tab, and you should see a subscription created.
Source code: Download the complete source code from GitHub available under MIT license.
Conclusion
I hope that now you know how to integrate Stripe in your Spring Boot web project to collect recurring and one-time payments. The example project also demonstrates the use of coupons which is another tricky thing to implement for most developers.
Stripe Java SDK offers a lot of possibilities for accepting payments. We only implemented a few operations. If you are interested in reading about all Stripe methods, please visit the official API reference documentation.
✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.