MBKYC uses a split-secret authentication model: the SDK holds only the non-secret API key ID, while the secret stays wherever you choose to keep it (normally your backend). The SDK delegates JWT signing to a callback you provide — the token signer.
The SDK never receives, stores, or transmits your API secret. This means a compromised device or browser cannot leak it.
The JWT
When the SDK needs to authenticate (device registration and handshake), it builds a JWT:
header: { "alg": "HS256", "typ": "JWT" }
payload: { "key_id": "<your API key ID>", "fingerprint": "<device fingerprint>" }
It then computes the signing input — base64url(header) + "." + base64url(payload) — and hands those bytes to your token signer. Your signer computes HMAC-SHA256(signingInput, keySecret) and returns the raw signature. The SDK assembles the final header.payload.signature and sends it in the x-auth-key: Bearer <JWT> header.
The backend looks up the key_id, confirms the key is active, and verifies the signature with its stored copy of the secret. A revoked key fails here.
Implementing the signer
The signer is the same idea in every SDK — receive bytes, return a signature.
TypeScript
Kotlin / Java
C#
C
const tokenSigner = {
async sign(signingInput: Uint8Array): Promise<Uint8Array> {
const res = await fetch('/api/mbkyc/sign', { method: 'POST', body: signingInput });
return new Uint8Array(await res.arrayBuffer());
},
};
val signer = TokenSignerHandler { signingInput -> myBackend.hmacSha256(signingInput) }
class MySigner : ITokenSigner {
public Task<byte[]> SignAsync(byte[] signingInput, CancellationToken ct)
=> myBackend.HmacSha256Async(signingInput, ct);
}
static void on_sign(void* ud, const uint8_t* input, uintptr_t len,
MBKYC_SignResponseCallback cb, void* cb_ud) {
uint8_t sig[32]; /* HMAC-SHA256(input, len) -> sig */
MBKYC_SignResult r = { .kind = MBKYC_SIGN_RESULT_KIND_SUCCESS,
.signature = sig, .signature_len = sizeof(sig) };
cb(cb_ud, r);
}
Server-side signing endpoint
Your /api/mbkyc/sign endpoint holds the secret and does the HMAC. In Node:
import { createHmac } from 'node:crypto';
app.post('/api/mbkyc/sign', express.raw({ type: '*/*' }), (req, res) => {
const signature = createHmac('sha256', process.env.MBKYC_KEY_SECRET)
.update(req.body) // the raw signingInput bytes
.digest();
res.type('application/octet-stream').send(signature);
});
Authenticate and rate-limit your signing endpoint. It is effectively a delegated capability to authenticate as your organization — treat it like any other privileged internal API. It should only ever sign inputs for sessions your own application initiated.
The SDK also sends:
x-sdk-id: <sdk>/<version> (e.g. kotlin/0.7.0) — identifies the SDK build.
- Any
extraHeaders you configured (max 5) — for example, headers your proxy or gateway requires to admit the request.