Authentication
Bearer secret keys
Authenticate every request with your secret key:
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxx
Keys are shown once at creation and stored hashed — keep them server-side and
never embed them in client apps. sk_test_ keys work only on the sandbox;
sk_live_ keys only in production. Keys are scoped and rotatable. The scopes:
| Scope | Grants |
|---|---|
trades:write |
quote and execute stablecoin trades |
treasury:write |
aggregator payouts and withdrawals (money-out) |
collections:write |
create collection / deposit addresses |
read |
read trades, orders, payouts, and settlement reports |
Service access — the ladder
API access to the trading and treasury endpoints is granted through a review
ladder, not by default (web trading in the dashboard is the always-on door). A
merchant moves not_requested → pending_review → approved_sandbox → approved_live
(or suspended / rejected):
- Request access in the dashboard →
pending_review. - An admin approves sandbox → your
sk_test_keys work on the sandbox. - Request live; once KYB and compliance clear, an admin approves live →
your
sk_live_keys work in production.
The gates compose: a sk_test_ key needs the service approved (sandbox or
live); a sk_live_ key needs it approved_live. A call that isn't yet approved
for its key's mode returns 403 compliance_block.
HMAC request signing
High-risk endpoints (/trades, /treasury/payouts, and the payments
/transfers) additionally require a signature. Send two headers:
X-Starpay-Timestamp— current unix time (seconds)X-Starpay-Signature—HMAC_SHA256(signing_secret, "{timestamp}.{method}.{path}.{sha256(body)}")
Requests are rejected if the timestamp is more than 300 seconds old.
cURL
TS=$(date +%s)
BODY='{"amount":250000,"currency":"GHS","msisdn":"0240000000"}'
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(printf '%s' "$TS.POST./api/v1/transfers.$BODY_HASH" \
| openssl dgst -sha256 -hmac "$STARPAY_SIGNING_SECRET" | awk '{print $2}')
curl -X POST https://api.starpay.example/api/v1/transfers \
-H "Authorization: Bearer $STARPAY_SECRET_KEY" \
-H "X-Starpay-Timestamp: $TS" \
-H "X-Starpay-Signature: $SIG" \
-H "Content-Type: application/json" \
-d "$BODY"
Python
import hashlib, hmac, time
def sign(secret: str, method: str, path: str, body: bytes) -> tuple[str, str]:
ts = str(int(time.time()))
body_hash = hashlib.sha256(body).hexdigest()
message = f"{ts}.{method}.{path}.{body_hash}"
sig = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
return ts, sig
Node.js
const crypto = require("crypto");
function sign(secret, method, path, body) {
const ts = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const message = `${ts}.${method}.${path}.${bodyHash}`;
const sig = crypto.createHmac("sha256", secret).update(message).digest("hex");
return { ts, sig };
}
PHP
function starpay_sign(string $secret, string $method, string $path, string $body): array {
$ts = (string) time();
$bodyHash = hash('sha256', $body);
$message = "{$ts}.{$method}.{$path}.{$bodyHash}";
$sig = hash_hmac('sha256', $message, $secret);
return [$ts, $sig];
}
Optionally restrict keys to an IP allowlist, enforced at the auth layer.