Edit Page

Webhooks

RESTHeart

POST /stripe/webhook receives Stripe events and applies them — updating subscription state or order status, recording transactions.

It is the mechanism that makes billing actually work: Checkout only opens a payment page, and nothing in the application changes until Stripe tells the server what happened.

Important

Without stripeWebhookService enabled and reachable from Stripe, a customer can pay successfully and remain on the free plan forever (subscriptions) or their order stays pending_payment forever (products).

The only public endpoint

/stripe/webhook has no authenticated caller: Stripe calls it directly. The module registers its own ACL allow rule for it — that exact path, POST and OPTIONS only.

Its security is signature verification. Every request must carry a Stripe-Signature header that verifies against stripeConfig.webhook-secret. A request without a valid signature is rejected before anything is parsed.

Status codes

Code Meaning

200

Event verified. Handled, ignored, or skipped as stale — all are success.

400

Signature failed, header missing, or no webhook-secret configured.

500

Unexpected failure (e.g. MongoDB). Stripe will retry.

Events handled — Subscriptions

Event Effect

checkout.session.completed

Logged only. Stripe follows with customer.subscription.created.

customer.subscription.created

Rebuilds full subscription state.

customer.subscription.updated

Plan change, renewal, quantity change, cancellation scheduling.

customer.subscription.deleted

Resets to default-plan, status canceled. Sends notification.

customer.subscription.trial_will_end

Sends the trial-will-end notification.

invoice.payment_succeeded

Status → active.

invoice.payment_failed

Status → past_due. Sends notification.

product.updated, price.updated

Invalidates the plans display cache.

Events handled — Products

Event Effect

checkout.session.completed (paid)

Order → paid. Appends payment transaction.

checkout.session.async_payment_succeeded

Order → paid. Appends payment transaction.

checkout.session.async_payment_failed

Order → failed.

checkout.session.expired

Order → expired (from pending_payment only).

charge.refunded

Appends refund transaction. Updates amount_refunded.

charge.dispute.created

Appends dispute transaction.

Which events to subscribe to

In Stripe Dashboard → Developers → Webhooks, subscribe to:

# subscriptions
checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
customer.subscription.trial_will_end
invoice.payment_succeeded
invoice.payment_failed
product.updated
price.updated

# products
checkout.session.async_payment_succeeded
checkout.session.async_payment_failed
charge.refunded
charge.dispute.created

checkout.session.completed appears in both lists — it is handled differently per mode.

Idempotency

Subscriptions — every write is conditional on the event timestamp being newer than the last one applied to that entity. Out-of-order delivery and redelivery are both no-ops.

Products — order status transitions are monotonic (the update filter asserts current status). The transactions collection has a unique index on stripe_event_id, so a redelivered event cannot double-record a refund.

Unknown customers

An event for a Stripe Customer that no entity is linked to is logged and ignored. This is normal when a Stripe account is shared with another application.

Local development

Stripe cannot reach localhost. Use the Stripe CLI:

stripe listen --forward-to localhost:8080/stripe/webhook

The CLI prints a signing secret (whsec_…) for that session — use it as STRIPE_WEBHOOK_SECRET.

Trigger events:

stripe trigger customer.subscription.updated
stripe trigger checkout.session.completed
stripe trigger charge.refunded