Testing in Postman
This guide drives every StarPay flow end-to-end from Postman: mobile-money collection, mobile-money & bank payout, name enquiry, and stablecoin deposit & payout. Run it against the sandbox first — the Simulator gives deterministic outcomes with no real money or providers.
1. Import the collection
A Postman collection is generated from the live OpenAPI schema, so it always mirrors the current contract:
make postman # writes docs/starpay.postman_collection.json
In Postman: Import → File → docs/starpay.postman_collection.json. Every
endpoint appears with its request body. (You can also import the raw schema from
GET /api/schema/.)
2. Create an environment
Add a Postman Environment with these variables:
| Variable | Example | Notes |
|---|---|---|
base_url |
https://api-sandbox.starpay.app |
No trailing slash, no stage path |
secret_key |
sk_test_… |
Your API secret key (sk_test_ sandbox / sk_live_ prod) |
signing_secret |
whsec_… |
The merchant signing secret (money-out HMAC) — distinct from the API key |
secret_key authenticates every request; signing_secret signs money-out
requests (see step 4). Find both in the admin/merchant console.
3. Auth & idempotency headers
Set these on the collection (Collection → Authorization / Headers) so every request inherits them:
Authorization: Bearer {{secret_key}}- On money-moving
POSTs, addIdempotency-Key: {{$guid}}— Postman's$guidmints a fresh UUID per send, so replays are safe. Re-sending the same key returns the original response; the same key with a different body returns409 idempotency_conflict.
4. HMAC signing (money-out only)
POST /transfers, POST /treasury/payouts/crypto, and
POST /treasury/payouts/crypto-from-ghs additionally require an HMAC signature.
The server verifies:
X-Starpay-Signature = HMAC_SHA256(signing_secret, "{ts}.{METHOD}.{path}.{sha256(body)}")
X-Starpay-Timestamp = {ts} # unix seconds; must be within 300s of server time
{path} is the API path only — e.g. /api/v1/transfers (no host, no stage
prefix). Paste this Pre-request Script on the signed requests (or on a folder
that groups them):
const ts = Math.floor(Date.now() / 1000).toString();
const method = pm.request.method.toUpperCase();
const path = pm.request.url.getPath(); // e.g. /api/v1/transfers
const raw = (pm.request.body && pm.request.body.raw) || "";
const body = pm.variables.replaceIn(raw); // resolve {{vars}} so the hash matches what is sent
const bodyHash = CryptoJS.SHA256(body).toString(); // hex
const message = `${ts}.${method}.${path}.${bodyHash}`;
const signature = CryptoJS.HmacSHA256(message, pm.environment.get("signing_secret")).toString();
pm.request.headers.upsert({ key: "X-Starpay-Timestamp", value: ts });
pm.request.headers.upsert({ key: "X-Starpay-Signature", value: signature });
If you get
401 signature_invalid, it's almost always one of: a body that still contains unresolved{{variables}}(the script above resolves them), abase_urlthat includes a path/stage prefix, or clock skew > 5 minutes.
5. Sandbox magic values
The Simulator resolves outcomes deterministically:
| Input | Outcome |
|---|---|
MoMo 0240000000 |
success |
MoMo 0240000001 |
insufficient funds; no match for name enquiry (404) |
MoMo 0240000002 |
timeout then success (proves provider failover) |
Card 4084 0840 8408 4081 |
success |
Card 4084 0840 8408 4082 |
declined |
6. Run the flows
Name enquiry (read-only — start here)
POST {{base_url}}/api/v1/resolve/momo-name → { "msisdn": "0240000000" } →
200 with data.name. Try 0240000001 to see the 404 not_found path.
POST {{base_url}}/api/v1/resolve/bank-name →
{ "account_number": "1234567890", "bank_code": "GCB" }.
Mobile-money collection
POST {{base_url}}/api/v1/charges (Bearer + Idempotency-Key):
{ "amount": 11050, "currency": "GHS", "channel": "momo", "msisdn": "0240000000" }
Returns pending_provider (or success on the Simulator's sync path). Fetch
state with GET /api/v1/charges/{id}.
Bank payout (signed)
POST {{base_url}}/api/v1/transfers (Bearer + Idempotency-Key + HMAC script):
{
"amount": 20000, "currency": "GHS", "channel": "bank",
"account_number": "1234567890", "bank_code": "GCB", "bank_account_name": "Ama Owusu"
}
Swap to a MoMo payout with { "channel": "momo", "msisdn": "0540000000" }.
Insufficient balance returns 422 insufficient_funds — fund the wallet first via
a collection.
Stablecoin deposit address
POST {{base_url}}/api/v1/treasury/collections/addresses (Bearer):
{ "asset": "USDT", "network": "TRC20" }
Returns a unique address. On the Simulator custody backend you can simulate a
deposit landing; on a real Cobo dev portal, send testnet USDT to the address and
watch GET /api/v1/treasury/deposits.
Stablecoin payout (signed)
POST {{base_url}}/api/v1/treasury/payouts/crypto (Bearer + Idempotency-Key +
HMAC script):
{ "asset": "USDT", "network": "TRC20", "to_address": "TRecipient...addr", "amount": 10500000 }
Returns pending; poll GET /api/v1/treasury/payouts/{reference}.
7. Callbacks
Collections and payouts finalize asynchronously on a provider callback, so a
pending initiate is expected. The provider posts to the internal webhook
receiver (/api/v1/providers/{provider}/webhook/ for MoMo/bank rails; Cobo for
stablecoin), which settles the transaction and fires your merchant
webhook. When testing locally you won't receive real provider
callbacks unless your endpoint is reachable — re-fetch the resource to observe
the final state, or use the Simulator's synchronous outcomes.