Skip to main content

Top-up — money in from outside

A top-up moves real money from a card or bank into a wallet, through a payment gateway. It is the one flow with a genuinely asynchronous middle, and the most common integration mistake is treating the redirect as the answer.

Server-initiated

POST /wallets/{walletId}/top-up-link

{ "amount": 50.00, "returnUrl": "https://yourtenant-app.wallet.gohubpay.com.my/topup/done" }

Returns a hosted payment URL. Open it in the system browser or a webview; the user pays there.

Two traps on this call

It takes no Idempotency-Key. A retry is not a replay — it mints a second live payment link. Store the URL you got back and reuse it rather than calling again.

returnUrl must be an https URL on a platform host. A custom scheme like yourapp:// is not accepted, and it does not error — an unacceptable value is silently replaced with the default return page, so your app never receives the user back. For a native app, return to an https page you control the deep-linking from.

User-present

If your app already holds a wallet-user token, it can start its own top-up with POST /me/top-ups — see the Client app guide. Same settlement path.

Getting the outcome

The return URL tells you nothing

The user coming back to your returnUrl means the browser navigated. It does not mean the payment settled, and a redirect can be forged. Never credit anything off the redirect.

Today, poll.

GET /wallets/{walletId}/top-ups

Status moves CREATED → PENDING → SUCCEEDED → CREDITED (or FAILED / EXPIRED / CANCELLED). Poll no tighter than a few seconds, and give up gracefully — design a real waiting state, because most settle in seconds and some do not.

Wait for CREDITED, not SUCCEEDED

SUCCEEDED means the payment cleared at the gateway. The wallet is credited on the next transition, and only CREDITED means the money is in the balance.

A poller that stops at SUCCEEDED shows the user a balance that has not moved yet — and because CREDITED is where it settles, it never sees the state it was actually waiting for.

Outbound webhooks are not built

We do not call your endpoint when a top-up settles. Signed outbound webhooks with retries and a delivery log are designed and not yet built, so polling is the supported mechanism, not a fallback.

The webhook endpoints you may see referenced elsewhere are ones the payment gateway calls on us. They are not part of your integration and you cannot call them.

What is true afterwards

GET /wallets/{id}/top-ups shows CREDITED; the wallet balance has increased by the amount; and a balanced ledger pair exists for it. If the balance moved and the top-up still reads PENDING, that is a discrepancy worth reporting.

Next: Money out.