Base URL
All endpoints are served from:/v1. The GET /health probe lives at the root.
Authentication
Every protected endpoint requires an API key passed in thex-api-key header.
PARTNER) issued to your partner wallet via a challenge-response flow — see Request API Key Challenge and the Authentication & Setup guide.
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: 100means 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: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 apartially_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 |
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
Authentication
Mint a partner API key via challenge-response.
Partner
List and revoke your partner’s API keys.
Users
Register and look up end-users under your partner.
Denominations
Look up redeemable physical gold denominations by country.
Trades
Buy and sell
$GOLD against USDC.Redemptions
Convert
$GOLD tokens into physical gold pickup.