Home » Free ACP Payments Module » Stripe Webhooks

How to Set Up Stripe Webhooks

In direct payment mode, the shopper pays on a hosted Stripe page, so the proof of payment arrives out of band as a Stripe webhook. The ACP Payment Module receives it at POST /webhooks/stripe, verifies the signature against your endpoint's signing secret, matches the event to a checkout session through metadata, marks the session completed, and triggers the order handoff. Setup is four small steps: create the endpoint in Stripe, choose two events, set one environment variable, and run a test payment.

Why the Webhook Is the Source of Truth

It is tempting to treat the shopper landing on your success page as proof of payment, and it is a mistake every payments team makes once. Redirects get abandoned, browsers crash mid-redirect, and a success URL can be visited by anyone who knows it. The webhook is different: it is a signed, server-to-server statement from Stripe that money actually moved, delivered with retries until your server acknowledges it. The module therefore changes order state only on verified webhook events in direct mode, the success page is decoration for humans, the webhook is the ledger. This is the standard architecture for hosted checkout integrations, and the module implements the verification and state rules so you do not have to.

Step 1: Create the webhook endpoint in Stripe.
In the Stripe dashboard, open the webhooks section (under Developers) and add a destination with the URL of your deployment plus the path /webhooks/stripe, for example https://store.yourtool.com/webhooks/stripe. The endpoint must be HTTPS and publicly reachable, Stripe delivers from its own infrastructure. If you embedded the module rather than running the standalone server, use whatever path you mounted handleStripeWebhook on, the embedding guide shows the wiring.
Step 2: Select the payment events.
Subscribe the endpoint to checkout.session.completed and payment_intent.succeeded. Those are the two event types the module recognizes as paid, the first fires when a hosted Checkout page is completed, the second when a PaymentIntent settles. Other event types are accepted and acknowledged but cause no state change, so over-subscribing is harmless noise rather than a bug, and under-subscribing is the failure mode to avoid.
Step 3: Set the signing secret.
Stripe shows a signing secret for the endpoint (it starts with whsec_). Put it in the STRIPE_WEBHOOK_SECRET environment variable and restart. The module verifies every delivery against this secret using the exact raw request body, requests with a missing or invalid Stripe-Signature header are rejected with a 400 and logged. There is no configuration to skip verification: the handler refuses to process events when no secret is set, an unverified payment confirmation is treated as no confirmation at all. The variable reference lives in the configuration page.
Step 4: Test with the Stripe CLI.
Local testing does not require a public URL. The Stripe CLI forwards live test-mode events to your machine:
stripe listen --forward-to localhost:3000/webhooks/stripe
The CLI prints a temporary signing secret, put that in STRIPE_WEBHOOK_SECRET for the session. Then run a real test purchase: create a cart through your MCP client, call checkout, open the returned URL, and pay with a Stripe test card such as 4242 4242 4242 4242. Within a second or two the forwarded event arrives, and a follow-up read of the checkout session shows status completed with an order attached.
Step 5: Verify the order handoff.
A completed session triggers exactly one order delivery to your configured order webhook, with the order id reused as the delivery's idempotency key, so even if Stripe delivers the same event several times (it does, by design, whenever your server is slow to acknowledge), your fulfillment systems see one order. Confirm your endpoint received the payload and check the signature if you configured ORDER_WEBHOOK_SECRET. The payload format and verification recipe are in the order webhook guide.

How the Module Processes a Paid Event

The handler's logic is worth knowing when you read logs. After signature verification, it extracts the checkout session id that the module stamped into Stripe metadata when it created the payment, plus the payment reference (the PaymentIntent id). If the event is paid and the session exists, it calls the checkout service's confirmPaid, which is idempotent and conservative: an already-completed session returns unchanged, and a canceled session is never resurrected to completed, a late webhook for a canceled checkout changes nothing. On success the session gains its order and the handoff fires. One edge is handled loudly instead of silently: a paid event that carries no session id (for example, a charge created outside the module on the same Stripe account) is logged as an error for manual reconciliation, because a real payment that cannot be matched to an order is exactly the thing you want a human to see.

The endpoint always acknowledges verified events with a 200, even no-op ones, which keeps Stripe's retry machinery calm. Failed signature checks return 400, prompting Stripe to retry later, the correct behavior if your secret was temporarily misconfigured.

Production Notes

Webhook reliability is mostly about boring operational hygiene. Keep the endpoint fast, the module's handler does milliseconds of work, so timeouts only appear if you put slow middleware in front of it. Keep the raw body intact: signature verification hashes the exact bytes Stripe sent, so any proxy or framework that re-serializes JSON before the handler will break verification, the standalone server and Lambda handler both preserve the raw body correctly. Monitor the Stripe dashboard's webhook delivery page during your first week live, it shows every delivery, response code, and retry, and is the fastest way to spot a misconfiguration. And if you rotate the signing secret, update the environment variable in the same deploy, a mismatch window shows up as a burst of 400s and delayed order completions, not lost money, since Stripe retries for days. Broader practices for keeping a store's infrastructure trustworthy are collected in our ecommerce security pillar.

Troubleshooting Quick Answers

Payments succeed but sessions never complete. Almost always the webhook never arrived: the endpoint URL is wrong, the events were not subscribed, or a firewall blocks Stripe. The Stripe dashboard's delivery log answers this in one glance.

Every delivery returns invalid_signature. The secret in STRIPE_WEBHOOK_SECRET does not match the endpoint (each endpoint has its own secret, and the CLI's temporary secret differs from the dashboard one), or something upstream modified the request body before verification.

Orders arrive twice at fulfillment. Your order endpoint is ignoring the Idempotency-Key header. The module sends a stable key per order exactly so duplicate webhook deliveries collapse into one fulfillment, dedupe on it.

Local test events do nothing. The mock payment provider also verifies webhooks in its own format, so make sure a real STRIPE_SECRET_KEY (test mode) is set when testing against actual Stripe events, otherwise the Stripe-format signature cannot be checked by the mock.