Building the Trading section of the StarPay app

This guide follows one person — call her Ama — from the moment she downloads StarPay to the moment she sells USDT and the cedis land in her wallet. At every step it tells you which endpoint to call, what you get back, and what to do when something goes wrong.

You are integrating against two backends, and knowing which is which will save you a lot of confusion:

What it is Base URL
StarPay Payments Ama's StarPay account: signup, sign-in, her Ghana Card, her payments wallet https://starpaygh.com
StarPay Trading Buying and selling stablecoin: quotes, trades, the trading wallet https://trading.starpaygh.com

They share one thing only: who Ama is. Everything else is separate — separate databases, separate wallets, separate balances. The two are the same company and the same licence, so this is an engineering boundary, not a legal one.

The single most useful thing to internalise: Ama has two wallets. Her payments wallet (for sending money, paying bills) and her trading wallet (which funds a buy and receives the proceeds of a sell). Money does not move between them automatically. If your UI shows one number labelled "balance", people will be confused and support will hear about it.


Chapter 1 — Ama signs up

Nothing special here. You call the payments backend exactly as you do today:

POST https://starpaygh.com/api/v1/me/signup/start
{ "email": "ama@example.com" }

POST https://starpaygh.com/api/v1/me/signup/verify
{ "email": "ama@example.com", "code": "<emailed code>", "password": "…",
  "device": { "install_id": "<your app's per-install id>", "platform": "ios" } }

You get back an access token (cat_…) and a refresh token (crt_…). That is her StarPay session, and it is what every payments call uses. Returning users sign in with POST /api/v1/me/login.

Keep that install_id. Generate it once per installation and reuse it everywhere — you will send the same value again in Chapter 3, and the trading side refuses a pass presented from a different one.

On dev the code really is emailed. There is no fixed 123456 on the payments backend's dev stage, so test with an inbox you can read.

What you don't see: in the background, StarPay Payments quietly registers Ama on the trading side too. She now has a trading account without ever being asked to create one. If that registration fails — trading is down, say — her signup still succeeds, and StarPay Payments simply tries again the first time she taps into Trading (Chapter 3). You don't have to handle the retry; you only have to show the message you get back if it still can't be done.

It never matters that Ama already uses the same email somewhere else at StarPay — as a merchant's teammate, say. Her trading account from the app is always its own.


Chapter 2 — Ama verifies her Ghana Card

Also unchanged, and also on the payments backend:

POST https://starpaygh.com/api/v1/me/kyc/ghana-card
{ "card_number": "GHA-123456789-0", "image": "<base64 selfie>" }

Behind the scenes, once NIA confirms she is who she says she is, the result is forwarded to the trading side — the method, the reference and the timestamp, signed so it cannot be forged. Her photo never leaves the payments backend.

Why you care: this is what unlocks trading. Before it, Ama can sign in to the Trading section but cannot trade — she's Tier 0, and Tier 0 has a hard block on it. After it, she is Tier 1 and can trade up to USD 5,000 a month.

One Ghana Card check. Both products. Do not build a second verification screen for trading. Order doesn't matter either: if her trading account is only set up after she verified — the retry in Chapter 1 — the result travels with it, and she arrives at Tier 1.


Chapter 3 — Ama opens the Trading section

This is the only genuinely new bit of plumbing, and it is two calls.

Step 1 — ask Payments for a pass.

POST https://starpaygh.com/api/v1/me/trading/session
Authorization: Bearer cat_…              ← her normal StarPay session
{ "install_id": "<your app's per-install id>" }
{ "status": true, "data": {
    "assertion": "spa1.eyJ…",
    "expires_in": 60,
    "exchange_url": "https://trading.starpaygh.com/api/v1/mobile/auth/exchange"
} }

Step 2 — trade it for a trading session.

POST {exchange_url}
{ "assertion": "spa1.eyJ…", "install_id": "<the same install_id>" }
{ "status": true, "data": {
    "access_token": "tat_…",  "expires_in": 900,
    "refresh_token": "trt_…", "refresh_expires_in": 2592000,
    "tier": 1, "trading_enabled": true
} }

From here on, every trading call uses Authorization: Bearer tat_…. Ama never typed a second password and does not know any of this happened.

When Payments can't give her a pass

Step 1 is also where StarPay Payments quietly finishes setting Ama up if her trading account didn't make it at signup. So it can fail in a few ways, and each one wants something different from you:

