Authentication
Three layers, all mandatory in both environments:
| Layer | Mechanism | Applies to |
|---|---|---|
| Transport | Mutual TLS (Sabika-issued client certificate) | every request |
| Authentication | OAuth2 client-credentials → certificate-bound JWT (10 min) | every API call |
| Request integrity | Detached JWS (ES256) + Idempotency-Key | orders, withdrawals |
Mutual TLS
Generate your key and CSR; send only the CSR:
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out partner.key
openssl req -new -key partner.key -subj "/O=Your Org/CN=your_client_id" -out partner.csr
Sabika returns partner.crt. Your private key never leaves your systems.
Tokens are bound to the certificate (RFC 8705): a stolen token is useless
without the matching certificate.
OAuth2 token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=sbk_live_...&client_secret=sks_...
Tokens expire after 600 seconds; request a new one as needed. Responses carry
Cache-Control: no-store — never persist tokens.
Signing money-moving requests (detached JWS)
For POST /v1/orders and POST /v1/withdrawals, send three extra headers:
| Header | Value |
|---|---|
X-Timestamp | Unix seconds (±300s window) |
X-Nonce | ≥16 chars, single-use per client |
X-JWS-Signature | detached compact JWS (see below) |
The signature covers the canonical string (note: URL includes any query string):
METHOD \n URL \n sha256hex(body) \n timestamp \n nonce
Sign with your registered ES256 key as an RFC 7515 detached compact JWS with
{"alg":"ES256","b64":false,"crit":["b64"]}. Node.js example (jose):
import { createHash } from 'node:crypto';
import { CompactSign, importPKCS8 } from 'jose';
async function signRequest(method, url, body, privateKeyPem) {
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomUUID().replaceAll('-', '');
const bodyHash = createHash('sha256').update(body).digest('hex');
const payload = `${method}\n${url}\n${bodyHash}\n${timestamp}\n${nonce}`;
const key = await importPKCS8(privateKeyPem, 'ES256');
const jws = await new CompactSign(new TextEncoder().encode(payload))
.setProtectedHeader({ alg: 'ES256', b64: false, crit: ['b64'] })
.sign(key);
const [protectedHeader, , signature] = jws.split('.');
return {
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-JWS-Signature': `${protectedHeader}..${signature}`,
};
}
POST /v1/signed-probe
— it verifies the full signature pipeline without moving money.Idempotency
Every write requires an Idempotency-Key header (any unique string, e.g. a UUID).
Retrying with the same key returns 200 with the original result. Reusing a key
with different parameters returns 409 idempotency_key_conflict.
Rotation
Certificates (1-year validity), client secrets, signing keys, and webhook secrets are all rotatable with overlap windows — contact operations. Multiple certificate fingerprints can be active simultaneously during cutover.