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

# Authentication

> Bearer keys for reading, signed requests for anything that moves money

Every request needs an API key, and your tenant is resolved from the key
— never from a header or body field you control. Keys are scoped
(`ledger:read`, `withdrawal:create`, `treasury:spend`, …); a 403 means the
key is valid but lacks the scope the operation requires, not that
something is broken.

## Bearer keys

The simple form, and the right default for anything read-only:

```bash theme={null}
curl https://api.keypair.io/v1/accounts/system \
  -H "Authorization: Bearer kp_test_51H8f…"
```

`kp_test_…` keys run against testnet; `kp_live_…` against mainnet. They
are otherwise identical — same endpoints, same shapes — so integrate
against test and swap the key when you're ready. See
[Networks](https://docs.tetlith.com/concepts/networks.md) for how the two stay isolated end to end.

> [!WARNING]
>
> A bearer key is a bearer credential: whoever holds it can act as your
> tenant. Keep it server-side only, and prefer signed requests (below) for
> anything that moves money, so a leaked key alone is not enough.

## Signed requests

For withdrawals, treasury spends, and anything else that moves funds,
sign the request instead of sending the secret. The signature covers:

```
timestamp \n nonce \n METHOD \n path \n sha256(body)
```

Ed25519-sign that string with your key's private half, then send:

| Header | Value |
| - | - |
| `x-keypair-key-id` | Your key's public prefix — never the secret |
| `x-keypair-timestamp` | Unix seconds, must be within 5 minutes of KeyPair's clock |
| `x-keypair-nonce` | Random per request; rejected if reused |
| `x-keypair-signature` | The Ed25519 signature, base64 |

No secret travels on the wire, and a captured request cannot be replayed
(the nonce) or altered (the signature covers the body's hash) or reused
past its window (the timestamp).

> [accordion] Reference implementation
>
> A minimal, zero-dependency signer — copy it straight into your backend
> rather than depending on a KeyPair SDK for something this small:
>
> ```ts theme={null}
> import { createHash, sign } from "node:crypto";
>
> function signRequest(
>   privateKey: Buffer,
>   method: string,
>   path: string,
>   body: string,
> ) {
>   const timestamp = Math.floor(Date.now() / 1000).toString();
>   const nonce = crypto.randomUUID();
>   const bodyHash = createHash("sha256").update(body).digest("hex");
>   const payload = [timestamp, nonce, method, path, bodyHash].join("\n");
>   const signature = sign(null, Buffer.from(payload), privateKey).toString("base64");
>   return { timestamp, nonce, signature };
> }
> ```

## Choosing between them

> [!TIP]
>
> Bearer for reads, signed for writes that move money — everything else
> (creating a customer, opening an account) is safe either way, since
> nothing there can move funds on its own.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.