Integrating payment processing into your Spring Boot application can seem daunting, but with Stripe's robust API and Spring Boot's flexibility, it’s a manageable task. In this guide, we'll walk through the process of integrating Stripe payments into a Spring Boot application, focusing on a booking system example. We'll cover setting up Stripe, initiating a payment session, handling webhooks, and confirming payments.
Prerequisites
- A Stripe account with API keys (secret key and webhook secret).
- A Spring Boot project set up with dependencies like
spring-boot-starter-web
andstripe-java
. - Basic knowledge of Spring Boot, REST APIs, and Java.
- Add the Stripe Java library to your
pom.xml
:
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>25.6.0</version>
</dependency>
Step 1: Configuring Stripe in Spring Boot
To use Stripe's API, you need to set up your Stripe secret key in the application. This key authenticates your API requests.
In your application.properties
or application.yml
, add:
stripe.secret.key=sk_test_your_stripe_secret_key
stripe.webhook.secret=whsec_your_webhook_secret
Create a configuration class to initialize the Stripe API with the secret key:
import com.stripe.Stripe;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StripeConfig {
@Value("${stripe.secret.key}")
private String stripeSecretKey;
@PostConstruct
public void init() {
Stripe.apiKey = stripeSecretKey; // Set up Stripe API key
}
}
This ensures the Stripe API key is set when the application starts.
Step 2: Initiating a Payment Session
To process payments, you’ll create a Stripe Checkout Session, which redirects users to a Stripe-hosted payment page. Below is an example of initiating a payment for a booking.
Controller for Initiating Payment
Create a REST endpoint to initiate a payment for a booking:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/bookings")
public class BookingController {
private final BookingService bookingService;
public BookingController(BookingService bookingService) {
this.bookingService = bookingService;
}
@PostMapping("/{bookingId}/payments")
public ResponseEntity<BookingPaymentInitResponseDto> initiateBookingPayment(@PathVariable Long bookingId) {
BookingPaymentInitResponseDto response = new BookingPaymentInitResponseDto(bookingService.initiateBookingPayment(bookingId));
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
The BookingPaymentInitResponseDto
is a simple data transfer object (DTO) to return the Stripe session URL:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BookingPaymentInitResponseDto {
private String sessionUrl;
}
Service Logic for Payment Initiation
In the service layer, validate the booking, check user authorization, and create a Stripe Checkout Session:
import com.stripe.exception.StripeException;
import com.stripe.model.Customer;
import com.stripe.model.Session;
import com.stripe.param.CustomerCreateParams;
import com.stripe.param.SessionCreateParams;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class BookingService {
private final BookingRepository bookingRepository;
private final InventoryRepository inventoryRepository;
private final String frontendUrl;
public BookingService(BookingRepository bookingRepository, InventoryRepository inventoryRepository, @Value("${frontend.url}") String frontendUrl) {
this.bookingRepository = bookingRepository;
this.inventoryRepository = inventoryRepository;
this.frontendUrl = frontendUrl;
}
@Transactional(noRollbackFor = BookingExpiredException.class)
public String initiateBookingPayment(Long bookingId) {
Booking booking = bookingRepository.findById(bookingId)
.orElseThrow(() -> new ResourceNotFoundException("Booking Not Found With Id: " + bookingId));
User user = getCurrentUser(); // Assume this retrieves the authenticated user
if (!user.equals(booking.getUser())) {
throw new UnAuthorisedException("Booking Does Not Belong To This User With Id: " + user.getId());
}
if (hasBookingExpired(booking.getCreatedAt())) {
inventoryRepository.expireBooking(booking.getRoom().getId(),
booking.getCheckInDate(),
booking.getCheckOutDate(),
booking.getNumberOfRooms());
booking.setBookingStatus(BookingStatus.EXPIRED);
bookingRepository.save(booking);
throw new BookingExpiredException("Booking Has Been Already Expired");
}
String sessionUrl = getCheckoutSession(booking, frontendUrl + "/payment/success", frontendUrl + "/payment/failure");
booking.setBookingStatus(BookingStatus.PAYMENT_PENDING);
bookingRepository.save(booking);
return sessionUrl;
}
public String getCheckoutSession(Booking booking, String successUrl, String failureUrl) {
log.info("Creating session for booking with Id: {}", booking.getId());
User user = getCurrentUser();
try {
CustomerCreateParams customerParams = CustomerCreateParams.builder()
.setName(user.getName())
.setEmail(user.getEmail())
.build();
Customer customer = Customer.create(customerParams);
SessionCreateParams sessionParams = SessionCreateParams.builder()
.setMode(SessionCreateParams.Mode.PAYMENT)
.setBillingAddressCollection(SessionCreateParams.BillingAddressCollection.REQUIRED)
.setCustomer(customer.getId())
.setSuccessUrl(successUrl)
.setCancelUrl(failureUrl)
.addLineItem(
SessionCreateParams.LineItem.builder()
.setQuantity(Long.valueOf(booking.getNumberOfRooms()))
.setPriceData(
SessionCreateParams.LineItem.PriceData.builder()
.setCurrency("inr")
.setUnitAmount(booking.getAmount().multiply(BigDecimal.valueOf(100)).longValue())
.setProductData(
SessionCreateParams.LineItem.PriceData.ProductData.builder()
.setName(booking.getHotel().getName() + " : " + booking.getRoom().getType())
.setDescription("Booking ID: " + booking.getId())
.build()
)
.build()
)
.build()
)
.build();
Session session = Session.create(sessionParams);
booking.setPaymentSessionId(session.getId());
bookingRepository.save(booking);
log.info("Session created successfully for booking with Id: {}", booking.getId());
return session.getUrl();
} catch (StripeException e) {
throw new RuntimeException("Failed to create Stripe session", e);
}
}
}
This code:
- Validates the booking and user.
- Checks if the booking has expired.
- Creates a Stripe customer and a checkout session with details like the hotel name, room type, and amount (in INR, multiplied by 100 for Stripe’s cent-based system).
- Updates the booking status to
PAYMENT_PENDING
and saves the session ID.
Step 3: Handling Stripe Webhooks
Stripe uses webhooks to notify your application of payment events, such as a completed checkout session. Set up a webhook endpoint to handle these events.
Webhook Controller
import com.stripe.exception.SignatureVerificationException;
import com.stripe.model.Event;
import com.stripe.net.Webhook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/webhook")
public class StripeWebhookController {
private final BookingService bookingService;
private final String endpointSecret;
public StripeWebhookController(BookingService bookingService, @Value("${stripe.webhook.secret}") String endpointSecret) {
this.bookingService = bookingService;
this.endpointSecret = endpointSecret;
}
@PostMapping("/payment")
public ResponseEntity<Void> capturePayments(@RequestBody String payload, @RequestHeader("Stripe-Signature") String sigHeader) {
try {
Event event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
bookingService.capturePayment(event);
return ResponseEntity.noContent().build();
} catch (SignatureVerificationException e) {
throw new RuntimeException("Invalid webhook signature", e);
}
}
}
Webhook Handling Logic
Add the webhook handling logic to the BookingService
:
import com.stripe.model.Session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class BookingService {
// Other methods as above...
@Transactional
public void capturePayment(Event event) {
if ("checkout.session.completed".equals(event.getType())) {
Session session = (Session) event.getDataObjectDeserializer().getObject().orElse(null);
if (session == null) return;
String sessionId = session.getId();
Booking booking = bookingRepository.findByPaymentSessionId(sessionId)
.orElseThrow(() -> new ResourceNotFoundException("Booking not found for session Id: " + sessionId));
booking.setBookingStatus(BookingStatus.CONFIRMED);
bookingRepository.save(booking);
List<Inventory> lockReservedInventory = inventoryRepository.findAndLockReservedInventory(
booking.getRoom().getId(),
booking.getCheckInDate(),
booking.getCheckOutDate(),
booking.getNumberOfRooms());
inventoryRepository.confirmBooking(
booking.getRoom().getId(),
booking.getCheckInDate(),
booking.getCheckOutDate(),
booking.getNumberOfRooms());
log.info("Successfully confirmed the booking for Booking Id: {}", booking.getId());
} else {
log.warn("Unhandled event type: {}", event.getType());
}
}
}
This code:
- Verifies the webhook event using the Stripe webhook secret.
- Processes the
checkout.session.completed
event to confirm the booking. - Updates the booking status to
CONFIRMED
and adjusts inventory (e.g., decreases reserved rooms and increases booked rooms).
Step 4: Testing the Integration
-
Set Up Webhooks Locally: Use a tool like
ngrok
to expose your local server to Stripe’s webhook events. Configure the webhook URL in the Stripe Dashboard (e.g.,https://your-ngrok-url/webhook/payment
). -
Test Payment Flow:
- Create a booking and initiate a payment via the
/api/bookings/{bookingId}/payments
endpoint. - Use Stripe’s test card (e.g.,
4242 4242 4242 4242
) to complete the payment. - Verify that the webhook updates the booking status to
CONFIRMED
.
- Create a booking and initiate a payment via the
- Handle Errors: Test edge cases like expired bookings or invalid payments to ensure proper exception handling.
Top comments (0)