Skip to content

[8.1.10] REST webhook answers 200 when the payment status can't be fetched, so Mollie stops retrying and paid orders stay pending #1304

Open

Description

@terminalstate

What happens

A Mollie webhook carries only the payment id, and the plugin then fetches the status from the API. On the default REST webhook URL (/wp-json/mollie/v1/webhook), when that fetch fails, the callback still answers 200. The fetch can fail on a timeout, on a 5xx or 429 from the API, or on a DNS or TLS problem on the store's server. Mollie then treats the webhook as handled and doesn't retry it, so the paid order stays in Pending payment.

The WC-API webhook answers 400 in the same situation (MollieOrderService.php#L137-L138). The REST callback's own fallback branch answers 500 for an ApiException (RestApi.php#L172-L174). Only the main branch acknowledges the failure.

Where (dev/develop @ 4d75849, 8.1.10)

Reproduction

I ran both webhook entry points with the plugin's own code: from RestApi::callback() and onWebhookAction() down to WordPressHttpAdapter and mollie-api-php v2.79.1. WordPress and WooCommerce were stubbed, and wp_remote_request() played the Mollie API:

Mollie API answer REST webhook WC-API webhook Order afterwards
200, payment paid 200 200 processing
timeout (cURL error 28) 200 400 pending
503 200 400 pending
429 200 400 pending
404 200 400 pending

The only trace is two debug lines: Could not load payment tr_… (live): … cURL error 28 …, then doPaymentForOrder: payment tr_… not found.

Why it matters

Mollie retries a webhook up to 10 times over about 26 hours until it gets a 200 (docs). A non-200 here would let timeouts, rate limits and short outages fix themselves on a later attempt. With a 200, the only second chance is the check before WooCommerce cancels an unpaid order (GatewayModule.php#L216-L232). That check runs only when stock management is on, and if its request fails too, a paid order is cancelled. #792 shows the same symptom: cURL error 6: Could not resolve host: api.mollie.com, and paid orders left waiting for payment.

Suggested fix

Answer non-200 (e.g. 503) when the status could not be fetched, and keep 200 for ids Mollie doesn't know, as the docs recommend. doPaymentForOrder() also returns false for other reasons, for example a status without a handler, so the callback needs to know why. One way is to keep the ApiException code (0 for transport errors, the HTTP status otherwise) instead of turning it into null.

Happy to open a PR if this direction works for you. Longer write-up: https://github.com/terminalstate/callback-audit/blob/main/docs/case-mollie-woocommerce-200.md

Activity

  1. fjbender commented on Sep 28, 2026

    @fjbender
    Contributor

    Thanks for your report! This seems an oversight when introducing the new wp-json route. As this is a pretty delicate part of the system, we'll create a patch on our end and have to go through QA in one go - but thanks for your offer to provide a PR.

    We're tracking this as PIWOO-951

  2. terminalstate commented on Sep 28, 2026

    @terminalstate
    Author

    Thanks, Florian, sounds good. If it helps QA: the harness in https://github.com/terminalstate/callback-audit/tree/main/repro/mollie-woocommerce runs the plugin's own webhook code against a fake Mollie API, on both webhook entry points, for a timeout, a 503, a 429 and an unknown payment id. Once the fix is public, I'm happy to run it against the patch and post the results here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions