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

  1. Create account or mint an API-only sandbox key. Save apiKey, hmacSecret, and vaultId.
  2. POST /v1/auth/token → JWT (step 2).
  3. GET /v1/vendors/me → vaults[0].id (no HMAC).
  4. POST /v1/customers with { vaultId, externalId } and HMAC (step 5).
  5. POST /v1/rewards/credit with { customerId, points: "100" } (HMAC). Optional idempotencyKey.
  6. 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.
  7. Inspect GET /v1/rewards/ledger and the console overview.

8. Going live

  1. Use your console to manage keys. Live vaults are activated by a platform admin after you are ready to fund real USDC or USDT.
  2. After activation, issue a coinharbour.co.za_live_ key from API keys. Sandbox accounts keep coinharbour.co.za_test_ keys.
  3. Fund escrow on-chain (or via admin EFT credit). Same endpoints and HMAC rules as sandbox.
  4. Point webhooks / production clients at the live base URL and rotate secrets out of sandbox.

Full endpoint catalogue: API reference · Swagger UI