Three endpoints. Every request is HMAC-SHA256 signed. Bearer tokens are not accepted.
/api/public/v1/subjects/{country}/{id_type}/{id_value}scope: read/api/public/v1/subjects/{country}/{id_type}/{id_value}/validatescope: validate/api/public/v1/parsescope: parseEach credential consists of a public Key ID and a 256-bit signing secret. The secret never leaves your servers — it is used only to compute an HMAC signature for each request.
Every request must include these four headers:
x-api-key-id : pjk_<32-hex> x-timestamp : <unix seconds> # must be within ±300s of server time x-nonce : <16–128 char random> # unique per request (replay protection) x-signature : hex( HMAC-SHA256( secret, canonical ) )
The canonical string is:
METHOD + "\n" +
PATH + "\n" +
TIMESTAMP + "\n" +
NONCE + "\n" +
SHA256_HEX( RAW_BODY ) # use SHA256_HEX("") for GET requestsimport { createHash, createHmac, randomBytes } from "crypto";
const KEY_ID = "pjk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const HOST = "https://your-domain.lovable.app";
async function call(method, path, body = "") {
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = randomBytes(16).toString("hex");
const bodyHash = createHash("sha256").update(body).digest("hex");
const canonical = [method, path, ts, nonce, bodyHash].join("\n");
const signature = createHmac("sha256", SECRET).update(canonical).digest("hex");
const r = await fetch(HOST + path, {
method,
headers: {
"content-type": "application/json",
"x-api-key-id": KEY_ID,
"x-timestamp": ts,
"x-nonce": nonce,
"x-signature": signature,
},
body: body || undefined,
});
return r.json();
}
// Look up a subject
await call("GET", "/api/public/v1/subjects/MY/nric/910101015555");
// Validate fields against our record
const body = JSON.stringify({ fields: { full_name: "Ali bin Ahmad", monthly_income: 8500 } });
await call("POST", "/api/public/v1/subjects/MY/nric/910101015555/validate", body);