Diagnostic Order status and webhooks
The Stripe webhook is never delivered, so no order updates
Orders come in, payments succeed on Stripe, and nothing moves in WooCommerce. When every order behaves this way, the usual cause is the webhook itself. The endpoint is wrong, it is disabled, or it does not listen for the events WooCommerce needs. This guide checks all three with a short script and shows you exactly where the wiring is broken.
If no order ever updates, the webhook is almost always the problem. Use the Stripe API to list your webhook endpoints and confirm one is enabled, points at your store URL, and subscribes to the events WooCommerce needs. Then list recent events and look for delivery attempts that did not return a 2xx. The script below reports each of these so you know what to fix.
The problem in plain words
A webhook is a message Stripe sends to your store after a payment. WooCommerce uses it to move the order to Processing. For that message to arrive, you must create a webhook endpoint in Stripe that points at your store and listens for the right events. If any part of that is wrong, the message never lands and orders stay on Pending.
There are three common ways this breaks. The endpoint URL is wrong or points at an old domain. The endpoint exists but is disabled. Or the endpoint is enabled but does not subscribe to the events WooCommerce cares about, so the important ones are never sent.
Why it happens
The WooCommerce docs are direct about this. The plugin updates orders automatically only when webhooks are set up correctly. A store move to a new domain, a staging clone that copied the wrong endpoint, or an endpoint created with only a couple of events selected will all leave orders stranded. The citations at the end link the plugin docs and the setup guide.
You do not have to guess. Stripe can tell you every webhook endpoint on the account, whether it is enabled, what events it listens for, and whether recent deliveries succeeded. A read only script turns a vague no orders are updating into a precise here is the misconfigured endpoint.
The fix, as a flow
We ask Stripe for the list of webhook endpoints and check each one against three rules. Is it enabled, does it point at your store domain, and does it listen for the events WooCommerce needs. Then we look at recent events for failed deliveries. The output is a short report you act on by hand, since fixing the endpoint is a one time change in the dashboard.
Build it step by step
Decide what a healthy endpoint looks like
Keep the rules in one pure function. An endpoint is healthy when it is enabled, its URL is on your store domain, and its events cover the ones WooCommerce needs. An endpoint that listens for all events, shown as a single star, also passes.
from urllib.parse import urlparse
REQUIRED = {"payment_intent.succeeded", "payment_intent.payment_failed",
"charge.succeeded", "charge.refunded"}
def endpoint_health(endpoint, store_host):
events = set(endpoint.get("enabled_events", []))
covers = "*" in events or REQUIRED.issubset(events)
host_ok = urlparse(endpoint.get("url", "")).netloc == store_host
return {
"id": endpoint["id"],
"enabled": endpoint["status"] == "enabled",
"points_at_store": host_ok,
"covers_events": covers,
"missing_events": sorted(REQUIRED - events) if not covers else [],
}
const REQUIRED = ["payment_intent.succeeded", "payment_intent.payment_failed",
"charge.succeeded", "charge.refunded"];
export function endpointHealth(endpoint, storeHost) {
const events = new Set(endpoint.enabled_events || []);
const covers = events.has("*") || REQUIRED.every((e) => events.has(e));
const hostOk = new URL(endpoint.url || "http://none").host === storeHost;
return {
id: endpoint.id,
enabled: endpoint.status === "enabled",
pointsAtStore: hostOk,
coversEvents: covers,
missingEvents: covers ? [] : REQUIRED.filter((e) => !events.has(e)),
};
}
Ask Stripe for the endpoints and check each one
List every webhook endpoint on the account and run the health check. Any endpoint that is disabled, points at the wrong domain, or misses events is worth flagging.
import stripe
def check_endpoints(store_host):
reports = []
for endpoint in stripe.WebhookEndpoint.list(limit=100).auto_paging_iter():
reports.append(endpoint_health(endpoint, store_host))
return reports
export async function checkEndpoints(stripe, storeHost) {
const reports = [];
for await (const endpoint of stripe.webhookEndpoints.list({ limit: 100 })) {
reports.push(endpointHealth(endpoint, storeHost));
}
return reports;
}
Look for events that failed to deliver
A healthy looking endpoint can still be failing on the wire. Each Stripe event carries a pending_webhooks count. When it stays above zero, the event has not been delivered and accepted. Counting recent events with pending deliveries tells you the endpoint is not returning a success response.
def undelivered_recent(limit=100):
pending = 0
total = 0
for event in stripe.Event.list(limit=limit).auto_paging_iter():
total += 1
if event.get("pending_webhooks", 0) > 0:
pending += 1
return {"checked": total, "still_pending": pending}
export async function undeliveredRecent(stripe, limit = 100) {
let pending = 0, total = 0;
for await (const event of stripe.events.list({ limit })) {
total++;
if ((event.pending_webhooks || 0) > 0) pending++;
}
return { checked: total, stillPending: pending };
}
This script never changes your store or your Stripe account. It reads and reports. Fixing the endpoint is a one time change you make in the Stripe dashboard using the WooCommerce Stripe webhook URL from your plugin settings.
The full code
The complete diagnostic lists your endpoints, checks the three rules, counts recent undelivered events, and prints a short report. Read it, fix the endpoint that is flagged, then send a test event from Stripe to confirm.
View this code on GitHub Full runnable folder with tests in the woocommerce-fixes repo.
"""Check whether the Stripe webhook that WooCommerce depends on is set up right.
Read only. It reports problems, it does not change anything.
"""
import os
from urllib.parse import urlparse
import stripe
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
STORE_HOST = urlparse(os.environ["WOO_STORE_URL"]).netloc
REQUIRED = {"payment_intent.succeeded", "payment_intent.payment_failed",
"charge.succeeded", "charge.refunded"}
def endpoint_health(endpoint, store_host):
events = set(endpoint.get("enabled_events", []))
covers = "*" in events or REQUIRED.issubset(events)
host_ok = urlparse(endpoint.get("url", "")).netloc == store_host
return {
"id": endpoint["id"],
"url": endpoint.get("url", ""),
"enabled": endpoint["status"] == "enabled",
"points_at_store": host_ok,
"covers_events": covers,
"missing_events": sorted(REQUIRED - events) if not covers else [],
}
def check_endpoints(store_host):
return [endpoint_health(e, store_host)
for e in stripe.WebhookEndpoint.list(limit=100).auto_paging_iter()]
def undelivered_recent(limit=100):
pending = total = 0
for event in stripe.Event.list(limit=limit).auto_paging_iter():
total += 1
if event.get("pending_webhooks", 0) > 0:
pending += 1
return {"checked": total, "still_pending": pending}
def run():
reports = check_endpoints(STORE_HOST)
healthy = [r for r in reports if r["enabled"] and r["points_at_store"] and r["covers_events"]]
print(f"Found {len(reports)} endpoint(s). {len(healthy)} healthy for this store.")
for r in reports:
flags = []
if not r["enabled"]:
flags.append("disabled")
if not r["points_at_store"]:
flags.append("wrong domain")
if not r["covers_events"]:
flags.append("missing events: " + ", ".join(r["missing_events"]))
status = "OK" if not flags else "PROBLEM: " + "; ".join(flags)
print(f" {r['id']} {r['url']} -> {status}")
if not healthy:
print("No healthy endpoint points at this store. That is why orders do not update.")
delivery = undelivered_recent()
print(f"Recent events checked: {delivery['checked']}, still pending delivery: {delivery['still_pending']}")
if delivery["still_pending"]:
print("Events are not being accepted by your endpoint. Check for a firewall, CDN, or a server error.")
if __name__ == "__main__":
run()
/**
* Check whether the Stripe webhook that WooCommerce depends on is set up right.
* Read only. It reports problems, it does not change anything.
*/
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const STORE_HOST = new URL(process.env.WOO_STORE_URL).host;
const REQUIRED = ["payment_intent.succeeded", "payment_intent.payment_failed",
"charge.succeeded", "charge.refunded"];
function endpointHealth(endpoint, storeHost) {
const events = new Set(endpoint.enabled_events || []);
const covers = events.has("*") || REQUIRED.every((e) => events.has(e));
let hostOk = false;
try { hostOk = new URL(endpoint.url).host === storeHost; } catch (e) { hostOk = false; }
return {
id: endpoint.id,
url: endpoint.url || "",
enabled: endpoint.status === "enabled",
pointsAtStore: hostOk,
coversEvents: covers,
missingEvents: covers ? [] : REQUIRED.filter((e) => !events.has(e)),
};
}
async function checkEndpoints(storeHost) {
const reports = [];
for await (const endpoint of stripe.webhookEndpoints.list({ limit: 100 })) {
reports.push(endpointHealth(endpoint, storeHost));
}
return reports;
}
async function undeliveredRecent(limit = 100) {
let pending = 0, total = 0;
for await (const event of stripe.events.list({ limit })) {
total++;
if ((event.pending_webhooks || 0) > 0) pending++;
}
return { checked: total, stillPending: pending };
}
async function run() {
const reports = await checkEndpoints(STORE_HOST);
const healthy = reports.filter((r) => r.enabled && r.pointsAtStore && r.coversEvents);
console.log(`Found ${reports.length} endpoint(s). ${healthy.length} healthy for this store.`);
for (const r of reports) {
const flags = [];
if (!r.enabled) flags.push("disabled");
if (!r.pointsAtStore) flags.push("wrong domain");
if (!r.coversEvents) flags.push("missing events: " + r.missingEvents.join(", "));
console.log(` ${r.id} ${r.url} -> ${flags.length ? "PROBLEM: " + flags.join("; ") : "OK"}`);
}
if (!healthy.length) console.log("No healthy endpoint points at this store. That is why orders do not update.");
const delivery = await undeliveredRecent();
console.log(`Recent events checked: ${delivery.checked}, still pending delivery: ${delivery.stillPending}`);
if (delivery.stillPending) {
console.log("Events are not being accepted by your endpoint. Check for a firewall, CDN, or a server error.");
}
}
run().catch((err) => { console.error(err); process.exit(1); });
Add a test
The health rule is the piece to test, since it decides what counts as a broken endpoint. It is pure, so the test just passes in endpoint objects and checks the result.
from check_webhook import endpoint_health
STORE = "shop.example.com"
ALL_EVENTS = ["payment_intent.succeeded", "payment_intent.payment_failed",
"charge.succeeded", "charge.refunded"]
def ep(**over):
base = {"id": "we_1", "status": "enabled",
"url": "https://shop.example.com/?wc-ajax=wc_stripe",
"enabled_events": ALL_EVENTS}
base.update(over)
return base
def test_healthy_endpoint_passes():
r = endpoint_health(ep(), STORE)
assert r["enabled"] and r["points_at_store"] and r["covers_events"]
def test_disabled_is_flagged():
assert endpoint_health(ep(status="disabled"), STORE)["enabled"] is False
def test_wrong_domain_is_flagged():
r = endpoint_health(ep(url="https://old-domain.com/?wc-ajax=wc_stripe"), STORE)
assert r["points_at_store"] is False
def test_missing_events_are_listed():
r = endpoint_health(ep(enabled_events=["charge.succeeded"]), STORE)
assert "payment_intent.succeeded" in r["missing_events"]
def test_star_covers_everything():
assert endpoint_health(ep(enabled_events=["*"]), STORE)["covers_events"] is True
import { test } from "node:test";
import assert from "node:assert/strict";
import { endpointHealth } from "./check.js";
const STORE = "shop.example.com";
const ALL = ["payment_intent.succeeded", "payment_intent.payment_failed",
"charge.succeeded", "charge.refunded"];
const ep = (over = {}) => ({
id: "we_1", status: "enabled",
url: "https://shop.example.com/?wc-ajax=wc_stripe",
enabled_events: ALL, ...over,
});
test("healthy endpoint passes", () => {
const r = endpointHealth(ep(), STORE);
assert.ok(r.enabled && r.pointsAtStore && r.coversEvents);
});
test("disabled is flagged", () => {
assert.equal(endpointHealth(ep({ status: "disabled" }), STORE).enabled, false);
});
test("wrong domain is flagged", () => {
assert.equal(endpointHealth(ep({ url: "https://old.com/?wc-ajax=wc_stripe" }), STORE).pointsAtStore, false);
});
test("missing events are listed", () => {
const r = endpointHealth(ep({ enabled_events: ["charge.succeeded"] }), STORE);
assert.ok(r.missingEvents.includes("payment_intent.succeeded"));
});
test("star covers everything", () => {
assert.equal(endpointHealth(ep({ enabled_events: ["*"] }), STORE).coversEvents, true);
});
Case studies
The endpoint that pointed at the old site
A shop moved from a myshop.wpengine.com staging address to its real domain. Payments worked, but no order updated. The endpoint in Stripe still pointed at the old staging URL, so every event went nowhere.
The diagnostic printed the endpoint with wrong domain in one line. The team created a new endpoint on the real domain, and orders started updating within minutes.
The endpoint that only listened for refunds
An endpoint was set up quickly with only charge.refunded selected. Refunds synced fine, which hid the problem, but new orders never moved because payment_intent.succeeded was never sent.
The report listed the missing events by name. Adding them, or switching the endpoint to all events, fixed new orders at once.
After the fix you should see one enabled endpoint on your store domain that listens for the needed events, and recent events showing zero still pending delivery. Send a test webhook from the Stripe dashboard to confirm the round trip end to end.
FAQ
How do I know if my Stripe webhook is set up correctly?
List your webhook endpoints with the Stripe API and confirm one is enabled, points at your store URL, and subscribes to the events WooCommerce needs, such as payment_intent.succeeded and charge.refunded. Then list recent events and check for delivery attempts that did not return a 2xx.
Which Stripe events does the WooCommerce Stripe plugin need?
At a minimum payment_intent.succeeded, payment_intent.payment_failed, charge.succeeded, and charge.refunded. Subscribing to all events also works and is the simplest way to stay correct.
Where is the WooCommerce Stripe webhook URL?
It is shown in WooCommerce, Settings, Payments, Stripe, under the webhook section. The endpoint you create in Stripe must point at that exact URL on your store domain.
Related field notes
Citations
On the problem:
- WooCommerce docs: automatic order status updates depend on webhooks being configured. woocommerce.com/document/stripe
- WooCommerce Stripe plugin issue: orders not updating when the webhook is misconfigured. github.com/woocommerce/woocommerce-gateway-stripe/issues/3342
- Guide: troubleshooting Stripe webhook delivery on WooCommerce. woohelpdesk.com
On the solution:
- Stripe API: list webhook endpoints and read enabled_events and status. docs.stripe.com/api/webhook_endpoints/list
- Stripe API: the Event object and pending_webhooks. docs.stripe.com/api/events/object
- Stripe docs: build and test a webhook endpoint. docs.stripe.com/webhooks
Stuck on a tricky one?
If you have a bug in WooCommerce, WooCommerce Subscriptions, or the WooCommerce Stripe gateway that you would rather hand off, this is the kind of work I do. Message me and we can work through it together.
Did this find your broken webhook?
If this pointed you straight at the misconfigured endpoint, you can buy me a coffee. It keeps these field notes free and growing.
Buy me a coffee on Ko-fi