Kite

Signing requests

How an API key signs a request, for when you're not using the SDK.

@kite-01/sdk does all of this for you. Read on if you're calling the API from another language or your own HTTP client.

Headers

HeaderValue
x-kite-api-key-idThe key's ID.
x-kite-timestampThe current Unix time in seconds.
x-kite-nonceA random string, 8 to 128 characters of A–Z a–z 0–9 _ -, never reused.
x-kite-signatureThe signature, base64url.

What's signed

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.

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

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

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.

On this page