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):

  1. Request access in the dashboard → pending_review.
  2. An admin approves sandbox → your sk_test_ keys work on the sandbox.
  3. 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-SignatureHMAC_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.