Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>In this example, we:
*
* <ul>
* <li>write a fallback callback to handle unrecognized event notifications
* <li>create a StripeClient called client
* <li>Initialize an EventNotificationHandler with the client, webhook secret, and fallback
* callback
* <li>register a specific handler for the "v1.billing.meter.error_report_triggered" event
* notification type
* <li>use handler.handle() to process the received notification webhook body
* </ul>
*/
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 =
Comment on lines +39 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: private static final variables are generally done in ALL_CAPS. I think private final would make more sense here anyway

Copy link
Member Author

@xavdid-stripe xavdid-stripe Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's fair! though, since we're calling it from public static void main, the compiler complains Cannot make a static reference to the non-static field. I think for the purposes of the example, it's fine as is.

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;
}
Comment on lines +64 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. I hadn't thought about this before, but perhaps they want to throw here so their webhook handler can return a non-200 response. Can you throw a checked exception here, or does that no longer compile?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I played with that, but then Java complains that the handler.onV1BillingMeterErrorReportTriggered(EventNotificationHandlerEndpoint::handleMeterErrors); line generates an unhandled exception. That's a little odd, since it feels like that's just a function pointer and isn't calling the function, but i'm not sure.

Given that it's just an example, I think i'm fine with either:

  • leaving the example not-quite-complete and letting users figure out how they want to handle the error
  • eating the error and understanding that it's not a production-ready handler.

do you have strong feelings?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the thing with checked exceptions--they are considered part of the signature of the function. I think before we go to GA, we should see if we can make a checked StripeException part of the signature of the Callback.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but I'm not sure how we communicate to java that calling onV1BillingMeterErrorReportTriggered wont throw that checked exception, but calling .handle() could throw every exception that any of the registered callback throw. I'm just not sure that's a doable java pattern

But yeah, it's a good thing to dig into before GA!

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();
}
}
}