What you get What it means What to show
404 not_found Trading isn't switched on for this stage Hide the Trading section
502 provider_unavailable Trading can't be reached right now The message, and a Try again button. Retrying in a moment is the right move
403 permission_denied with details.requirement: "support" She can't be set up for trading automatically — a person has to look at it The message, and a way to contact support. Don't retry — it won't change
400 validation_error The install_id you sent is bad Fix your call; this one is on you

The messages are written for Ama. They say what to do next and never explain why — show them as they are.

Things that will bite you if you skip them

  • install_id must be the same in both calls. It is baked into the assertion's signature. This stops a copied assertion opening a session on someone else's phone.
  • The assertion is good for about 60 seconds and works exactly once. Don't cache it. Don't retry an exchange with the same one — get a fresh pass.
  • Do it lazily, when Ama taps into the Trading section, not at app launch.
  • trading_enabled: false is normal, not an error. It means Tier 0. Send her to the Ghana Card screen (Chapter 2), not to a dead end.

Keeping the session alive

The access token lasts 15 minutes. When it expires:

POST https://trading.starpaygh.com/api/v1/mobile/auth/refresh
{ "refresh_token": "trt_…" }

You get a fresh pair, and the old pair stops working immediately — so store the new one before you use it.

⚠️ Never reuse a refresh token. Each one works once. If you send a token that has already been rotated, we assume it has been stolen — because two parties now hold it — and we end the whole session. Ama gets signed out of trading. The usual cause is a race between two refresh calls, so serialise them.

If a refresh fails, go back to Chapter 3 Step 1. You always can: her StarPay session is the source of truth.


Chapter 4 — Ama looks around

Three read-only calls to build your home screen. All take Bearer tat_….

GET /api/v1/mobile/limits      → tier, monthly cap (USD), used, remaining
GET /api/v1/mobile/wallet      → her TRADING cedi balance
GET /api/v1/mobile/orders      → her 50 most recent trades

limits is the one to show prominently. monthly_remaining is what she can still trade this month; monthly_cap is null only for uncapped accounts.

You can also show live prices without signing anyone in:

GET /api/v1/public/rates       ← no auth, poll every ~10s

Chapter 5 — Ama puts money in her trading wallet

Her payments balance can't fund a trade. She tops the trading wallet up directly:

POST /api/v1/mobile/wallet/topup
Idempotency-Key: <a UUID you generate>
{ "amount": 50000, "channel": "momo", "instrument": "0244123456" }

amount is in pesewas — 50000 is GHS 500.00. Every money amount in both APIs is an integer in minor units. There are no decimals anywhere; if you see one, something has gone wrong.

A pending status is normal — mobile money settles asynchronously. Poll GET /api/v1/mobile/wallet until the balance moves.

Idempotency-Key on every money call. Generate a UUID per user action, not per HTTP attempt. If the network drops and you retry with the same key, you get the original result back instead of a second charge. This is your protection against double-spending on a flaky connection, and it only works if you reuse the key across retries of the same tap.


Chapter 6 — Ama buys USDT

Step 1 — get a price.

POST /api/v1/mobile/quote
{ "side": "buy", "asset": "USDT", "network": "TRC20", "fiat_amount": 50000 }

Send either fiat_amount (spend this many pesewas) or asset_amount (buy this much USDT) — never both.

{ "data": { "reference": "qte_…", "applied_rate": "12.6250",
            "asset_amount": 3960000, "fiat_amount": 50000,
            "fee_amount": 0, "net_fiat_amount": 50000,
            "expires_at": "2026-09-22T10:00:45Z" } }

The quote is good for 45 seconds. Show a countdown. When it runs out, get a new one — don't try to execute an expired quote.

Step 2 — execute it.

POST /api/v1/mobile/orders
Idempotency-Key: <UUID>
{ "quote_reference": "qte_…", "delivery_address": "TR7NHqje…" }

A buy settles immediately: her trading wallet is debited and the USDT is sent to delivery_address.

⚠️ The delivery address is the dangerous field

There is no undo. Coins sent to the wrong address are gone — no support ticket recovers them, and no one can reverse it.

We check the address's checksum and reject anything that fails, which catches typos and truncated pastes. But we cannot tell whose address it is. A valid address belonging to a stranger looks exactly like a valid address belonging to Ama.

So please:

  • Offer paste and QR scan. Both beat typing.
  • Show the address back in a large, readable font before she confirms.
  • Make the network unmistakable. A TRC20 address is not an ERC20 address, and sending to the wrong network loses the coins. We reject a mismatched one — but your UI should never offer the combination in the first place.
  • Consider remembering addresses she has used before.

