A real diagnosis flow for orders that stay PENDING, jump to CANCELLED, or never reach your balance after a successful Cryptomus pay_url load. Built from production webhook logs and the payment_orders schema on monkeytheme.com.
If a Cryptomus USDT top-up looks stuck, work through these four checks before opening a ticket. Each one has a concrete answer you can verify yourself.
Sign in, open /dashboard/orders, and find the order id. If the row does not exist at all, the create-order call never reached the backend, which usually means the request was made before you were signed in, or the same key was reused across browsers.
If the row exists, look at status, paid_at and expires_at:
| status | What it usually means | Next action |
|---|---|---|
| PENDING | User opened pay_url but did not finish the on-chain transfer, or the webhook has not landed yet. | Wait up to 30 minutes, then check again. Cryptomus retries webhooks. |
| COMPLETED | Webhook validated, balance was credited, order is closed. | Nothing to do. If balance did not move, file a ticket with the order id. |
| CANCELLED | User closed pay_url without paying, or the order expired. | Create a fresh order. Old one is not recoverable. |
| EXPIRED | Hit the ORDER_TIMEOUT_MINUTES (default 30) without a webhook. | Create a fresh order. If this repeats, the upstream webhook is being lost. |
| FAILED | Webhook arrived but the upstream reported failure. | File a ticket with the order id, paid_at and the cryptomus invoice id. |
The frontend bundle on monkeytheme.com (providerConfig-*.js) hard-codes one and only one notifyUrl:
cryptomus:"/api/v1/payment/webhook/cryptomus"
Anything else is wrong. From seven days of production logs we counted four distinct webhook URLs being posted to monkeytheme.com, and three of them are noise:
| Path seen | Status | Source |
|---|---|---|
| /api/v1/payment/webhook/cryptomus | 200 on real webhooks, 400 on probes | Official frontend |
| /api/v1/payment/cryptomus/webhook | 404 | Probe (path-order swap) |
| /api/v1/payment/webhooks/cryptomus | 404 | Probe (extra s) |
| /api/cryptomus/webhook | 404 | Probe (version-less) |
The 400s on the official path are all cryptomus webhook missing sign with bodyLen: 2, which means an empty {} body. That is not a real top-up, that is a scanner or a misconfigured client. A real Cryptomus webhook body always carries a JSON sign field plus the order payload.
Cryptomus hosted page will show a success state once the on-chain transfer reaches the required confirmations. Until that happens, Cryptomus will not POST the webhook. If the user closed the tab before paying, the order is still PENDING and will time out at expires_at.
You can also ask the user to copy the cryptomus invoice id from the hosted page URL (it looks like pay_url?...&orderid=... or the order id shown after redirect) and check it directly in the Cryptomus merchant dashboard under Payments.
After a successful on-chain payment, the hosted page redirects to /payment/result. The frontend polls GET /api/v1/payment/orders/my for a few seconds to detect the COMPLETED state. If the user closes that tab too early, the balance is still credited by the webhook, but the UI keeps saying PENDING until the next page load.
Fix: refresh the orders page. Do not assume the webhook is broken just because the popup closed early.
How a USDT top-up actually moves from /purchase to a credited balance on monkeytheme.com:
Step 3 is the only step where a server-side misconfiguration can silently drop a paid order. Step 4 happens inside the same webhook handler, so if step 3 returns 200, step 4 has run. If you see a 400 on step 3, step 4 never started and the user is not credited yet.
A real webhook log line on monkeytheme.com looks like this. Source: journalctl -u sub2api --since "7 days ago", captured 2026-08-08.
INFO middleware/logger.go:82 http request completed service=sub2api env=production component=http request_id=f55564df-948e-4847-8a17-d72285534880 path=/api/v1/payment/webhook/cryptomus method=POST status_code=400 latency_ms=7 client_ip=65.49.151.58 protocol=HTTP/1.1
Right above it, in the same transaction:
ERROR slog logger/slog_handler.go:53 [Payment Webhook] verify failed provider=cryptomus error="cryptomus webhook missing sign" method=POST bodyLen=2
Two things to take from this:
missing sign + bodyLen: 2 means the body was {}. That is a probe, not a real Cryptomus invoice.payment_orders update, not a verify-failed error./api/cryptomus/webhook, /api/auth/google, /api/v1/settings/public) in the same minute. If your log shows the same pattern, you are looking at a probe, not a customer.Open a ticket only after the four checks above. Send these four pieces of information so support can act on the first reply:
/dashboard/orders).paid_at from the row you already pulled.journalctl -u sub2api --since "5 minutes ago" --no-pager | grep -E "orderid=YOUR_ORDER_ID|payment/webhook/cryptomus|verify failed"Do not send screenshots of the wallet address. Do not send your account password. Do not paste the API key. The order id and invoice id are enough to look up the row.
/api/v1/payment/webhook/cryptomus. Other paths such as /api/v1/payment/cryptomus/webhook, /api/v1/payment/webhooks/cryptomus (with the extra s) and /api/cryptomus/webhook are not registered routes; they return 404 even though production logs sometimes see requests hit them.
It means the body Cryptomus posted did not include a sign field, or the body was empty. From the production log on 2026-08-02 the failing POSTs were all bodyLen=2 (literally {}). Real Cryptomus webhooks always carry a JSON body with a sign field, so an empty body is not a real top-up, it is either a probe, a misconfigured client, or a proxy that strips the body.
Not necessarily. The webhook path is only one half of the flow. Cryptomus usually retries the webhook when the first attempt fails; it also offers a manual resend from the merchant dashboard. The other half is the returnUrl hop: after paying, the user is bounced back to /payment/result, which the frontend polls for status. If the user closes the tab before the return hop, the order may sit PENDING until expiry (default 30 minutes via ORDER_TIMEOUT_MINUTES).
This page is about the post-payment side: did the webhook arrive, did the order move to COMPLETED, is the balance credited. It is not about choosing a wallet, picking a network, or estimating gas. Those have their own decisions and their own risks; the upstream changes them faster than we can keep docs in sync, so link to Cryptomus docs instead of copying numbers that go stale.