1---
2title: React to Stripe events
3description: consumeEvents provisions the webhook endpoint and runs one Effect per delivery. When to use it, which events to listen for, and how to manage the endpoint yourself.
4---
5
6Stripe tells you what happened asynchronously. A payment succeeded, a
7renewal was declined, a merchant finished onboarding — none of these
8come back on the request that started them. Anything your app needs to
9*know* about billing state, it learns from webhooks.
10
11There are two levels:
12
131. **`WebhookEndpoint`** — the Stack resource. Owns the Stripe
14 endpoint: URL, enabled event types, signing secret.
152. **`consumeEvents`** — the event source. One handler, one typed
16 `Effect` per delivery.
17
18For the common case you only write level 2. `consumeEvents` creates
19the `WebhookEndpoint` pointed at the Worker it runs on, enables exactly
20the events you listed, and binds the minted signing secret so every
21delivery is verified before your handler sees it.
22
23## On a Worker
24
25```typescript
26// src/Api.ts
27import * as Cloudflare from "alchemy/Cloudflare";
28import * as Stripe from "alchemy/Stripe";
29import * as Effect from "effect/Effect";
30import * as HttpServerResponse from "effect/http/HttpServerResponse";
31
32export default class Api extends Cloudflare.Worker<Api>()(
33 "Api",
34 { main: import.meta.url },
35 Effect.gen(function* () {
36 yield* Stripe.consumeEvents(
37 "Events",
38 {
39 events: [
40 Stripe.CustomerSubscriptionCreated,
41 Stripe.CustomerSubscriptionDeleted,
42 ],
43 },
44 Effect.fn(function* (event) {
45 // event: CustomerSubscriptionCreated | CustomerSubscriptionDeleted
46 switch (event.type) {
47 case "customer.subscription.created":
48 yield* Effect.log(`new subscription ${event.object.id}`);
49 return;
50 case "customer.subscription.deleted":
51 yield* Effect.log(`canceled ${event.object.id}`);
52 return;
53 }
54 }),
55 );
56
57 return {
58 fetch: Effect.succeed(HttpServerResponse.text("ok")),
59 };
60 }).pipe(Effect.provide(Stripe.ConsumeEventsLive)),
61) {}
62```
63
64Pass event **classes**, not `"customer.subscription.created"` strings.
65The class is both the subscription key and the type of `event` inside
66the handler, so `event.object` is a `Subscription` in the first case
67and the switch is exhaustive.
68
69Deliveries arrive at `/webhooks/stripe` on the Worker (override with
70`path`). The listener verifies `Stripe-Signature`; a bad signature is a
71`401` and never reaches your handler. A defect in your handler is a
72`500`, which makes Stripe retry.
73
74:::note[Handlers must be idempotent]
75Stripe retries on any non-2xx and may deliver an event more than once.
76Write your handler so running it twice for the same event is harmless —
77upserting a row keyed by customer or account id, as both examples do,
78is the usual shape.
79:::
80
81## Which events
82
83The classes exported from `alchemy/Stripe`:
84
85| Class | Event | You'll want it when |
86|---|---|---|
87| `CheckoutSessionCompleted` | `checkout.session.completed` | A buyer paid on hosted Checkout |
88| `CustomerSubscriptionCreated` | `customer.subscription.created` | A subscription started (any path) |
89| `CustomerSubscriptionUpdated` | `customer.subscription.updated` | Renewal, plan change, `past_due` recovery |
90| `CustomerSubscriptionDeleted` | `customer.subscription.deleted` | Cancellation completed |
91| `InvoicePaid` | `invoice.paid` | A renewal charge succeeded |
92| `InvoicePaymentFailed` | `invoice.payment_failed` | A renewal charge was declined |
93| `PaymentIntentSucceeded` / `PaymentIntentFailed` | `payment_intent.*` | One-off payments |
94| `CustomerCreated` / `Updated` / `Deleted` | `customer.*` | Mirror customer records |
95| `AccountUpdated` | `account.updated` | A Connect merchant finished onboarding |
96
97[Sell a subscription](/stripe/guides/subscriptions) shows the first six
98driving an entitlement record. [Connect](/stripe/guides/connect) shows
99`AccountUpdated` driving a merchant table.
100
101## Managing the endpoint yourself
102
103Declare a `WebhookEndpoint` directly when the delivery URL is not a
104Worker alchemy deploys — an existing service, a different cloud:
105
106```typescript
107// alchemy.run.ts
108const webhook = yield* Stripe.WebhookEndpoint("Events", {
109 url: "https://api.example.com/stripe",
110 enabledEvents: ["customer.subscription.created"],
111});
112```
113
114`webhook.secret` is the signing secret; get it to that service however
115you get secrets there.
116
117If the target *is* an alchemy Worker but you want the endpoint declared
118in the Stack rather than inside the Worker, bind the secret explicitly:
119
120```typescript
121const api = yield* Api;
122const webhook = yield* Stripe.WebhookEndpoint("Events", {
123 url: Output.interpolate`${api.url}/webhooks/stripe`,
124 enabledEvents: [Stripe.CustomerSubscriptionCreated],
125});
126yield* Stripe.bindWebhookSecret(api, webhook.secret);
127```
128
129## Reference
130
131[WebhookEndpoint](/providers/stripe/webhookendpoint) ·
132[consumeEvents](/providers/stripe/consumeeventslive)