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
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 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.