diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b8461103c8..b107b8827e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
This release changes the pinned API version to `2025-12-15.preview`.
* [#2104](https://github.com/stripe/stripe-java/pull/2104) Add EventNotificationHandler
+ * This is a new, simplified way to handle event notifications (AKA thin event webhooks). Learn more in the docs: https://docs.stripe.com/webhooks/event-notification-handlers
* [#2117](https://github.com/stripe/stripe-java/pull/2117) Update generated code for beta
* Add support for new resources `reserve.Hold`, `reserve.Plan`, and `reserve.Release`
* Add support for `list` and `retrieve` methods on resources `reserve.Hold` and `reserve.Release`
diff --git a/src/main/java/com/stripe/examples/EventNotificationHandlerEndpoint.java b/src/main/java/com/stripe/examples/EventNotificationHandlerEndpoint.java
new file mode 100644
index 00000000000..195420e022d
--- /dev/null
+++ b/src/main/java/com/stripe/examples/EventNotificationHandlerEndpoint.java
@@ -0,0 +1,107 @@
+package com.stripe.examples;
+
+import com.stripe.StripeClient;
+import com.stripe.StripeEventNotificationHandler;
+import com.stripe.StripeEventNotificationHandler.UnhandledNotificationDetails;
+import com.stripe.events.V1BillingMeterErrorReportTriggeredEventNotification;
+import com.stripe.exception.StripeException;
+import com.stripe.model.billing.Meter;
+import com.stripe.model.v2.core.EventNotification;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Receive and process event notifications (AKA thin events) like
+ * "v1.billing.meter.error_report_triggered" using EventNotificationHandler.
+ *
+ *
In this example, we:
+ *
+ *
+ * - write a fallback callback to handle unrecognized event notifications
+ *
- create a StripeClient called client
+ *
- Initialize an EventNotificationHandler with the client, webhook secret, and fallback
+ * callback
+ *
- register a specific handler for the "v1.billing.meter.error_report_triggered" event
+ * notification type
+ *
- use handler.handle() to process the received notification webhook body
+ *
+ */
+public class EventNotificationHandlerEndpoint {
+ private static final String API_KEY = System.getenv("STRIPE_API_KEY");
+ private static final String WEBHOOK_SECRET = System.getenv("WEBHOOK_SECRET");
+
+ private static final StripeClient client = new StripeClient(API_KEY);
+ private static final StripeEventNotificationHandler handler =
+ client.notificationHandler(
+ WEBHOOK_SECRET, EventNotificationHandlerEndpoint::fallbackCallback);
+
+ public static void main(String[] args) throws IOException {
+ handler.onV1BillingMeterErrorReportTriggered(
+ EventNotificationHandlerEndpoint::handleMeterErrors);
+
+ HttpServer server = HttpServer.create(new InetSocketAddress(4242), 0);
+ server.createContext("/webhook", new WebhookHandler());
+ server.setExecutor(null);
+ server.start();
+ }
+
+ private static void fallbackCallback(
+ EventNotification notif, StripeClient client, UnhandledNotificationDetails details) {
+ System.out.println("Received unhandled event notification type: " + notif.getType());
+ }
+
+ private static void handleMeterErrors(
+ V1BillingMeterErrorReportTriggeredEventNotification notif, StripeClient client) {
+ Meter meter;
+ try {
+ meter = notif.fetchRelatedObject();
+ } catch (StripeException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return;
+ }
+ System.out.println("Handling meter error for meter: " + meter.getDisplayName());
+ }
+
+ static class WebhookHandler implements HttpHandler {
+ // For Java 1.8 compatibility
+ public static byte[] readAllBytes(InputStream inputStream) throws IOException {
+ final int bufLen = 1024;
+ byte[] buf = new byte[bufLen];
+ int readLen;
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ while ((readLen = inputStream.read(buf, 0, bufLen)) != -1)
+ outputStream.write(buf, 0, readLen);
+
+ return outputStream.toByteArray();
+ }
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ if ("POST".equals(exchange.getRequestMethod())) {
+ InputStream requestBody = exchange.getRequestBody();
+ String webhookBody = new String(readAllBytes(requestBody), StandardCharsets.UTF_8);
+ String sigHeader = exchange.getRequestHeaders().getFirst("Stripe-Signature");
+
+ try {
+ handler.handle(webhookBody, sigHeader);
+
+ exchange.sendResponseHeaders(200, -1);
+ } catch (StripeException e) {
+ exchange.sendResponseHeaders(400, -1);
+ }
+ } else {
+ exchange.sendResponseHeaders(405, -1);
+ }
+ exchange.close();
+ }
+ }
+}