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, add Idempotency-Key: {{$guid}} — Postman's $guid mints 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 returns 409 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), a base_url that 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.