java
42 lines · 7 steps
How a Stripe webhook controller works in Spring
A Spring REST controller that verifies Stripe webhook signatures and dispatches events to fulfillment.
Explained by
highlit
1@RestController
2@RequestMapping("/webhooks/stripe")
3public class StripeWebhookController {
4
5 private static final Logger log = LoggerFactory.getLogger(StripeWebhookController.class);
6
7 private final String webhookSecret;
8 private final OrderFulfillmentService fulfillmentService;
9
10 public StripeWebhookController(@Value("${stripe.webhook-secret}") String webhookSecret,
11 OrderFulfillmentService fulfillmentService) {
12 this.webhookSecret = webhookSecret;
13 this.fulfillmentService = fulfillmentService;
14 }
15
16 @PostMapping
17 public ResponseEntity<String> handle(@RequestBody String payload,
18 @RequestHeader("Stripe-Signature") String signature) {
19 final Event event;
20 try {
21 event = Webhook.constructEvent(payload, signature, webhookSecret);
22 } catch (SignatureVerificationException e) {
23 log.warn("Rejected Stripe webhook with invalid signature", e);
24 return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid signature");
25 }
26
27 switch (event.getType()) {
28 case "checkout.session.completed" -> {
29 Session session = (Session) event.getDataObjectDeserializer()
30 .getObject()
31 .orElseThrow(() -> new IllegalStateException("Unable to deserialize session"));
32 fulfillmentService.fulfill(session.getClientReferenceId(), session.getPaymentIntent());
33 }
34 case "payment_intent.payment_failed" ->
35 log.info("Payment failed for event {}", event.getId());
36 default ->
37 log.debug("Ignoring unhandled Stripe event type {}", event.getType());
38 }
39
40 return ResponseEntity.ok("");
41 }
42}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Always verify a webhook's signature against a shared secret before trusting its payload.
- 2Return a clear 4xx for tampered requests and a 2xx for accepted events so the sender stops retrying.
- 3Branch on the event type so you only act on the events your system actually cares about.
Related explainers
java
public class ThumbnailProcessor { private static final int MAX_CONCURRENCY = 4;
Bounded parallel thumbnail rendering in Java
concurrency
thread-pool
futures
Intermediate
7 steps
java
public class SortedListMerger { public static int[] merge(int[] a, int[] b) { int[] result = new int[a.length + b.length];
Merging two sorted arrays in Java
two-pointers
merging
arrays
Beginner
6 steps
java
import java.util.ArrayDeque; import java.util.Deque; public final class RollingAverage {
A rolling average over a sliding window
sliding-window
running-sum
deque
Intermediate
7 steps
java
@Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = StrongPasswordValidator.class) @Documented
Building a custom @StrongPassword validator in Spring
bean-validation
annotations
regex
Intermediate
7 steps
python
import json import logging import stripe
Handling Stripe webhooks in Django
webhooks
signature-verification
idempotency
Intermediate
7 steps
java
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider tokenProvider;
How a JWT auth filter works in Spring
authentication
jwt
servlet-filter
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-a-stripe-webhook-controller-works-in-spring-explained-java-902e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.