If the address is malformed you get 400 validation_error with a message written for her, not for you — show it as-is:

"That TRON address failed its checksum. Copy it again — one wrong character sends the coins somewhere unrecoverable."


Chapter 7 — Ama sells USDT

A sell runs the other way: she sends coins to us, we send her cedis.

Step 1 — quote, exactly as for a buy but "side": "sell".

Step 2 — open the order.

POST /api/v1/mobile/orders
Idempotency-Key: <UUID>
{ "quote_reference": "qte_…" }

No delivery_address — she is sending to us.

{ "data": { "reference": "ord_…", "status": "pending_payment",
            "deposit_address": "TXk…", "deposit_network": "TRC20",
            "asset_amount": 5000000 } }

Step 3 — she sends the coins. Show deposit_address as a QR code and as copyable text, with the exact amount beside it. Tell her plainly:

  • Send exactly that amount.
  • Send on that network.
  • The address belongs to this trade only — it is not a reusable wallet address, and she should not save it.

Step 4 — poll until it settles.

GET /api/v1/mobile/orders/{reference}

Poll every few seconds while she is on the screen. This endpoint does more than read a database — it actively asks our custody provider whether her coins have arrived, so polling it is often what makes the trade complete. It is throttled server-side, so you cannot poll it too hard.

Watch status:

status What to show
pending_payment "Waiting for your coins"
funded "Coins received, completing your sale"
settled Done — cedis are in her trading wallet
suspended Her coins arrived but the sale is held. Support, not a retry.
failed / expired Nothing was received. She can quote again.

What if she sends the wrong amount?

  • Too little — the order stays pending_payment and we keep her coins safely. She gets an SMS. She should quote again for the amount she actually sent.
  • Too much — the trade completes at the quoted amount and the surplus is held for our team to return. She gets an SMS.

Either way, her money is not lost. Say so in your copy; this is the moment people panic.


Chapter 8 — Ama takes her cedis out

POST /api/v1/mobile/wallet/withdraw
Idempotency-Key: <UUID>
{ "amount": 30000, "channel": "momo", "instrument": "0244123456", "pin": "4821" }

A withdrawal needs her transaction PIN. This is the one call that does, and deliberately: a stolen session token would otherwise be enough to empty her wallet to any phone number. The session says who is calling; the PIN says she is here.

It is the same PIN she uses everywhere in StarPay

Ama has one transaction PIN, and it lives in StarPay settings. The Trading section has no PIN screen of its own — don't build one.

She sets, changes and resets it on the payments backend, exactly as she would for a payment:

POST https://starpaygh.com/api/v1/me/pin
{ "pin": "4821" }                          ← first time
{ "current_pin": "4821", "pin": "9999" }   ← changing it

POST https://starpaygh.com/api/v1/me/pin/reset/start     ← forgot it: emails a code
POST https://starpaygh.com/api/v1/me/pin/reset/verify    ← the code + her new PIN

Behind the scenes StarPay Payments passes the change on to the trading side — signed, and as a scrambled hash, never the PIN itself — and it applies to trading within seconds. A reset there also lifts a trading lockout.

So when she taps Withdraw, just ask for "your StarPay PIN" and send it as pin. Two answers mean "she has to go to StarPay settings first":

What you get What it means What to do
400 validation_error with details.requirement: "pin" She hasn't set a PIN yet Show the message and a button to StarPay's PIN screen
403 permission_denied with details.requirement: "starpay_pin" You called POST /api/v1/mobile/pin — trading won't set her PIN Send her to StarPay's PIN screen. Better: never call it

If she has only just set her PIN and the withdrawal still says requirement: "pin", give it a few seconds and let her try again — the change is on its way across.

4–6 digits. After 5 wrong attempts the PIN locks for 15 minutes and you get 422 limit_exceeded — note that this is not a 401, so do not sign her out over a mistyped PIN. Resetting her PIN in StarPay settings unlocks it straight away.

After a reset, buys and withdrawals pause for a day

Say Ama forgets her PIN and resets it in StarPay settings (or resets her password). StarPay treats that as the moment someone else is most likely to be holding her account, so for the next 24 hours money can't leave it — and the trading side honours the same pause:

  • Paused: withdrawals (Chapter 8) and buys (Chapter 6 — the coins go to an outside address, so a buy is money leaving too).
  • Still working: sells (Chapter 7) and top-ups (Chapter 5). Those only bring money to her.

A paused call answers:

