What your app calls
This page is about the part of your integration that runs in your app, using a wallet-holder token. You get one by minting a delegated token for the user.
If your backend instructs every movement on its own credential instead, your users hold no session and the operations marked User token are not part of your build.
This is the part of your integration that runs in your app, carrying the token of the person holding the wallet rather than your machine token. For the server-to-server half, see Start here.
What makes it different
Every /me/* operation resolves the caller from the token and acts only as that person. There is
no userId parameter to get wrong, and no way to ask for someone else's data. That is what makes
it safe to ship inside a mobile app.
The practical consequences:
- A machine token cannot call these. It has no wallet holder to resolve to, and is rejected.
- Withdrawals live here, not on the tenant surface. The person withdrawing must be the person asking.
GET /transactionsreturns only their activity. The same URL called with your machine token returns your whole tenant; the narrowing happens on our side.
Authentication
Auth is backend-mediated — your app talks to us, never to the identity provider directly.
POST /auth/login | email + password → access token |
POST /auth/refresh | new access token |
POST /auth/logout | end the session |
POST /auth/forgot-password · /auth/reset-password | self-service recovery |
GET /auth/password-policy | so your UI can validate before submitting |
POST /auth/invite/complete | first login after an invite — consumes the emailed token and sets the password |
Token storage. Keep the access token in memory. The refresh token differs by platform: on web
it is an HttpOnly cookie you never see; on native there are no cookies, so send X-Client: mobile
and you receive it in the response body — store it in the platform keychain
(flutter_secure_storage, Keychain, Keystore). Never localStorage.
Resolving the tenant
Before login, the app needs to know which tenant it belongs to and what to look like:
GET /public/tenants/resolve
Resolved from the host, so a white-label build serves every tenant from one binary. Returns display name, logo and brand colours.
It is not the only call that needs no token — so do GET /auth/password-policy, POST /auth/login,
POST /auth/forgot-password, POST /auth/reset-password and POST /auth/invite/complete, for the
obvious reason that a person who cannot sign in has nothing to present.
What your app can do
| Area | |
|---|---|
| Wallets | GET /wallets, balances, fund buckets |
| Capabilities | GET /me/wallet-types — which actions to show |
| Activity | GET /transactions, /{id}, /{id}/detail — their own |
| Send money | POST /transfers/peer, GET /transfers/lookup-recipient |
| Top up | POST /me/top-ups, GET /me/top-ups |
| Withdraw | /me/withdrawal-accounts, /me/withdrawals |
| Notifications | GET /me/notifications, unread count, mark read |
| Banks | GET /banks — for the payout account form |
Two things that will bite you
Withdrawals require KYC VERIFIED. Adding a payout account and requesting a payout both return
403 KYC verification required until a GoHubPay reviewer has verified the user. Neither your app
nor your backend can grant that. Build a real "verification pending" state — it is a normal
condition, not an error.
Never credit a top-up off the return URL. The user landing back in your app means the browser
navigated, not that money moved. Poll GET /me/top-ups/{id} until it reads SUCCEEDED.
The Screens → API guide maps each of these to a real screen.