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

# Authentication & Setup

> Mint a PARTNER API key via challenge-response and verify it against a protected endpoint.

## Overview

Every protected endpoint in GRAIL takes a single header: `x-api-key`. There is one partner scope (`PARTNER`) — one key grants access to everything a partner can do (trades, redemptions, users).

Keys are minted via **challenge-response**: you sign a short-lived message with your partner wallet's private key, and exchange the signature for a key. GRAIL stores only a hash — raw keys are shown once.

The two endpoints in the auth flow are the only `/v1/*` routes that do not themselves require authentication. They are rate-limited to **10 requests per minute per IP**.

## Prerequisites

Before you start, you need:

* Your **partner ID** (UUID) — given to you by ORO at onboarding
* Your **partner wallet keypair** — the private key that signs the challenge. This must correspond to a wallet registered under your partner (the wallet is the one ORO whitelists on-chain).
* A way to sign a UTF-8 message with Ed25519 and produce a **base64** signature. The snippet below uses `tweetnacl`.

<Warning>
  The signature format is **base64**, not base58. Submitting base58 produces `400 invalid_signature`. This is the most common onboarding mistake.
</Warning>

## The flow at a glance

```
1. POST /v1/auth/challenge { wallet_address, partner_id }
         ↓
2. GRAIL returns { challenge_id, message, expires_at }
         ↓
3. You sign `message` with the partner wallet's private key → base64 signature
         ↓
4. POST /v1/auth/api-key { challenge_id, signature, key_name }
         ↓
5. GRAIL returns the raw api_key — SHOWN ONCE. Store it.
```

Challenges expire after **2 minutes**. Complete signing and exchange before then.

## Step 1 — Request a challenge

```bash theme={null}
BASE=https://grail-stack-dev.onrender.com
PARTNER_WALLET=Fd31QxW7RRZwvMfNnhNaPvczJpMh7wyzBTWvtMA66wjN
PARTNER_ID=d8a8df53-36ca-4f4d-96ef-cf0a49d51e5d

curl -s -X POST "$BASE/v1/auth/challenge" \
  -H "Content-Type: application/json" \
  -d "{\"wallet_address\":\"$PARTNER_WALLET\",\"partner_id\":\"$PARTNER_ID\"}"
```

Response:

```json theme={null}
{
  "challenge_id": "ch_9b5a3a12-7b89-4d6c-9f6a-18c52d3f9e3a",
  "message": "Sign this message to generate an API key for GRAIL: ch_9b5a3a12-7b89-4d6c-9f6a-18c52d3f9e3a",
  "expires_at": "2026-04-19T12:32:00.000Z"
}
```

Save `challenge_id` and the full `message` string.

## Step 2 — Sign the challenge message

Sign the `message` (UTF-8 bytes) with the partner wallet's Ed25519 private key. Encode the signature as **base64**.

```typescript theme={null}
import { Keypair } from '@solana/web3.js';
import nacl from 'tweetnacl';
import bs58 from 'bs58';

// Load your partner keypair — secret in base58 is the common CLI export format
const partner = Keypair.fromSecretKey(bs58.decode(PARTNER_SECRET_BASE58));

const message = "Sign this message to generate an API key for GRAIL: ch_9b5a3a12-7b89-4d6c-9f6a-18c52d3f9e3a";
const messageBytes = new TextEncoder().encode(message);

const sigBytes = nacl.sign.detached(messageBytes, partner.secretKey);
const signatureBase64 = Buffer.from(sigBytes).toString("base64");

console.log(signatureBase64);
```

<Tip>
  You can use any Ed25519 signing library in any language — `tweetnacl` is just the common JS choice. The only hard requirement is that the final output is base64 of the 64-byte signature.
</Tip>

## Step 3 — Exchange the signature for an API key

```bash theme={null}
curl -s -X POST "$BASE/v1/auth/api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "challenge_id": "ch_9b5a3a12-7b89-4d6c-9f6a-18c52d3f9e3a",
    "signature": "<base64 from Step 2>",
    "key_name": "integration test"
  }'
```

Response:

```json theme={null}
{
  "api_key": "grail_partner_7f3a...<64 hex chars>",
  "key_id": "0c9bd7e4-6b2e-4f3a-a9a7-1f6e5d4c3b2a",
  "scope": "PARTNER",
  "key_name": "integration test",
  "wallet_address": "Fd31QxW7RRZwvMfNnhNaPvczJpMh7wyzBTWvtMA66wjN",
  "created_at": "2026-04-19T12:30:45.123Z"
}
```

<Warning>
  `api_key` is shown **only once**. Store it somewhere you can retrieve it later (secret manager, encrypted env var). If lost, revoke it via [Revoke API Key](/api-reference/partner/revoke-api-key) and mint a new one.
</Warning>

## Step 4 — Verify the key works

Hit a protected endpoint. The denominations list is a safe one — read-only, no side effects:

```bash theme={null}
curl -s "$BASE/v1/denominations?country=PK" \
  -H "x-api-key: $API_KEY"
```

If you get a JSON body with a `denominations` array, you're authenticated. If you get `401 unauthorized`, check that the header name is `x-api-key` (lowercase, with hyphen) and that you pasted the full key including the `grail_partner_` prefix.

## Managing keys

### List your keys

```bash theme={null}
curl -s "$BASE/v1/partner/api-keys" -H "x-api-key: $API_KEY"
```

Returns metadata for every key minted against your partner's wallet (including revoked ones). Raw keys are never returned.

### Revoke a key

```bash theme={null}
curl -s -X POST "$BASE/v1/partner/api-keys/$KEY_ID/revoke" \
  -H "x-api-key: $API_KEY"
```

Revocation is immediate and permanent. Use this when a key is compromised or no longer needed.

<Tip>
  It's common to mint a separate key per environment or per deployed service (e.g., `prod-backend`, `staging-cron`). Label them clearly via `key_name` — the list endpoint shows the label, which makes tracking easier.
</Tip>

## Troubleshooting

| Symptom                                                        | Likely cause                                                                                                       |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `400 invalid_signature`                                        | Signature sent as base58 instead of base64                                                                         |
| `400 invalid_challenge`                                        | You're re-using a `challenge_id` that was already exchanged, or the challenge row doesn't exist                    |
| `400 challenge_expired`                                        | Took longer than 2 minutes between Step 1 and Step 3. Request a new challenge.                                     |
| `400 wallet_revoked`                                           | The partner wallet's status has been flipped to `revoked` by ORO. Contact support.                                 |
| `404 wallet_not_found` on Step 1                               | The wallet isn't registered under the supplied `partner_id`. Check you're using the correct pubkey and partner ID. |
| `429 rate_limited`                                             | More than 10 requests to `/v1/auth/*` from this IP in the last minute. Wait, then retry.                           |
| `401 unauthorized` on a protected endpoint after minting a key | Key may have been revoked, or the header name is wrong (must be `x-api-key`, lowercase with hyphen)                |

## Next steps

With your API key working, proceed to [Creating & Managing Users](/guides/creating-and-managing-users).
