> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.kotanipay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Request Signing

> Protect outbound API requests with HMAC-SHA256 signatures when secure mode is enabled on your account.

When request signing (`isSecure` mode) is enabled on your integrator account, every request you send to the Kotani Pay API must include three additional headers carrying an HMAC-SHA256 signature. The server rejects any request that is missing these headers, uses a stale timestamp, or replays a nonce it has already seen.

<Note>
  Request signing governs requests **from your server to Kotani Pay**. It is separate from webhook signature verification, which covers the opposite direction — notifications Kotani Pay sends to your server. See [Webhook Notifications](/v3/essentials/webhooks) for that.
</Note>

## Required Headers

Include all three headers on every request.

| Header        | Description                                                                              |
| ------------- | ---------------------------------------------------------------------------------------- |
| `x-timestamp` | Unix timestamp in seconds (e.g. `1715123456`). Must be within ±5 minutes of server time. |
| `x-nonce`     | UUID v4. Must be unique per request — each value is accepted exactly once.               |
| `x-signature` | HMAC-SHA256 of the signing payload using your API secret, hex-encoded.                   |

## Signing Payload

Construct the payload string before computing the signature.

**POST / PUT / PATCH**

```
${timestamp}.${nonce}.${JSON.stringify(requestBody)}
```

**GET**

```
${timestamp}.${nonce}.${lastPathSegment}
```

where `lastPathSegment` is the final segment of the request URL path. For example, a request to `/api/v3/wallets/fiat/64a1b2c3d4e5f6a7b8c9d0e2` uses `64a1b2c3d4e5f6a7b8c9d0e2`.

<Warning>
  Serialize the request body with **no extra spaces** — compact JSON only. Any whitespace difference between your serialisation and the server's will cause a signature mismatch and a `401 Unauthorized` response.
</Warning>

### Example signature value

```
8b5a21c2e33f3cf33a2e9c92d585cb5a1c304b6f63f91d2f6b0a428cbe9854d1
```

## Postman Pre-request Script

Paste this into the **Pre-request Script** tab of your Postman collection or individual request. It reads your secret from a Postman environment variable and automatically injects the three signing headers before each request.

```javascript theme={null}
// Kotani Pay — Request Signing Pre-request Script
// Add KOTANI_PAY_SIGNATURE to your Postman environment before running.

const secret = pm.environment.get("KOTANI_PAY_SIGNATURE");
if (!secret) {
  console.warn("[KotaniPay] KOTANI_PAY_SIGNATURE is not set in the active environment.");
}

const timestamp = Math.floor(Date.now() / 1000).toString();

// UUID v4 — Postman sandbox does not expose a uuid library
const nonce = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
  const r = (Math.random() * 16) | 0;
  return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
});

const method = pm.request.method;
let payload;

if (method === "GET") {
  const parts = pm.request.url.getPath().split("/");
  const lastSegment = parts[parts.length - 1] || "";
  payload = `${timestamp}.${nonce}.${lastSegment}`;
} else {
  const body = pm.request.body?.raw || "{}";
  payload = `${timestamp}.${nonce}.${body}`;
}

// CryptoJS is available globally in the Postman sandbox
const signature = CryptoJS.HmacSHA256(payload, secret).toString();

pm.request.headers.add({ key: "x-timestamp", value: timestamp });
pm.request.headers.add({ key: "x-nonce",     value: nonce });
pm.request.headers.add({ key: "x-signature", value: signature });

console.log("[KotaniPay] Signed:", { timestamp, nonce, payload });
```

**Setup steps:**

1. Open your collection → **Variables** tab (or **Environments**) and add `KOTANI_PAY_SIGNATURE` with your secret value.
2. Paste the script into the collection's **Pre-request Script** tab so it runs for every request automatically.
3. Set your request body as **raw → JSON**. The script reads `pm.request.body.raw` directly, so the body must already be the final JSON string before the script runs.

<Note>
  Postman uses **CryptoJS** (not Node's `crypto` module) — `CryptoJS` is available globally with no `require()` needed. Do not use `crypto.createHmac` here; it will throw a reference error.
</Note>

## Code Snippets

### Node.js

```typescript theme={null}
import crypto from "crypto";
import { v4 as uuidv4 } from "uuid";

function signRequest({
  secret,
  body,
  method,
}: {
  secret: string;
  body: Record<string, unknown> | string;
  method: string;
}) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = uuidv4();
  const payload =
    method === "GET"
      ? `${timestamp}.${nonce}.${body}` // body = last path segment for GETs
      : `${timestamp}.${nonce}.${JSON.stringify(body)}`;

  const signature = crypto
    .createHmac("sha256", secret)
    .update(payload, "utf8")
    .digest("hex");

  return {
    "x-timestamp": timestamp,
    "x-nonce": nonce,
    "x-signature": signature,
  };
}
```

Usage:

```typescript theme={null}
const headers = signRequest({
  secret: process.env.KOTANI_PAY_SIGNATURE!,
  body: { wallet_id: "64a1b2c3d4e5f6a7b8c9d0e2", amount: 1000 },
  method: "POST",
});

await fetch("https://api.kotanipay.io/api/v3/deposits/mobile-money", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.KOTANI_API_KEY}`,
    "Content-Type": "application/json",
    ...headers,
  },
  body: JSON.stringify({ wallet_id: "64a1b2c3d4e5f6a7b8c9d0e2", amount: 1000 }),
});
```

### Python

```python theme={null}
import hmac, hashlib, time, uuid, json

def sign_request(secret: str, body: dict | str, method: str) -> dict:
    timestamp = str(int(time.time()))
    nonce = str(uuid.uuid4())
    if method.upper() == "GET":
        payload = f"{timestamp}.{nonce}.{body}"
    else:
        payload = f"{timestamp}.{nonce}.{json.dumps(body, separators=(',', ':'))}"
    signature = hmac.new(
        secret.encode(), payload.encode(), hashlib.sha256
    ).hexdigest()
    return {"x-timestamp": timestamp, "x-nonce": nonce, "x-signature": signature}
```

## Security Notes

* **The nonce is single-use.** Reusing a nonce that the server has already accepted returns `401 Unauthorized`. Generate a fresh UUID v4 for every request.
* **The timestamp window is ±5 minutes.** A request whose `x-timestamp` falls outside that window returns `401 Unauthorized`. Ensure your server clock is NTP-synchronised.
* **Compact JSON serialisation is required.** Use `JSON.stringify` without formatting options in Node.js, and `json.dumps(..., separators=(',', ':'))` in Python. Extra whitespace breaks the signature.
* **Your API secret is shown once.** The secret is displayed at key generation time and cannot be retrieved afterwards. Store it in a secrets manager or environment variable — never in source code.
* **Secure mode is permanent.** Once request signing is activated for your account it cannot be turned off. Test your signing implementation in the sandbox environment before going live.

## Getting Your API Secret

Your API secret is generated alongside your API key in the dashboard.

1. Log in → **Settings** → **API Keys**
2. Click **Generate Key** and choose **Secure** mode
3. Copy both the API key and the API secret immediately — the secret is shown only once

If you have an existing API key without a secure secret, generate a new key with secure mode enabled. See [API Keys](/v3/getting-started/api-keys) for full key management instructions.
