Authentication

Three layers, all mandatory in both environments:

LayerMechanismApplies to
TransportMutual TLS (Sabika-issued client certificate)every request
AuthenticationOAuth2 client-credentials → certificate-bound JWT (10 min)every API call
Request integrityDetached JWS (ES256) + Idempotency-Keyorders, 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:

HeaderValue
X-TimestampUnix seconds (±300s window)
X-Nonce≥16 chars, single-use per client
X-JWS-Signaturedetached 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}`,
  };
}
Test your implementation against 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.