> For the complete documentation index, see [llms.txt](https://docs.kiiglobal.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kiiglobal.io/docs/kiichain-pay/introduction/generating-api-keys.md).

# Generating API keys

Create an API key in the KiiChain Pay dashboard, understand scopes and expiry, and learn how to authenticate, sign and safely retry your API requests.

Every request to the KiiChain Pay API is authenticated with an **API key**. Write requests are additionally **signed** with the key's private key. You create and manage keys from the dashboard; you then use them from your backend.

## 1. Create a key in the dashboard

API keys are created and managed in the KiiChain Pay app. Sign in at [pay.kiichain.io](https://pay.kiichain.io) and go to **Settings → API Keys**.

<figure><img src="https://3914576130-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLp9bjbiKt9M7tGE7GNdh%2Fuploads%2Fgit-blob-1aa991ec3d1ed1d9f5455e21cfb81ffd64edd8b9%2Fkiichain-pay-api-keys-list.png?alt=media" alt="Settings → API Keys page"><figcaption><p>Settings → API Keys, with the <strong>Create New API Key</strong> button and any existing keys.</p></figcaption></figure>

Click **Create API Key** and fill in:

<table><thead><tr><th width="170">Field</th><th>Description</th></tr></thead><tbody><tr><td><strong>Name</strong></td><td>A label to identify the key (e.g. <code>Production backend</code>). Required.</td></tr><tr><td><strong>Scopes</strong></td><td>The permissions the key grants. At least one is required. A key can only be granted scopes you already have — see <a href="#scopes">Scopes</a>. Quick-select buttons let you pick <em>All access</em>, <em>Read only</em>, <em>Write only</em>, or <em>None</em>.</td></tr><tr><td><strong>Expires at</strong></td><td>Optional expiry date. If omitted, the key does not expire. We recommend setting one and rotating regularly.</td></tr></tbody></table>

<figure><img src="https://3914576130-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLp9bjbiKt9M7tGE7GNdh%2Fuploads%2Fgit-blob-fc087326f58159249e389a3704199add9c39d595%2Fkiichain-pay-create-api-key.png?alt=media" alt="Create API Key dialog"><figcaption><p>The <strong>Create API Key</strong> dialog: name, scope quick-select, and the grouped scope list.</p></figcaption></figure>

## 2. Store the secret — shown only once

When the key is created you'll see three values:

* **API key** (`api_key`) — the opaque token you send in the `Authorization` header.
* **Private key** (`priv_key`) — the Ed25519 private key used to sign write requests.
* **Public key** (`pub_key`) — stored by KiiChain Pay to verify your signatures; also visible later on the key's card.

{% hint style="danger" %}
The **API key** and **private key** are displayed **only once**, at creation. KiiChain Pay does not store the private key and cannot recover it. Copy both into a secure secret store before closing the dialog. If you lose them, rotate the key.
{% endhint %}

<figure><img src="https://3914576130-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLp9bjbiKt9M7tGE7GNdh%2Fuploads%2Fgit-blob-409a4337efab588ad4748af2507cd2d9de5986b4%2Fkiichain-pay-api-key-secret.png?alt=media" alt="One-time secret reveal dialog"><figcaption><p>The one-time reveal: copy the <strong>API Key</strong> and <strong>Private Key</strong> before closing — they're never shown again.</p></figcaption></figure>

After you close the dialog, the key appears in the list showing only a **masked prefix** (`abc123def456***`) and its public key. From the key's card you can **copy the public key**, **rotate**, or **delete** the key.

<figure><img src="https://3914576130-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLp9bjbiKt9M7tGE7GNdh%2Fuploads%2Fgit-blob-cd178c8e526f27dd2983044ca2d65584d9fa16bb%2Fkiichain-pay-api-key-card.png?alt=media" alt="API key card with scopes expanded"><figcaption><p>An existing key: masked value, the expanded <strong>Scopes</strong> list, and the copy / rotate / delete actions.</p></figcaption></figure>

* **Rotate** issues a new API key and private key for the same key entry (the old credentials stop working immediately). The new secret is shown once, exactly like creation.
* **Delete** revokes the key immediately.

## 3. Authenticate your requests

Send your API key as an `Authorization` header with the **`APIKey`** scheme (not `Bearer`):

```
Authorization: APIKey <your_api_key>
```

**Read requests** (`GET`, `OPTIONS`) need only this header:

```bash
curl https://backend.pay.kiichain.io/accounts/v1/countries \
  -H "Authorization: APIKey $KII_API_KEY"
```

**Write requests** (`POST`, `PATCH`, `PUT`, `DELETE`) must additionally be **signed** — see below.

## 4. Sign write requests

Write requests carry two extra headers:

<table><thead><tr><th width="180">Header</th><th>Value</th></tr></thead><tbody><tr><td><code>x-timestamp</code></td><td>The current time, as a string. Unix seconds (<code>1739471625</code>) or milliseconds (<code>1739471625123</code>) both work — the server signs whatever string you send. Use the <strong>same value</strong> in the signed payload.</td></tr><tr><td><code>x-signature</code></td><td>Base64url (no padding) Ed25519 signature of the canonical payload below.</td></tr></tbody></table>

The signed **payload** is four lines joined by `\n`:

```
payload = timestamp + "\n" + method + "\n" + uri + "\n" + bodyHash
```

| Part        | Value                                                                                                                                                                                |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `timestamp` | The exact string you put in the `x-timestamp` header (Unix seconds or milliseconds — just keep it identical in both places).                                                         |
| `method`    | The HTTP method, uppercase (`POST`, `PATCH`, `PUT`, `DELETE`).                                                                                                                       |
| `uri`       | The request URI **including path and query string** (e.g. `/users/v1/api/create`).                                                                                                   |
| `bodyHash`  | Lowercase **hex** SHA-256 of the raw request body bytes. For an empty body, use the SHA-256 of the empty string: `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`. |

Sign the UTF-8 bytes of `payload` with your Ed25519 **private key** and send the base64url-encoded signature in `x-signature`.

{% hint style="warning" %}
Sign exactly the bytes you send. The signature covers the timestamp, method, URI (with query string) and a hash of the request body — any mismatch is rejected. Use a **fresh `x-timestamp` per request** to avoid replay rejection.
{% endhint %}

### Reference implementation

These helpers add the correct headers and sign write requests using only each language's **standard library** — no third-party dependencies.

{% tabs %}
{% tab title="Node.js" %}
Requires Node.js 18+ (for the global `fetch` and `base64url` encoding). The `priv_key` is the raw 64-byte Ed25519 key (`seed || publicKey`), which `node:crypto` imports via a JWK.

```typescript
import { createHash, createPrivateKey, sign } from "node:crypto";

const BASE_URL = "https://backend.pay.kiichain.io";
const API_KEY = process.env.KII_API_KEY!; // the opaque "api_key"
const PRIV_KEY = process.env.KII_PRIV_KEY!; // base64url "priv_key", shown once

const WRITE_METHODS = new Set(["POST", "PATCH", "PUT", "DELETE"]);

// The priv_key is the raw 64-byte Ed25519 key: seed (first 32) || public key (last 32).
const rawPrivKey = Buffer.from(PRIV_KEY, "base64url");
const signingKey = createPrivateKey({
  format: "jwk",
  key: {
    kty: "OKP",
    crv: "Ed25519",
    d: rawPrivKey.subarray(0, 32).toString("base64url"), // private seed
    x: rawPrivKey.subarray(32).toString("base64url"), // public key
  },
});

/**
 * `path` must include the query string, e.g. "/market/v1/quotes?type=onramp".
 */
export async function kiiFetch(method: string, path: string, body?: unknown) {
  method = method.toUpperCase();
  const rawBody = body === undefined ? "" : JSON.stringify(body);
  const timestamp = Date.now().toString();

  const headers: Record<string, string> = {
    Authorization: `APIKey ${API_KEY}`,
    "x-timestamp": timestamp,
  };

  if (WRITE_METHODS.has(method)) {
    const bodyHash = createHash("sha256").update(rawBody, "utf8").digest("hex");
    const payload = `${timestamp}\n${method}\n${path}\n${bodyHash}`;
    headers["x-signature"] = sign(
      null,
      Buffer.from(payload, "utf8"),
      signingKey,
    ).toString("base64url");
    headers["Content-Type"] = "application/json";
  }

  return fetch(BASE_URL + path, {
    method,
    headers,
    body: rawBody === "" ? undefined : rawBody,
  });
}
```

{% endtab %}

{% tab title="Go" %}

```go
package kiipay

import (
	"bytes"
	"crypto/ed25519"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"time"
)

const baseURL = "https://backend.pay.kiichain.io"

var writeMethods = map[string]bool{
	http.MethodPost: true, http.MethodPatch: true,
	http.MethodPut: true, http.MethodDelete: true,
}

// KiiRequest builds and sends a signed KiiChain Pay API request.
// path must include the query string, e.g. "/market/v1/quotes?type=onramp".
func KiiRequest(method, path string, body []byte) (*http.Response, error) {
	apiKey := os.Getenv("KII_API_KEY")   // the opaque "api_key"
	privKey := os.Getenv("KII_PRIV_KEY") // base64url "priv_key", shown once

	timestamp := strconv.FormatInt(time.Now().Unix(), 10) // seconds or millis both work

	req, err := http.NewRequest(method, baseURL+path, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "APIKey "+apiKey)
	req.Header.Set("x-timestamp", timestamp)

	// Sign write requests (POST, PATCH, PUT, DELETE).
	if writeMethods[method] {
		sum := sha256.Sum256(body)
		bodyHash := hex.EncodeToString(sum[:])
		payload := fmt.Sprintf("%s\n%s\n%s\n%s", timestamp, method, path, bodyHash)

		// priv_key decodes to the raw 64-byte Ed25519 private key.
		priv, err := base64.RawURLEncoding.DecodeString(privKey)
		if err != nil {
			return nil, err
		}
		if len(priv) != ed25519.PrivateKeySize {
			return nil, fmt.Errorf("invalid priv_key: got %d bytes, want %d", len(priv), ed25519.PrivateKeySize)
		}
		sig := ed25519.Sign(ed25519.PrivateKey(priv), []byte(payload))
		req.Header.Set("x-signature", base64.RawURLEncoding.EncodeToString(sig))
		req.Header.Set("Content-Type", "application/json")
	}

	return http.DefaultClient.Do(req)
}
```

{% endtab %}
{% endtabs %}

### Signing in other languages

The scheme is language-agnostic — reproduce these steps with any Ed25519 and SHA-256 library:

1. `bodyHash = hex(sha256(rawBodyBytes))`
2. `payload = "{timestamp}\n{METHOD}\n{uri}\n{bodyHash}"`
3. `signature = base64url_nopad(ed25519_sign(privateKey, utf8(payload)))`
4. Send headers `Authorization: APIKey <api_key>`, `x-timestamp: <timestamp>`, `x-signature: <signature>`.

Base64url-decode `priv_key` to obtain the raw 64-byte Ed25519 private key.

## 5. Retry write requests safely

A write request can time out or lose its connection after the server has already processed it. To retry without running the operation twice (for example creating two tickets), send an optional **`Idempotency-Key`** header:

```
Idempotency-Key: 3f1c9a52-7d4e-4b8a-9e21-6c0f5d2b8a17
```

Generate a new unique key (a UUID v4 works well) for each operation, and send **the same key** on every retry of that operation.

<table><thead><tr><th width="260">Retry</th><th>Result</th></tr></thead><tbody><tr><td>Same key, same request, first attempt succeeded</td><td>The stored response is returned. The operation is <strong>not</strong> run again.</td></tr><tr><td>Same key, first attempt still running</td><td><code>409 Conflict</code>. Wait and retry with the same key.</td></tr><tr><td>Same key, different body or parameters</td><td><code>400 Bad Request</code>. Use a new key for a new operation.</td></tr><tr><td>Same key, first attempt failed</td><td>Nothing was stored, so the request runs again as a new one.</td></tr></tbody></table>

* The key must be between 1 and 255 characters; anything else is rejected with `400 Bad Request`.
* A key is scoped to your user and the endpoint: the same key sent to two different endpoints is treated as two separate keys.
* Keys are kept for **24 hours**. After that, a request with the same key runs as a new one.
* Read requests ignore the header.

{% hint style="warning" %}
The idempotency check runs **after** authentication. Every retry still needs a **fresh `x-timestamp` and a new `x-signature`**, and a valid `X-Mfa-Code` on endpoints that require one. Don't replay the original request's headers byte for byte.
{% endhint %}

`Idempotency-Key` has no effect on [creating or rotating an API key](/docs/kiichain-pay/api-reference/users.md). A retry of those runs again.

## Scopes

Scopes follow the pattern `<module>.<resource>.<action>`, where the action is one of `read`, `write`, `update`, `delete`, or `admin`. A key's scopes are validated on every call, and the endpoint you hit declares which scope it requires.

{% hint style="info" %}
A key can never have more access than the account that created it — requested scopes are capped by **your own** scopes. Scopes with an `:all` suffix (cross-account) and `admin` scopes are elevated permissions and are generally not available to integrator keys.
{% endhint %}

Commonly used scopes:

<table><thead><tr><th width="330">Scope</th><th>Grants</th></tr></thead><tbody><tr><td><code>users.api.read</code> / <code>write</code> / <code>update</code> / <code>delete</code></td><td>Manage your API keys.</td></tr><tr><td><code>users.accounts.read</code> / <code>write</code></td><td>Read and manage accounts.</td></tr><tr><td><code>accounts.kyc.read</code> / <code>write</code></td><td>Read KYC status / start KYC.</td></tr><tr><td><code>accounts.withdrawal_destination.read</code> / <code>write</code></td><td>Manage withdrawal destinations (bank accounts, wallet addresses).</td></tr><tr><td><code>market.products.read</code>, <code>market.providers.read</code>, <code>market.dex.read</code>, <code>market.instruments.read</code>, <code>market.fiat_assets.read</code></td><td>Discover on-/off-ramp products, providers, DEX routes, instruments and fiat assets.</td></tr><tr><td><code>market.providers_virtual_accounts.read</code> / <code>write</code></td><td>Create and read virtual accounts for fiat on-/off-ramps.</td></tr><tr><td><code>tickets.tickets.read</code> / <code>write</code></td><td>Create and track on-ramp, off-ramp and swap tickets.</td></tr><tr><td><code>ledger.entries.read</code></td><td>Read ledger balances and transaction history.</td></tr></tbody></table>

The dashboard's create dialog always lists the full, current set of scopes available to your account.

## Next steps

* [Internal wallets](/docs/kiichain-pay/introduction/internal-wallets.md) — the Kii Wallet model your API operations act on.
* [Privy delegated access](/docs/kiichain-pay/introduction/privy-delegated-access.md) — required before your key can drive wallet operations.
* [API Reference](/docs/kiichain-pay/api-reference.md) — per-endpoint scopes and payloads.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.kiiglobal.io/docs/kiichain-pay/introduction/generating-api-keys.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
