The Stripe Element keeps your platform’s view of payments, invoices, and subscriptions in sync with Stripe by receiving webhooks at a single endpoint, verifying their signature, and republishing them as internal Elements events. Every verified webhook is published as a raw event; the most common event types are also published as strongly-typed records so consuming code never has to deserialize a Stripe payload itself.
The Webhook Endpoint #
POST /stripe/webhook takes the raw request body and a Stripe-Signature header, and verifies them with the Stripe SDK’s Webhook.constructEvent against the configured webhook secret. This endpoint requires no Session authentication — Stripe can’t supply a Session secret — but every request is rejected unless its signature checks out:
- 503 if no webhook secret is configured yet
- 400 if the signature doesn’t verify
- 200
{"received":true}once the event is published, or{"received":true,"typed":false}if the raw event was published but Stripe’s payload couldn’t be deserialized into a typed record (see below)
On every successfully verified webhook — regardless of type — the endpoint first publishes a StripeRawEvent and writes an entry to the event log, then attempts to dispatch a typed event. This ordering means a mismatch between this Element’s Stripe SDK version and the shape of an incoming event (an EventDataObjectDeserializationException) never loses the event — the raw event and audit log entry are already recorded before the typed dispatch is attempted.
Stripe Dashboard Setup #
In the Stripe Dashboard, go to Developers → Webhooks → Add endpoint.
- Use the “Account” webhook type, not “Event destinations / v2” — the v2 format uses thin payloads and a different signing scheme that this Element does not support.
- Set the endpoint URL to your deployed base URL plus the webhook path, e.g.
https://your-host/Element/stripe/api/stripe/webhook. - Subscribe to the event types you need. Any webhook that arrives with a valid signature is always forwarded as a
RAW_WEBHOOKevent regardless of which types you subscribe to; the typed events below only fire for their corresponding subscribed types. - After saving, Stripe shows a Signing secret (
whsec_...). Copy it into thedev.getelements.elements.stripe.webhook.secretattribute — see Configuring the Stripe Element.
Testing Webhooks Locally #
Forward Stripe events to a local instance with the Stripe CLI:
stripe listen --forward-to localhost:8080/Element/stripe/api/stripe/webhook
Typed Events #
Constants live on StripeEvents in the api module. Each is declared at the class level of the webhook endpoint with @ElementEventProducer, and each carries strongly-typed fields pulled straight off the corresponding Stripe object — no payload parsing required in the consumer.
StripeEvents constant | Stripe event type | Record | Fields |
|---|---|---|---|
PAYMENT_SUCCEEDED | payment_intent.succeeded | StripePaymentSucceededEvent | paymentIntentId, amount, currency, customerId |
PAYMENT_FAILED | payment_intent.payment_failed | StripePaymentFailedEvent | paymentIntentId, failureMessage, customerId |
PAYMENT_CANCELED | payment_intent.canceled | StripePaymentCanceledEvent | paymentIntentId, customerId |
INVOICE_PAYMENT_SUCCEEDED | invoice.payment_succeeded | StripeInvoicePaymentSucceededEvent | invoiceId, paymentIntentId, amountPaid, currency |
INVOICE_PAYMENT_FAILED | invoice.payment_failed | StripeInvoicePaymentFailedEvent | invoiceId, subscriptionId, customerId, failureMessage |
SUBSCRIPTION_CREATED | customer.subscription.created | StripeSubscriptionCreatedEvent | subscriptionId, customerId, status, orgId |
SUBSCRIPTION_UPDATED | customer.subscription.updated | StripeSubscriptionUpdatedEvent | subscriptionId, customerId, status, orgId |
SUBSCRIPTION_CANCELLED | customer.subscription.deleted | StripeSubscriptionCancelledEvent | subscriptionId, customerId, orgId |
SUBSCRIPTION_TRIAL_WILL_END | customer.subscription.trial_will_end | StripeSubscriptionTrialWillEndEvent | subscriptionId, customerId, trialEnd (ISO-8601), orgId |
SETUP_INTENT_SUCCEEDED | setup_intent.succeeded | StripeSetupIntentSucceededEvent | setupIntentId, customerId |
PAYMENT_METHOD_ATTACHED | payment_method.attached | StripePaymentMethodAttachedEvent | paymentMethodId, customerId |
CHECKOUT_SESSION_COMPLETED | checkout.Session.completed | StripeCheckoutSessionCompletedEvent | sessionId, customerId, paymentIntentId, subscriptionId, mode, Metadata |
RAW_WEBHOOK | stripe.webhook | StripeRawEvent | type, eventId, rawJson — published for every verified webhook, including all of the above |
The orgId field on subscription events is read from the Stripe object’s Metadata under StripeService.METADATA_ORG_ID — it’s only populated if the subscription (or its customer) was created with that Metadata key set, e.g. via createCustomer‘s orgId parameter or a Checkout Session’s Metadata.
Two of these event types also trigger a side effect inside the webhook handler itself: on PAYMENT_SUCCEEDED and INVOICE_PAYMENT_SUCCEEDED, the handler calls StripeService.recordPaymentReceipt to record a receipt in the platform receipt store, using the payment’s userId Metadata (silently skipped if that Metadata is absent or blank).
Consuming Events from Another Element #
Any Element that depends on Stripe’s api module can subscribe to these events with @ElementEventConsumer, annotating a method on any Guice-managed Service:
import com.google.inject.Inject;
import dev.getelements.elements.sdk.annotation.ElementEventConsumer;
import dev.getelements.elements.stripe.StripeEvents;
public class EntitlementService {
@Inject
private UserInventoryDao userInventoryDao;
@ElementEventConsumer(StripeEvents.PAYMENT_SUCCEEDED)
public void onPaymentSucceeded() {
// called whenever a payment_intent.succeeded webhook is received
}
@ElementEventConsumer(StripeEvents.SUBSCRIPTION_CANCELLED)
public void onSubscriptionCancelled() {
// revoke access, notify player, etc.
}
@ElementEventConsumer(StripeEvents.RAW_WEBHOOK)
public void onAnyWebhook() {
// called for every verified Stripe webhook — use for event types
// that don't have a dedicated typed event
}
}
The Webhook Event Log #
Every verified webhook — typed or not — is also recorded to a MongoDB-backed audit log (StripeEventLogService, collection stripe_event_log) storing the Stripe event id, event type, and receipt timestamp. Browse it via the Stripe tab in the superuser admin panel, or query it directly with GET /stripe/events?type=&limit=20&offset=0, which supports filtering by event type and offset-based pagination. This is the fastest way to confirm whether a webhook actually arrived and how it was typed, without digging through Application logs.
Related Pages #
- Stripe — Element overview and core concepts
- Configuring the Stripe Element — setting the webhook secret and other attributes
- Stripe REST API Reference — the webhook endpoint alongside every other REST endpoint

