Imported from Lonsdale201/wp-agent-skills (
woocommerce/wc-stripe-webhooks/SKILL.md). Install upstream withnpx skills add Lonsdale201/wp-agent-skills --skill wc-stripe-webhooks. Copyright stays with the author.
WooCommerce Stripe webhooks
Use this when custom code observes Stripe events or diagnoses paid orders stuck in Pending/On hold. Let the gateway own signature verification, order matching, locks, deferred processing, and order transitions.
Endpoint and trust boundary
The plugin endpoint is generated by WC_Stripe_Helper::get_webhook_url():
https://example.com/?wc-api=wc_stripe
The handler runs on woocommerce_api_wc_stripe and accepts POST only. Do not register a second endpoint for the same Stripe events merely to run business logic.
For normal Stripe events it verifies:
- configured live/test webhook secret
Stripe-Signatureshape- HMAC-SHA256 over
timestamp.raw_body - timestamp within five minutes
Never parse php://input, trust metadata.order_id, or update an order before the installed handler validates the request. Agentic Commerce uses the same URL but a separate secret and event family; do not treat its delegated checkout payload as an ordinary order webhook.
Stripe Gateway 10.9.0 updates the stored pending-webhook count only after signature validation succeeds. Do not write health/status counters from an unverified payload in custom webhook code. Webhook timestamps and pending counts are scalar non-negative integer state; 10.9.0 ignores invalid array/object/non-digit values instead of persisting them.
Connected-account binding in 10.9
After signature validation, the gateway compares a Connect event's account or an agentic delegated-checkout event's context with the cached connected Stripe account ID for the active mode. A known mismatch is logged, acknowledged with HTTP 200, and is not processed; it does not update pending/success health state or fire wc_stripe_webhook_received.
The check deliberately fails open when the event carries no account/context or the connected account is unknown. Do not describe the account check as a replacement for signature verification, and do not copy the fail-open policy into a custom multi-account endpoint without an explicit compatibility reason. A custom endpoint that knows its expected account should fail closed before order lookup.
Event families
The gateway handles, among others:
payment_intent.processing,.succeeded,.payment_failed,.amount_capturable_updated,.requires_actionsetup_intent.succeeded,.setup_failedcheckout.session.completed,.async_payment_succeeded,.expired,.async_payment_failed- charge success/failure/capture/refund/dispute events
- legacy asynchronous Source events
account.updated
Event delivery is not the business event. For example, payment_intent.processing can place an order On hold, while requires_action is not payment success.
Order resolution
The handler resolves orders using signed metadata plus stored Stripe identifiers, with fallbacks for intent/session/source data. It verifies the stored intent/signature where possible and uses wc_get_order() for HPOS compatibility.
Custom observers must use the resolved WC_Order|null passed by the gateway. Do not independently load metadata.order_id and assume it belongs to the event.
Stripe order meta includes intent, SetupIntent, Checkout Session, customer, source/payment method, presentment amount/currency, transaction, lock, and status flags. These are implementation details. Use WC_Order and gateway helper APIs; never query wp_postmeta or copy Stripe intent/lock meta between orders.
Deferred settlement
Stripe 10.8.3 deliberately defers common successful PaymentIntent processing through Action Scheduler. Checkout Session events are also deferred when the order or its metadata is not yet available. This protects checkout/webhook races and lets the order-received request finish storing identifiers.
Relevant action:
wc_stripe_deferred_webhook
Default PaymentIntent deferral is two minutes. A webhook that loses the order-payment lock race can retry after a shorter delay. Do not assume a 200 response means order settlement completed synchronously.
Stripe 10.9 serializes deferred PaymentIntent success and Checkout Session success against the return/3DS path with the order lock. A collision is re-queued rather than dropped, and wc_stripe_webhook_received is withheld until the retry actually processes the event. This prevents duplicate transitions while preserving the initial paid transition's stock, notes, and email side effects. The observer still does not guarantee payment success: tolerate delayed execution and inspect the final order state instead of inferring success from the event name or original delivery time.
Do not disable async processing with wc_stripe_process_payment_intent_webhook_async merely to make custom code run sooner. If changed, test redirect methods, concurrent checkout, duplicate delivery, and order locks.
Locks and idempotency
The gateway uses order payment/refund locks and Stripe API idempotency keys. lock_order_payment() returns true when the order is already locked, not when the caller acquired it; this inverted-looking contract is easy to misuse.
Do not add your own payment attempt on wc_stripe_webhook_received. Duplicate Stripe delivery, checkout return handling, and deferred jobs may all represent the same payment. Make custom side effects idempotent with a durable key such as order ID plus transaction ID/event purpose.
For fulfillment/provisioning, prefer Woo's paid-order hooks and verify the order is paid:
add_action( 'woocommerce_payment_complete', function ( int $order_id ): void {
$order = wc_get_order( $order_id );
if ( ! $order || 0 !== strpos( $order->get_payment_method(), 'stripe' ) ) {
return;
}
myplugin_provision_once( $order->get_id(), $order->get_transaction_id() );
} );
Use WCS renewal-complete hooks for subscription renewal provisioning.
Safe webhook observer
wc_stripe_webhook_received fires after that event's gateway processing step and receives:
add_action(
'wc_stripe_webhook_received',
function ( string $type, object $event, ?WC_Order $order ): void {
if ( ! $order || 'payment_intent.succeeded' !== $type || ! $order->is_paid() ) {
return;
}
myplugin_record_stripe_observation_once(
$order->get_id(),
(string) ( $event->id ?? '' )
);
},
10,
3
);
The order can be null for account events, unmatched events, or non-order flows. The action can also run later from the deferred job. Catch failures in your own integration; the gateway catches thrown Throwable, logs it, and still owns the HTTP response.
Do not use this observer as the only fulfillment signal: filter to the exact event, require the expected final order state, and make the side effect idempotent.
Adaptive Pricing and Checkout Sessions
Adaptive Pricing uses Checkout Sessions and requires healthy webhooks. Stripe 10.8 disables Adaptive Pricing when webhooks are disabled. The gateway distinguishes Adaptive Pricing sessions from agentic sessions via checkout metadata and defers early events until order metadata exists.
Stripe 10.9 exposes wc_stripe_is_adaptive_pricing_supported( true, WC_Cart|null $cart ) as a final opt-out after its own account, settings, page, subscription, pre-order, and deposit checks pass. Return false for a narrowly identified incompatible cart; it cannot force an otherwise unavailable flow on. The older wc_stripe_is_checkout_sessions_available filter was removed, so delete integrations that still depend on it.
For custom order fields:
- Store durable Woo data before redirect whenever possible.
- Do not mutate Stripe's
checkout_typeor signature metadata. - Do not create an order from an unmatched Adaptive Pricing session; the gateway intentionally refuses to route it into agentic order creation.
- Reconcile presentment currency/amount from gateway helpers rather than replacing the Woo order currency/total.
Settlement amount and currency integrity in 10.9.0
Before marking a Checkout Session paid, the gateway now compares the settlement amount/currency with the Woo order total/currency. It understands both Stripe schemas:
- before the
2025-03-31.basilAPI version, settlement values for Adaptive Pricing are undercurrency_conversion.source_currencyandcurrency_conversion.amount_total; - Basil and later, including the gateway's
2026-03-25.dahliaAPI version, use top-levelcurrencyandamount_totalfor settlement, while buyer-facing figures can live inpresentment_details.
On a missing or mismatched settlement value, the gateway refuses payment completion, writes a diagnostic order note with the Checkout Session and available PaymentIntent reference, and moves the order to on-hold. Holding prevents Woo's unpaid-pending cancellation from restoring stock for a payment Stripe may already have captured.
Treat this state as a manual-reconciliation signal. Do not auto-call payment_complete(), rewrite the order total/currency, or move the order back to pending merely because Stripe reported checkout.session.completed. Resolve the Stripe object, Woo order, API-version schema, and amount conversion first; make any operator remediation explicit and auditable.
Unexpected charges
Stripe 10.8 detects a captured Stripe charge for an order already paid by another gateway. It writes an order note, deduplicates by PaymentIntent, and fires:
wc_stripe_unexpected_charge_detected
Arguments are WC_Order $order, Stripe charge object, and webhook type. Use this for alerting/reconciliation, not automatic refunding without an explicit, idempotent business policy.
Logging and health
Use WooCommerce > Status > Logs and Stripe's webhook/status UI. Correlate Woo order ID, PaymentIntent/Checkout Session/charge ID, event type, and Action Scheduler action.
Stripe 10.8 adds wc_stripe_logger_can_log for targeted enablement when verbose debug and the global logging setting are both off. It cannot suppress logs already enabled by those settings because the logger returns true before applying the filter. The filter receives allowed flag, level, calling class, and calling function and can run frequently, so keep callbacks cheap. Never log webhook secrets, API keys, client secrets, full payloads containing customer data, or card/bank details.
Useful checks:
wp action-scheduler action list --hook=wc_stripe_deferred_webhook --status=pending
wp option get woocommerce_stripe_settings --format=json
Redact secrets before sharing option output.
Abilities API caveat
Stripe 10.8 contains read-only Stripe abilities, but registration is gated by wc_stripe_abilities_enabled and defaults to false. It also requires WooCommerce 10.9's AbilitiesLoader; on older Woo versions it no-ops. The registrar and ability classes are marked internal. Do not make payment settlement, reconciliation, or production automation depend on this surface until your integration explicitly controls the feature gate and pins the Stripe/Woo versions.
Deprecated surfaces
WC_Gateway_Stripeis deprecated; use the runtimeWC_Stripe_UPE_Payment_Gatewayonly when direct class integration is unavoidable.wc_gateway_stripe_process_paymentis deprecated since 9.7; the replacement iswc_gateway_stripe_process_payment_charge.WC_Stripe_Webhook_Handler::process_checkout_session()is deprecated since 10.6; success/failure handlers replaced it.wc_stripe_is_checkout_sessions_availablewas removed in 10.9; usewc_stripe_is_adaptive_pricing_supportedonly for final cart-specific opt-out.- Payment Request Button naming/classes are legacy; use Express Checkout terminology and current helpers.
- Gateway/webhook classes are plugin internals. Prefer documented actions and Woo order lifecycle hooks over subclassing or direct handler calls.
Test matrix
- Valid, missing, stale, and wrong webhook signatures; matching, mismatched, absent, and unknown account context.
- Duplicate event delivery and duplicate checkout/3DS return.
- PaymentIntent success before and after order meta persistence.
- Checkout Session success, async success/failure, expiration, and amount/currency mismatch across pre-Basil and Basil schemas.
- Lock collision followed by deferred retry, with the observer firing only after actual processing.
- Redirect/SCA, capture, refund, dispute, and failed payment.
- Adaptive Pricing enabled with healthy/disabled webhooks.
- Invalid webhook-state values and pending-count updates only after a valid signature.
- HPOS and Action Scheduler worker/cron delays.
Cross-references
- Use
wc-stripe-future-paymentsfor charge-and-save and later off-session PaymentIntent state machines. - Use
wc-payment-gatewayfor provider-neutral payment state and webhook design. - Use
wc-stripe-add-payment-methodfor SetupIntent token creation in My Account. - Use
wc-stripe-subscriptionsfor Stripe renewal and subscription change-payment behavior. - Use
wc-loggingandwc-action-scheduler-jobsfor general diagnostics.
References
- Verified source paths:
wp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-webhook-handler.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-webhook-state.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-order-handler.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-order-helper.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-api.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-helper.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-logger.phpwp-content/plugins/woocommerce-gateway-stripe/includes/abilities/class-wc-stripe-abilities-registrar.php