The webhook
A webhook is Stripe telling your website that a payment succeeded. Without one, your shop still takes money perfectly well, and then nothing else happens at all.
Set it up once, per mode, and then forget it exists. It lives on the Stripe tab, in the Stripe connection card, directly beneath your API keys and the test/live toggle.
Without the webhook, payments succeed and nothing follows. No confirmation email to your customer, no notification to you, no stock decrease, and no discount code redemption counted. The money arrives in Stripe and your site never learns of it. A warning sits across your admin until the connection is real.
The one-button way
On the Stripe tab, under the heading Webhook (Test mode) or Webhook (Live) depending on which mode you are in, press Set up webhook automatically.
The plugin creates the destination inside your Stripe account, subscribes it to all four events it needs, and saves the signing secret back into the plugin for you. There is nothing to copy, paste or reveal in the Stripe dashboard.
This is the route to take unless you have a reason not to. Doing it by hand is easy to get subtly wrong, and the two things people miss are the two that matter most.
The manual way
If you would rather do it yourself, or your Stripe permissions do not allow the plugin to create endpoints, everything you need is in the same card, tucked behind a fold marked Prefer to set it up by hand?. Open that and you will find the endpoint URL in a read-only box with a Copy button, a link straight to the Webhooks page of your Stripe dashboard already in the mode that matches the key above it, and the Signing secret box.
- Open Prefer to set it up by hand? and copy the endpoint URL.
- In Stripe, open Developers → Webhooks and add a destination. Older accounts call this "Add endpoint".
- Paste the URL, then tick all four of the events listed below. Ticking only the first one is the single most common way to end up with a shop that takes money and does nothing about it.
- Reveal the destination's signing secret, which begins
whsec_, and paste it into the Signing secret box back in the plugin. Save. - Press Re-check only. It reads your endpoints back from Stripe and verifies the URL, the events, the status, and that the secret is saved. Then press Verify connection now, which proves the whole path end to end by having Stripe generate a real event and confirming this site received it with a valid signature.
The four events
| Event | Why it is needed |
|---|---|
checkout.session.completed | An ordinary card payment succeeded. This is the one that sends the emails and moves stock. |
checkout.session.async_payment_succeeded | A delayed bank payment such as Bacs Direct Debit finally cleared. Without it, those orders are paid for and never fulfilled. |
checkout.session.async_payment_failed | A delayed bank payment failed, so the stock it was holding can be released. |
customer.created | Used by Verify connection now to prove the connection without taking a payment. |
If you build the endpoint by hand, include customer.created. The plugin quietly adds that event to an endpoint it created itself, but it will not edit one you made outside it. Leave it off and Verify connection now refuses to run, telling you the endpoint "does not listen for customer.created". Re-check only still works.
Test and live are separate destinations, with separate signing secrets. A destination created in live mode never fires for test payments, and the reverse. Set one up for each mode you actually use. The plugin remembers both secrets independently, so flipping the mode toggle swaps the whole connection over rather than making you re-enter anything.
What the connection check will and will not tell you
If the two async_payment events are missing, Re-check only reports it as a warning rather than a failure. It tells you the endpoint is listening for card payments only, and that a delayed bank payment would take the customer's money without ever completing the order.
It stops short of calling that a failure for an honest reason: the check cannot see which payment methods your Stripe account has switched on. Reading that would take a Stripe call the plugin does not make. A card-only shop genuinely does not need those two events, so blocking on them would be wrong for the shops that are fine.
Which means the judgement is yours, and it is a short one. If Bacs or any other delayed bank method is enabled in your Stripe account, treat that warning as an error and go and add the events.
What it actually does when it fires
When a payment completes, whether straight away on a card or days later on a bank debit, four things happen off the back of it.
| Job | Why it waits for the webhook |
|---|---|
| Sends the buyer's confirmation | Stripe cannot send it. Stripe does not know your delivery service or your collection note. |
| Sends your seller notification | Same reason, plus it carries the signed Mark as shipped button. |
| Decrements stock | Nothing else ever decrements stock. Not the browser, not the checkout. |
| Counts a discount code redemption | Counting at payment means an abandoned basket never burns a limited-use code. |
Is it secure?
Every delivery is verified against Stripe's signature before it is acted on. The timestamp must be within five minutes and the HMAC must match. Multiple signatures are accepted at once, because Stripe sends two during a secret rotation and rejecting the pair would break your shop mid-rotation. Duplicate deliveries are ignored by a ledger of session ids, so a Stripe retry cannot email your customer twice or take stock down twice.
When it is not working
| What you see | Where to look |
|---|---|
| No emails arrive at all | Check the webhook is connected first. If it is, the problem is mail delivery, not the webhook - see Emails. |
| Stock never goes down | The webhook is not connected. Nothing else decrements stock. |
| Re-check only fails after a mode switch | You are looking at the other mode's destination. Create one for this mode. |
| It worked, then stopped | The signing secret was rotated in Stripe. Paste the new one and save. |
| It worked, then stopped right after you changed a Stripe key | A key from a different Stripe account leaves its webhook behind in the old one. Since 3.7.0 the plugin warns you at the moment you save such a key, and the connection box on the Stripe tab stays red until it is put right. Set the webhook up again for the new account. |
| Card orders fulfil, bank payments never do | A hand-made endpoint missing the two async_payment events. Add them in Stripe. |
The Stripe tab also records the last webhook Stripe delivered and whether it succeeded, which answers "did it even try" without leaving WordPress.
Replacing a secret key is the moment to check this. Without a webhook, payments still complete - and that is what makes it dangerous. There are no order emails, no stock changes and no order records, and nothing tells the customer anything is wrong. Since 3.7.0 the plugin asks Stripe outright where it made the webhook itself, and gives an honest "check this" where the webhook was made by hand.
If you remember the webhook living on the Emails tab, you are not imagining it. It moved to the Stripe tab so the whole connection sits in one place, and the Emails tab keeps a card pointing you here.
Still stuck?
Write to me and it is me who answers, not a ticket queue. Tell me what you expected and what happened instead.


