Troubleshooting

The gateway failures that look like something else — localhost checkout links, currency errors from the wrong list, merchants who cannot make keys, webhooks that never verify, and balances that never become money.

8 min readUpdated 3 August 2026troubleshooting, support, webhooks, payouts

Most gateway tickets are one of nine things. They are grouped by who reports them, because the symptom a merchant describes and the cause on your server are usually in different places.

Nothing responds at all

The extension row is off, or the backend has not been restarted since you turned it on. Routes are registered at boot. Check Extensions → Payment Gateway, then pnpm restart.

The route registered but the licence check failed. /api/gateway and /api/admin/gateway are both covered by the gateway extension licence. Check the activation under Extensions, and check that the server can reach updates.mashdiv.com — licence validation is an outbound HTTPS call with a 72-hour grace period, so a firewall change takes three days to show up here.

Same cause. The navigation entry is gated on the gateway extension being enabled.

The pages and their data endpoints are gated separately. /admin/gateway/payout in particular has no page-level permission, so a user without view.gateway.payout gets the layout with nothing in it rather than a 403. Grant the whole set from Install.

"Checkout is broken"

APP_PUBLIC_URL is unset. The API returns 201, the merchant redirects, and the buyer's browser tries to open a page on their own machine. Nothing errors server-side and there is no log line.

APP_PUBLIC_URL="https://exchange.example.com"

Restart the backend. Existing pending payments keep the bad URL stored on the row — the merchant must create fresh sessions.

APP_DEFAULT_LOCALE is not in .env.example and defaults to en. Set it explicitly if your install's default locale differs.

Three possible causes, in order of likelihood:

  1. They are not signed in. The wallet list requires an authenticated user.
  2. They hold no balance in any enabled currency. The checkout can only spend wallets that exist on this platform — there is no card rail.
  3. gatewayAllowedWalletTypes is empty or does not include anything they hold. Check the Wallets tab in the gateway settings.

ECO pricing goes through the ecosystem matching engine. Without the Ecosystem addon installed and running, the price resolves to zero and the payment is refused. Either install Ecosystem or remove ECO from the wallet map.

Not a fault. The buyer's quote is checked against a freshly computed rate with a 2% tolerance; a volatile market or a page left open crosses it. The payment stays PENDING and the buyer can retry. This is deliberately not reported to the merchant as payment.failed.

"The API rejects everything"

The merchant's status is not ACTIVE. Approve it on the merchant screen. Note that a brand-new merchant already holds four working-looking API keys — they were minted at registration and are refused until approval.

Different field. verificationStatus must read VERIFIED before a merchant can create additional keys. Approving status does not set it. Use the Verify action.

A pk_ key was used on an endpoint that writes. Only sk_live_ and sk_test_ can create payments, cancel them or refund.

The key carries an allowlist and the caller is not on it. Client IP is read from x-forwarded-for (first entry), then x-real-ip, then cf-connecting-ip. If your reverse proxy sets none of these the allowlist rejects everything — including calls from the right address. Also note the CIDR arithmetic is IPv4-only; an IPv6 range only matches as an exact string.

The currency is missing from the merchant's allowedCurrencies. Edit the merchant.

The currency is missing from the platform's gatewayAllowedWalletTypes map, or is listed under a different wallet type. Edit the gateway settings. Codes are compared literally after upper-casing, and a currency enabled for FIAT is not enabled for SPOT.

Two different ceilings. The first is the merchant's own transactionLimit, the second is the platform's gatewayMaxPaymentAmount. Both are compared directly against the payment amount in the payment's own currency, despite being labelled USD.

Almost always a test/live key mismatch. A sk_test_ key cannot see a live payment and returns 404 rather than 403, deliberately. Call GET /v1/validate and check the mode field.

The create path ends in /create. The in-app reference at /gateway/docs prints the shorter form on its overview panel; it is wrong.

"We never get webhooks"

The webhook destination is a field on each create-payment call. There is no merchant-level webhook setting: the webhookUrl on an API key and on the merchant profile are both accepted, stored and never read. Add webhookUrl to the create request.

Three classic mistakes. The signed string is <timestamp>.<raw body>, not the body alone. The header value carries a sha256= prefix that is part of the comparison. And the body must be hashed exactly as received — re-serialising the JSON changes the bytes. The key is the merchant's webhookSecret, not an API key. Recipe in Webhooks.

Five attempts, then FAILED forever. Backoff is 1 minute, 5 minutes, 30 minutes, 2 hours, 24 hours. The retry attempts and delay fields on the settings screen are inert — those numbers are fixed in code.

processGatewayWebhookRetries is a cron job. If the cron worker is not running, one minute of downtime on the merchant's endpoint loses the event permanently. Check Admin → System → Cron.

The merchant's webhookSecret could not be resolved. An unsigned webhook is never sent, because a receiver cannot tell one apart from a forgery. Check the merchant row.

By design, effectively: the plugin does not send a webhookUrl when it creates a payment, so nothing is ever delivered to it. Orders reconcile through the return handler and an hourly WordPress cron sweep instead. See WooCommerce.

"Where is my money"

Check the cron worker first — processGatewayPayouts is what proposes payouts, hourly. Then check three things on the merchant: is the balance above their payoutThreshold; is their status still ACTIVE (the job only looks at active merchants); and is gatewayEnabled on, because the job skips its entire run when it is off while payments keep crediting pending.

That is the design. A payout sits at PENDING until an admin approves it. There is no auto-approval for any schedule, including INSTANT. Work the queue at /admin/gateway/payout.

INSTANT means the payout is proposed on the next hourly run rather than waiting for a period boundary. It still waits for approval.

The balance was refunded after the payout was proposed. The error names both figures. Reject that payout — the next run will propose the correct, smaller amount.

Rejecting blocks that payout for that period; it does not take the money back. The funds stay in pending and a later period proposes them again. That is intentional. To withhold funds, suspend the merchant.

Refunds are drawn from the merchant's pending balance. If the money was already paid out and they hold nothing else in that currency, the transaction rolls back and nobody is refunded. Keep the payout SLA long enough that your refund window closes first.

Things that are not broken

A short list of behaviour that gets reported as a bug and is not.

  • Daily and monthly limits do nothing. They are stored, displayed and never enforced. Only the per-payment transactionLimit and the platform min/max bite.
  • Global Test Mode does nothing. The API key prefix decides the mode.
  • The webhook retry settings do nothing. Five attempts on a fixed ladder.
  • The checkout design gallery does not save. It is a preview tool; the live design comes from the gatewayCheckoutDesign setting and defaults to Dark Premium.
  • BIWEEKLY and MANUAL in the payout schedule dropdown are not implemented. The job understands INSTANT, DAILY, WEEKLY and MONTHLY.
  • The admin settings screen needs edit.settings to save, not edit.gateway.settings.
  • API keys cannot be shown again. They are stored as SHA-256 hashes with four characters of tail. Rotation is the only recovery.

Getting a clean diagnosis fast

  1. Validate the key. GET /api/gateway/v1/validate reports the merchant, its status, the mode and the key's permissions in one call. It separates a key problem from a request problem immediately.

  2. Read the error body, not the status. Every failure returns { "message": …, "statusCode": … }, and the message names the specific list, limit or state that refused you.

  3. Check the payment row in the admin payment screen. Status, test flag, allocations and the stored checkoutUrl and webhookUrl are all there.

  4. Check the webhook rows. Every attempt records what you sent, the status the endpoint returned and its response body. "We never got it" is answerable in one look.

  5. Check the cron category. Three of the six most common complaints on this page are a stopped cron worker.