{ "status": false, "error": {
    "code": "permission_denied",
    "message": "For your security, withdrawals and buys are paused until 9 Oct 2026, 09:14 GMT after a recent PIN or password reset.",
    "details": { "requirement": "security_hold", "until": "2026-10-09T09:14:03.512204+00:00" } } }

Show the message, and use details.until to tell her when it lifts — a countdown, or "back on tomorrow at 09:14". Don't retry in a loop and don't send her to support: nothing is wrong, and it ends on its own. A buy can be refused at the quote or when she confirms it, if the reset happened in between — handle it in both places.


Errors: the whole list

Every response is { "status": …, "message": …, "data": … } or { "status": false, "error": { "code", "message", "details" } }.

Code HTTP What it means What to do
validation_error 400 Something in the request is wrong. With details.requirement: "pin": she has no PIN yet (Chapter 8) Show message — it is written for her. For "pin", add a button to StarPay's PIN screen
authentication_failed 401 Session expired or invalid Refresh; if that fails, re-exchange
compliance_block 403 Not verified, or held for review Send her to verification, or to support
permission_denied 403 Getting a pass (Chapter 3): she can't be set up for trading automatically — details.requirement is "support". /mobile/pin (Chapter 8): details.requirement is "starpay_pin" — her PIN is managed in StarPay settings. A buy or a withdrawal (Chapter 8): details.requirement is "security_hold" — paused after a reset until details.until "support": show message and a way to reach support, don't retry. "starpay_pin": send her to StarPay's PIN screen. "security_hold": show message and when it lifts; sells and top-ups still work
not_found 404 No such trade/quote for her Don't retry
rate_expired 409 The 45s quote lapsed Get a fresh quote
idempotency_conflict 409 Same key, different body Fix the key; never reuse one across actions
insufficient_funds 422 Her wallet can't cover it Offer a top-up
limit_exceeded 422 Tier cap, or PIN locked Show the cap, or the lockout
rate_limited 429 Too fast Back off and retry
server_error 500 Ours Retry once, then apologise
provider_unavailable 502 Getting a pass (Chapter 3): Trading can't be reached right now Show message and let her try again in a moment

message is written to be shown to a person. Please don't replace it with your own copy — it usually says what to do next.


A checklist before you ship

  • [ ] Two balances, clearly labelled. Never one number called "balance".
  • [ ] Amounts are integer minor units everywhere. Never send a decimal.
  • [ ] A fresh Idempotency-Key per user action, reused across retries of it.
  • [ ] Refresh calls serialised — never two in flight at once.
  • [ ] A visible 45-second countdown on every quote.
  • [ ] The delivery address confirmed on a screen of its own, big enough to read.
  • [ ] Network shown everywhere coins move, and impossible to mismatch.
  • [ ] trading_enabled: false routes to verification, not to an error.
  • [ ] A pass refused with requirement: "support" offers support, not a retry loop.
  • [ ] A PIN-locked message that does not look like being signed out.
  • [ ] No PIN screen in Trading: requirement: "pin" opens StarPay's PIN settings.
  • [ ] requirement: "security_hold" shows when buys and withdrawals resume (details.until), on the quote and the confirm.
  • [ ] "Your money is safe" copy for suspended and for under/over-payment.

Quick reference

StarPay Payments — https://starpaygh.com, Bearer cat_…

Method Path Why
POST /api/v1/me/signup/start · /me/signup/verify Create her account
POST /api/v1/me/login Sign a returning user in
POST /api/v1/me/kyc/ghana-card Verify her — unlocks trading too
POST /api/v1/me/trading/session Get a pass into Trading
POST /api/v1/me/pin Set or change her PIN — for trading too
POST /api/v1/me/pin/reset/start · /me/pin/reset/verify Reset a forgotten PIN

StarPay Trading — https://trading.starpaygh.com, Bearer tat_…

Method Path Why
POST /api/v1/mobile/auth/exchange Pass → trading session
POST /api/v1/mobile/auth/refresh Keep it alive
GET /api/v1/mobile/limits Tier, cap, remaining
GET /api/v1/mobile/wallet Trading cedi balance
POST /api/v1/mobile/wallet/topup Put cedis in
POST /api/v1/mobile/wallet/withdraw Take cedis out (PIN)
POST /api/v1/mobile/quote Price a trade (45s)
POST /api/v1/mobile/orders Execute it
GET /api/v1/mobile/orders History
GET /api/v1/mobile/orders/{ref} One trade + poll a sell
GET /api/v1/mobile/kyc Verification status
GET /api/v1/public/rates Live prices, no auth