Skip to content

Signing requests

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.

Join these with newlines (\n), with no trailing newline:

timestamp
nonce
METHOD
path?query
sha256(body) as lowercase hex
  • METHOD is upper case: GET, POST, PATCH…
  • path?query is 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.

  • 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.
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",
});

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.