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

# API Overview

> Shape, conventions, and authentication model for the GRAIL API.

## Base URL

All endpoints are served from:

```
https://grail-stack-dev.onrender.com
```

The path prefix for every versioned endpoint is `/v1`. The `GET /health` probe lives at the root.

## Authentication

Every protected endpoint requires an API key passed in the `x-api-key` header.

```bash theme={null}
curl -H "x-api-key: grail_partner_<hex>" \
  https://grail-stack-dev.onrender.com/v1/denominations?country=PK
```

There is one partner scope (`PARTNER`) issued to your partner wallet via a challenge-response flow — see [Request API Key Challenge](/api-reference/authentication/request-challenge) and the [Authentication & Setup guide](/guides/authentication-and-setup).

Two endpoints do **not** require auth: `GET /health` and the two endpoints under `/v1/auth/*` (which are how you obtain a key in the first place). The `/v1/auth/*` endpoints are rate-limited at **10 requests / minute / IP**; exceeding returns `429 rate_limited`.

## Request & response conventions

* **Content type**: JSON for both request and response bodies (`Content-Type: application/json`).
* **Field naming**: all request and response fields use `snake_case`.
* **IDs**: prefixed identifiers — `gu_` (user), `trd_` (trade), `red_` (redemption), `ch_` (challenge).
* **Amounts**: human-readable decimals (e.g., `usdc_amount: 100` means 100 USDC, not 100 microUSDC). The server converts to on-chain smallest units when building transactions.
* **Timestamps**: ISO-8601 with timezone (e.g., `2026-04-17T10:30:00.000Z`).
* **Solana signatures / partially-signed transactions**: always **base64**.

## Error responses

All errors return a JSON body of the form:

```json theme={null}
{
  "error": "invalid_request",
  "message": "usdc_amount must be a positive number",
  "field": "usdc_amount"
}
```

`field` is included only when the error is tied to a specific input field.

### Common error codes

| HTTP | `error`                    | When you'll see it                                                              |
| ---- | -------------------------- | ------------------------------------------------------------------------------- |
| 400  | `invalid_request`          | Missing or malformed fields in the request body                                 |
| 400  | `invalid_wallet`           | A Solana address isn't a valid pubkey                                           |
| 400  | `invalid_signature`        | Auth signature verification failed (most often: sent base58 instead of base64)  |
| 400  | `invalid_challenge`        | Auth challenge not found or already used                                        |
| 400  | `challenge_expired`        | Auth challenge older than 2 minutes                                             |
| 400  | `invalid_denomination`     | Denomination ID not found or inactive                                           |
| 400  | `invalid_city`             | City doesn't match the denomination's city                                      |
| 400  | `kyc_level_insufficient`   | User KYC level is not `full`                                                    |
| 400  | `broadcast_failed`         | Solana RPC rejected the transaction — the message carries the underlying reason |
| 400  | `cancellation_not_allowed` | Redemption has moved past `submitted`                                           |
| 401  | `unauthorized`             | Missing or invalid `x-api-key`                                                  |
| 403  | `insufficient_scope`       | API key does not have the required scope                                        |
| 403  | `partner_mismatch`         | Resource belongs to a different partner                                         |
| 403  | `partner_suspended`        | Partner account is suspended                                                    |
| 403  | `user_suspended`           | User account is suspended                                                       |
| 404  | `user_not_found`           | Unknown `grail_user_id`                                                         |
| 404  | `trade_not_found`          | Trade row doesn't exist (often: indexer hasn't written it yet — wait \~15s)     |
| 404  | `redemption_not_found`     | Redemption is still at the internal `quoted` state, or doesn't exist            |
| 409  | `user_already_exists`      | `wallet_address` or `partner_user_id` already registered                        |
| 429  | `rate_limited`             | Auth endpoint exceeded 10 req / minute / IP                                     |
| 503  | `pricing_unavailable`      | Gold price oracle unreachable or returned stale data                            |

## Transaction model

Buy, sell, and redemption flows all return a `partially_signed_transaction` (base64) in their quote response. You co-sign with the remaining required signer(s), then submit — either via the corresponding `/submit` endpoint (pure passthrough broadcast) or directly to any Solana RPC.

The partially-signed transaction bakes in a Solana `recentBlockhash` that expires after **\~60 seconds**. If you take too long to sign and submit, you'll get `400 broadcast_failed: Blockhash not found` — re-quote and try again.

**Signers required:**

| Flow       | Signers                         |
| ---------- | ------------------------------- |
| Buy        | GRAIL (server) + partner + user |
| Sell       | GRAIL (server) + partner + user |
| Redemption | GRAIL (server) + user           |

GRAIL always partial-signs first and returns the serialized transaction. You add the remaining signatures on your side and submit.

## Indexer-driven state

Trade and redemption rows are **not** written at submit time. An indexer watches Solana for the signed transactions and writes the authoritative row once the transaction confirms on-chain.

Practical implication: right after you submit, `GET /v1/trades/:id` returns `404` for \~10–15 seconds. The same applies to redemptions — `GET /v1/redemptions/:id` returns `404` while the redemption is still at the internal `quoted` state, and only becomes visible once the indexer advances it to `submitted`.

## API groups

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/api-reference/authentication/request-challenge">
    Mint a partner API key via challenge-response.
  </Card>

  <Card title="Partner" icon="briefcase" href="/api-reference/partner/list-api-keys">
    List and revoke your partner's API keys.
  </Card>

  <Card title="Users" icon="users" href="/api-reference/users/create-user">
    Register and look up end-users under your partner.
  </Card>

  <Card title="Denominations" icon="coins" href="/api-reference/denominations/list-denominations">
    Look up redeemable physical gold denominations by country.
  </Card>

  <Card title="Trades" icon="chart-line" href="/api-reference/trades/quote-buy">
    Buy and sell `$GOLD` against USDC.
  </Card>

  <Card title="Redemptions" icon="truck" href="/api-reference/redemptions/quote-redemption">
    Convert `$GOLD` tokens into physical gold pickup.
  </Card>
</CardGroup>
