Your first call
A signed GET /v1/health, then a real quote, in curl, Node and Python.
GET /v1/health is the smallest signed request you can make. It takes no body
and no parameters, so it isolates your signing code from everything else: if it
returns 200, your signature, key and clock are all correct.
It is still authenticated. There is no unauthenticated route on this API.
Check the unsigned response first
Before you sign anything, see what an unauthenticated request looks like, so you recognise it later:
curl -i https://sandbox.onlink.africa/v1/healthHTTP/2 401
content-type: application/json; charset=utf-8
x-request-id: 428
{"statusCode":401,"message":"An error occurred","error":"unauthorized","requestId":90512}That is the response for every authentication failure — bad signature,
expired timestamp, replayed nonce, unknown key. Keep the requestId: it is the
only handle that resolves to your request in our logs, and it is also returned
as the X-Request-Id header.
Sign it
#!/usr/bin/env bash
# Usage: ONLINK_KEY_ID=pk_... ONLINK_SECRET=sk_... ./health.sh
set -euo pipefail
HOST="https://sandbox.onlink.africa"
METHOD="GET"
PATH_AND_QUERY="/v1/health"
BODY=""
TIMESTAMP="$(python3 -c 'import time; print(int(time.time() * 1000))')"
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
# Five fields, newline-joined. printf, not echo -e: the trailing newline echo
# adds would be signed and the signature would not match.
SIGNING_STRING="$(printf '%s\n%s\n%s\n%s\n%s' \
"$METHOD" "$PATH_AND_QUERY" "$TIMESTAMP" "$NONCE" "$BODY_HASH")"
SIGNATURE="$(printf '%s' "$SIGNING_STRING" \
| openssl dgst -sha256 -hmac "$ONLINK_SECRET" -hex \
| awk '{print $NF}')"
curl -i "${HOST}${PATH_AND_QUERY}" \
-H "X-OnLink-Key: ${ONLINK_KEY_ID}" \
-H "X-OnLink-Timestamp: ${TIMESTAMP}" \
-H "X-OnLink-Nonce: ${NONCE}" \
-H "X-OnLink-Signature: v1=${SIGNATURE}"A successful call returns:
{ "status": "ok" }status is always the exact string ok. If the service is not serving you get
an HTTP error, not a degraded value in that field.
Then price something
POST /v1/quotes is the first call that does real work. Supply one leg and the
other is derived:
{
"side": "sell",
"usdtAmount": "250.000000"
}The response locks a rate:
{
"quoteId": "f1c0a5d2-3b4e-4a71-9c8d-0e1f2a3b4c5d",
"side": "sell",
"kesAmount": "32025.00",
"usdtAmount": "250.000000",
"rate": "128.10",
"expiresAt": "2026-09-04T09:31:30.000Z",
"settlementEstimateSeconds": 1800
}Three things to notice, because each is a decision you have to make now rather than later:
- Every amount is a string. Parsing one into a binary float loses money quietly. See Money.
expiresAtis short and the quote is single-use. Create the order against it promptly; a spent or expired quote is a409.settlementEstimateSecondsis a banded estimate, not a promise. It tells you roughly what to tell your user, not what to time out on.
Amounts here are illustrative
The rate above is an example, not a live figure. Read rate, kesAmount and
expiresAt off your own quote response — never assume a rate or a window.
Next
- Sell USDT, receive KES — the full flow
- Webhooks — how you learn an order finished
- Errors — the envelope and the full code catalogue