Signing requests
Headers
Section titled “Headers”| Header | Value |
|---|---|
x-kite-api-key-id |
The key’s ID. |
x-kite-timestamp |
The current Unix time in seconds. |
x-kite-nonce |
A random string, 8 to 128 characters of A–Z a–z 0–9 _ -, never reused. |
x-kite-signature |
The signature, base64url. |
What’s signed
Section titled “What’s signed”Join these with newlines (\n), with no trailing newline:
timestampnonceMETHODpath?querysha256(body) as lowercase hexMETHODis upper case:GET,POST,PATCH…path?queryis the path and query string exactly as sent, such as/api/payment-links?reference=order-1, without the scheme or host.- The body hash covers the exact bytes you send. With no body, hash the empty string.
Sign that string with the private key using Ed25519, and send the 64-byte signature in base64url.
Rules the server checks
Section titled “Rules the server checks”- The timestamp is within 5 minutes of the server’s clock.
- Each nonce works once per key.
- The key exists, isn’t revoked or expired, and its app hasn’t been deleted.
- The signature matches the request as received. Anything that rewrites the path, query or body on the way, such as a proxy, breaks it.
Example in Node
Section titled “Example in Node”import { createHash, createPrivateKey, randomBytes, sign } from "node:crypto";
const keyId = process.env.KITE_API_KEY_ID;const privateKey = createPrivateKey({ key: Buffer.from(process.env.KITE_PRIVATE_KEY, "base64url"), format: "der", type: "pkcs8",});
const kiteFetch = async (method: string, path: string, body?: unknown) => { const raw = body === undefined ? "" : JSON.stringify(body); const timestamp = String(Math.floor(Date.now() / 1000)); const nonce = randomBytes(16).toString("hex"); const signed = [ timestamp, nonce, method.toUpperCase(), path, createHash("sha256").update(raw).digest("hex"), ].join("\n");
return fetch(`https://pay.example.com${path}`, { method, body: raw || undefined, headers: { ...(raw ? { "content-type": "application/json" } : {}), "x-kite-api-key-id": keyId, "x-kite-timestamp": timestamp, "x-kite-nonce": nonce, "x-kite-signature": sign(null, Buffer.from(signed), privateKey).toString( "base64url", ), }, });};
const response = await kiteFetch("POST", "/api/payment-links", { title: "Pro plan", currency: "USD", amount: "49",});When it fails
Section titled “When it fails”A 401 says what was wrong: Invalid signature, Nonce has already been used, Request timestamp is outside the allowed window, Invalid API key, or a missing header.