Integration guide
You already have an API key. Next: exchange it for a JWT, look up your vault, then HMAC-sign POSTs such as create customer. GETs only need the JWT.
1. Base URL
https://api.coinharbour.co.za/v1
Interactive OpenAPI: https://api.coinharbour.co.za/docs
2. Exchange API key for JWT
Sandbox keys start with coinharbour.co.za_test_. Live keys use coinharbour.co.za_live_. The JWT lasts 15 minutes. Re-run this if you get 401.
API=https://api.coinharbour.co.za/v1
API_KEY='coinharbour.co.za_test_…'
TOKEN=$(curl -sS -X POST "$API/auth/token" \
-H 'content-type: application/json' \
-d "{\"apiKey\":\"$API_KEY\"}" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["accessToken"])')
echo "$TOKEN"Put that token on later calls as Authorization: Bearer $TOKEN.
3. Reads: JWT only
GET requests do not use HMAC. After step 2, read the account and take vaults[0].id. That is vaultId. It is not vendorId from the JWT.
curl -sS "$API/vendors/me" \ -H "authorization: Bearer $TOKEN" VAULT_ID=$(curl -sS "$API/vendors/me" \ -H "authorization: Bearer $TOKEN" \ | python3 -c 'import sys,json; print(json.load(sys.stdin)["vaults"][0]["id"])') echo "$VAULT_ID" curl -sS "$API/customers" \ -H "authorization: Bearer $TOKEN"
4. Writes: JWT + HMAC
POST, PUT, PATCH, and DELETE change state. A stolen JWT should not be enough to create customers or move points, so those calls also need a signature from the hmacSecret shown once when the API key was created (prefix coinharbour.co.za_hmac_). If you no longer have it, mint a new key from sandbox or the console.
You send two extra headers:
- X-Timestamp: Unix seconds (must be within about 5 minutes of server time)
- X-Signature: hex HMAC-SHA256 of this exact string:
{timestamp}.{METHOD}.{path}.{rawBody}path includes /v1. rawBody must be the exact JSON bytes in -d (one line, no extra spaces). Example string to HMAC:
1730000000.POST./v1/customers.{"vaultId":"…","externalId":"cust-1"}5. Create a customer with curl
Body: vaultId from GET /vendors/me (vaults[0].id, not the JWT vendorId) and at least one of externalId, email, mobile.
HMAC='coinharbour.co.za_hmac_…' # shown once at key create
# VAULT_ID already set in step 3 from vaults[0].id (not JWT vendorId)
BODY='{"vaultId":"'"$VAULT_ID"'","externalId":"cust-1"}'
ROUTE='/customers' # curl URL after $API
PATH_NAME="/v1$ROUTE" # HMAC signs the full path
TS=$(date +%s)
SIG=$(printf '%s' "$TS.POST.$PATH_NAME.$BODY" \
| openssl dgst -sha256 -hmac "$HMAC" \
| awk '{print $NF}')
curl -sS -X POST "$API$ROUTE" \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-H "x-timestamp: $TS" \
-H "x-signature: $SIG" \
-d "$BODY"$API already includes /v1, so curl uses $API$ROUTE. HMAC still signs PATH_NAME=/v1$ROUTE. Recompute TS and SIG whenever BODY or the route changes.
# Credit 100 points (HMAC)
BODY='{"customerId":"CUSTOMER_ID","points":"100"}'
ROUTE='/rewards/credit'
PATH_NAME="/v1$ROUTE"
TS=$(date +%s)
SIG=$(printf '%s' "$TS.POST.$PATH_NAME.$BODY" \
| openssl dgst -sha256 -hmac "$HMAC" \
| awk '{print $NF}')
curl -sS -X POST "$API$ROUTE" \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-H "x-timestamp: $TS" \
-H "x-signature: $SIG" \
-d "$BODY"6. Same HMAC in Node (optional)
Apps should sign in code the same way. Curl above is enough for terminal testing.
import crypto from 'node:crypto';
function sign(hmacSecret, method, path, body) {
const ts = Math.floor(Date.now() / 1000);
const raw = typeof body === 'string' ? body : JSON.stringify(body);
const payload = `${ts}.${method.toUpperCase()}.${path}.${raw}`;
const sig = crypto.createHmac('sha256', hmacSecret).update(payload).digest('hex');
return { ts, sig, raw };
}7. Sandbox sequence
- Create account or mint an API-only sandbox key. Save apiKey, hmacSecret, and vaultId.
- POST /v1/auth/token → JWT (step 2).
- GET /v1/vendors/me → vaults[0].id (no HMAC).
- POST /v1/customers with { vaultId, externalId } and HMAC (step 5).
- POST /v1/rewards/credit with { customerId, points: "100" } (HMAC). Optional idempotencyKey.
- POST /v1/claims/issue then POST /v1/claims/redeem with a destination wallet (HMAC). Optional email and mobile are stored on the customer; if email is present we send a payout notice from the programme name. Sandbox completes in the ledger without a chain job.
- Inspect GET /v1/rewards/ledger and the console overview.
8. Going live
- Use your console to manage keys. Live vaults are activated by a platform admin after you are ready to fund real USDC or USDT.
- After activation, issue a coinharbour.co.za_live_ key from API keys. Sandbox accounts keep coinharbour.co.za_test_ keys.
- Fund escrow on-chain (or via admin EFT credit). Same endpoints and HMAC rules as sandbox.
- Point webhooks / production clients at the live base URL and rotate secrets out of sandbox.
Full endpoint catalogue: API reference · Swagger UI
