Skip to main content

Signing requests

Every call to the Partner API carries a Bearer key ID (identifies you) and an HMAC-SHA256 signature (proves you hold the secret — which is never transmitted). The same scheme, in reverse, signs the webhooks we send you.

Headers

Authorization: Bearer <apiKeyId>
Content-Type: application/json
X-Quikkred-Timestamp: <unix-milliseconds>
X-Quikkred-Signature: <hex signature>

The canonical string

signature = hex( HMAC-SHA256( METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + RAW_BODY, hmacSecret ) )
ComponentRuleCommon mistake
METHODUppercase (POST)lowercase
PATHURL path only, no query string (/api/v1/partner/cases)including ?dryRun=1
TIMESTAMPExact value of the X-Quikkred-Timestamp header, unix millisecondsseconds instead of ms
RAW_BODYThe exact bytes you transmit; empty string for GETre-serialising JSON after signing (key order/whitespace changes the bytes)

The timestamp must be within ±5 minutes of server time (replay protection).

Implementations

Node.js
const crypto = require("crypto");

function sign(method, path, rawBody, secret) {
const ts = Date.now().toString();
const canonical = `${method}\n${path}\n${ts}\n${rawBody}`;
const sig = crypto.createHmac("sha256", secret).update(canonical).digest("hex");
return { ts, sig };
}

const body = JSON.stringify({ rows: [/* … */] });
const { ts, sig } = sign("POST", "/api/v1/partner/cases", body, process.env.QK_SECRET);
// send `body` EXACTLY as signed — do not stringify again
Python
import hashlib, hmac, json, time

def sign(method: str, path: str, raw_body: str, secret: str):
ts = str(int(time.time() * 1000))
canonical = f"{method}\n{path}\n{ts}\n{raw_body}"
sig = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
return ts, sig

body = json.dumps(payload, separators=(",", ":")) # serialise ONCE, sign, send as-is
ts, sig = sign("POST", "/api/v1/partner/cases", body, SECRET)
PHP
function qk_sign(string $method, string $path, string $rawBody, string $secret): array {
$ts = (string) round(microtime(true) * 1000);
$canonical = "$method\n$path\n$ts\n$rawBody";
return [$ts, hash_hmac('sha256', $canonical, $secret)];
}
cURL (bash)
TS=$(date +%s%3N)
BODY='{"rows":[...]}'
SIG=$(printf 'POST\n/api/v1/partner/cases\n%s\n%s' "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$QK_SECRET" -r | cut -d' ' -f1)
curl -X POST "$QK_BASE/api/v1/partner/cases" \
-H "Authorization: Bearer $QK_KEY_ID" -H "Content-Type: application/json" \
-H "X-Quikkred-Timestamp: $TS" -H "X-Quikkred-Signature: $SIG" \
-d "$BODY"

Verifying OUR webhooks

Identical scheme, with your callback path as PATH and the raw request body you received as RAW_BODY. Always compare with a constant-time function. See webhooks for full receiver code.

Test it immediately

GET /api/v1/partner/health is the auth smoke test — a correct signature returns your organisation code:

{ "ok": true, "lenderCode": "YOURORG", "env": "test", "serverTime": "…" }

Failing? The signature debugger shows you the server-side canonical string to diff against yours.