> ## 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.

# Create API Key

> Step 2 of the API key mint flow. Exchanges a signed challenge for a PARTNER scope API key.

## Overview

Submits the signature produced in [Request Challenge](/api-reference/authentication/request-challenge) to mint a new API key. On success, the raw API key is returned **exactly once** — store it somewhere safe. GRAIL only retains a hash.

<Warning>
  The returned `api_key` is shown **only once**. It cannot be retrieved later — if lost, revoke it and mint a new one.
</Warning>

<Note>
  The signature must be a **base64-encoded** Ed25519 signature of the challenge `message` (the full string returned by Step 1). Do not send base58 — that produces `400 invalid_signature`. See the [Authentication & Setup guide](/guides/authentication-and-setup) for a signer snippet.
</Note>

<Note>
  This endpoint is **rate-limited to 10 requests per minute per IP**. Exceeding the limit returns `429 rate_limited`.
</Note>

## Request Body

<ParamField body="challenge_id" type="string" required>
  The `challenge_id` returned from `POST /v1/auth/challenge`.
</ParamField>

<ParamField body="signature" type="string" required>
  Base64-encoded Ed25519 signature of the challenge `message`, signed by the partner wallet's private key.
</ParamField>

<ParamField body="key_name" type="string" required>
  Human-readable label for this key (e.g., `"production integration"`, `"staging tests"`). Shown in the key list.
</ParamField>

## Response

<ResponseField name="api_key" type="string">
  The raw API key. Format: `grail_partner_<64-hex>`. Pass this in the `x-api-key` header on all subsequent requests. **Shown only once.**
</ResponseField>

<ResponseField name="key_id" type="string">
  UUID of the key record. Use this to revoke the key later.
</ResponseField>

<ResponseField name="scope" type="string">
  Always `"PARTNER"` for keys minted via this endpoint.
</ResponseField>

<ResponseField name="key_name" type="string">
  Echo of the `key_name` you supplied.
</ResponseField>

<ResponseField name="wallet_address" type="string">
  The partner wallet that signed the challenge.
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO-8601 timestamp of key creation.
</ResponseField>

## Errors

| HTTP | `error`             | When                                                                              |
| ---- | ------------------- | --------------------------------------------------------------------------------- |
| 400  | `invalid_request`   | Missing `challenge_id`, `signature`, or `key_name`                                |
| 400  | `invalid_challenge` | Challenge not found, or already used                                              |
| 400  | `challenge_expired` | Challenge older than 2 minutes                                                    |
| 400  | `wallet_revoked`    | Partner wallet's status is `revoked`                                              |
| 400  | `invalid_signature` | Ed25519 verification failed — most often because the signature was sent as base58 |
| 429  | `rate_limited`      | More than 10 requests in the last minute from this IP                             |

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://grail-stack-dev.onrender.com/v1/auth/api-key \
    -H "Content-Type: application/json" \
    -d '{
      "challenge_id": "ch_9b5a3a12-7b89-4d6c-9f6a-18c52d3f9e3a",
      "signature": "r8JvAw...base64...Q==",
      "key_name": "integration test"
    }'
  ```
</RequestExample>

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