Stripe is one of the most popular cloud-based payment services that allows businesses and freelancers to easily collect payments over the internet. Founded in 2011, Stripe has revolutionized the way payments are accepted online. It is easier to use and faster to integrate in any application.

Within minutes, a business can create a merchant account and start accepting credit card payments thanks to Stripe's simple API integration and developer tools. The best thing is you do not need to store customer credit card information on your own server. Rather, it is securely handled by Stripe which, in return, provides you an authorization token to charge the credit card.

In this tutorial, I will show you how to use Stripe Java API to collect payments in a Spring Boot and Thymeleaf project. We will 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 code 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.

Stripe does not ask you to enter credit card details while signing up. You only need to verify your email address and phone number before start using their service.

After activating your account, login to access the Stripe dashboard. Then go to Developers -> API keys on the leftside menu and you will find Publishable key and Secret key. Copy these keys as we will be using them later to make requests.

Stripe API keys

Project 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 stripe-java dependency to use Stripe in our application. Our 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

We will create a Spring Boot Service class called StripeService. Our 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;
    }
}

In above service class StripeService, we defined several methods for completing different tasks:

  • createCustomer() - This method accepts user email and token obtained via stripe.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 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's Coupon 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 amount field. Also, this value must an integer.

The API_SECRET_KEY is populated from stripe.keys.secret environment variable that we got from Stripe dashboard and declared in 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's create a controller class called PaymentController that handles our GET and POST web requests. @Controller annotation is used by 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 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 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 one-time charge
  • /create-subscription - handles POST request for creating a new subscription
  • /cancel-subscription - handles POST subscription cancellation request
  • /create-charge - handles one-time charge POST request
  • /coupon-validator - handles POST 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 font-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 how our homepage looks like. It has two buttons to test both recurring and one-time Stripe charges.

Homepage

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

Subscription page shows a form to customer to fill out. The credit card form is generated by Stripe Elements.

Subscription Form

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 Form

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 one-time charge), you should get an alert message that displays subscription ID or charge ID depending on what you are testing. Furthermore, you can open your Stripe account, go to Billing -> Subscriptions tab and you should see a subscription created.

Stripe Dashboard

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. Our example project also demonstrates use of coupons which is another tricky thing to implement for most of the developers.

Stripe Java API offers a lot of possibilities for accepting payments. We only implemented a few operations. If you are interested in reading more about all Stripe methods, please visit the official API reference documentation.

At the end, I would to thank you for reading such a long tutorial. I really appreciate it. If you have any question or feedback, please feel free to send me a tweet.

P.S. If you are looking for help to integrate Stripe payments in your web application, I am available. Please checkout my hiring page for more details.

✌️ Like this article? Follow @attacomsian on Twitter. You can also follow me on LinkedIn and DEV. Buy me a coffee (cost $3)


Need help to start a new Spring Boot or MEAN stack project? I am available for contract work. Hire me to accomplish your business goals with engineering and design. Let’s talk about your project: hi@attacomsian.com.