Skip to main content

Money out — withdrawals

The chapter most likely to differ from what you expect, so it opens with the constraint rather than the endpoints.

The withdrawal endpoints live on the /me/* surface and need a wallet-holder token. A machine token is rejected on them.

That is a statement about tokens, not about people, and the difference matters. You can obtain a wallet-holder token for any user you have registered, through POST /auth/delegated-token — that is what a headless integration is for. So the endpoint requires a user token; it does not require the user.

Nothing yet proves the customer agreed

There is no OTP and no step-up on money-out today. At the moment a payout is authorised, nothing distinguishes a token your backend minted from one the customer logged in for.

The design exists and the schema is built; the enforcement is not. Until it ships, the control that a customer actually asked for a withdrawal is yours, and a disputed payout has no evidence on our side that they did. Under the e-Money Policy Document the burden of proving the customer authorised a payment sits with the issuer, so this is a live commercial exposure and not a theoretical one. Raise it before you go live on a self-service payout flow.

And the wallet holder must be KYC VERIFIED. That is not configurable, and only a GoHubPay reviewer can grant it — see Signup. Until then, both adding a payout account and posting a withdrawal return 403 KYC verification required.

What your app does

With a wallet-user token (documented in the Client app guide):

POST /me/withdrawal-accounts/enquiryconfirmation-of-payee before saving
POST /me/withdrawal-accountssave a bank or DuitNow destination
GET /me/withdrawal-accountslist saved destinations
POST /me/withdrawalsrequest the payout
GET /me/withdrawalsstatus

What your backend can see

Read-only visibility across your tenant, plus one intervention:

GET /admin/withdrawalsevery withdrawal in your tenant
GET /admin/withdrawals/{id}one, with its gateway detail
GET /admin/withdrawal-accountssaved destinations across your users
POST /admin/withdrawals/{id}/reverserelease a stuck withdrawal — one still CREATED, PENDING or MANUAL_REVIEW — marking it FAILED and returning the hold
These paths are likely to move

The /admin prefix on these is a historical misnomer — they are yours to call, not platform-only. We intend to move them to unprefixed paths before general availability, without an alias at the old location. Pin the operation, not the string, and watch the changelog.

Reverse is for stuck, not failed

A payout that has already failed returned its hold automatically — there is nothing left to reverse, and calling it does nothing. The endpoint is for a withdrawal that never reached a terminal state. On a succeeded one it is refused with 422.

How a withdrawal behaves

Funds are held when the withdrawal is created — debited immediately, so the user cannot spend them twice while the payout is in flight. Then:

OutcomeWhat happens to the hold
SUCCEEDEDKept. The money has left
FAILEDReversed automatically — funds return to the wallet
PENDING / MANUAL_REVIEWKept while it resolves

Every one of those transitions writes ledger entries, so a withdrawal is reconcilable exactly like any other movement.

Limits apply, and they are the same for everyone

A payout must be at least RM 1.00 and at most RM 5,000.00, with a rolling 24-hour ceiling of RM 10,000.00 per user. Breaching any of them is a 422.

These are platform-wide constants today, not per-tenant settings — if your business needs different numbers, that is a conversation to have before launch rather than a value you can configure.

What is true afterwards

The withdrawal reaches a terminal status; the wallet balance reflects the hold (kept or returned); and GET /transactions/by-reference ties the whole thing back to your own record of it.

Next: eKYC onboarding for where identity verification is heading, or the Go-live checklist.