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

Walkthrough

Space play ←→ step click any line
Three takeaways
  1. 1Always verify a webhook's signature against a shared secret before trusting its payload.
  2. 2Return a clear 4xx for tampered requests and a 2xx for accepted events so the sender stops retrying.
  3. 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.

How a Stripe webhook controller works in Spring — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code