Skip to main content

Overview

Every end-user that trades or redeems through your platform is registered in GRAIL as a user under your partner. GRAIL stores:
  • Your internal identifier for the user (partner_user_id)
  • The user’s Solana wallet address (unique across all GRAIL users)
  • A KYC record — a mix of required structured fields and free-form JSON
Only users with kyc_level: "full" can quote trades or redemptions. Partial or basic KYC is rejected at user creation.
GRAIL does not run KYC for you. You KYC users on your side, then push the verified record to GRAIL via the create endpoint. GRAIL stores the record and uses it to gate trading.

The KYC model

Core fields (structured columns, all required):
FieldTypeNotes
countrystringISO-3166-1 alpha-2 (PK, AE, SA, …). Must be active in GRAIL.
full_namestring1–255 characters.
kyc_providerstringLabel of your KYC provider (e.g., "sumsub", "manual"). Free-form.
kyc_levelstringMust be "full".
kyc_verified_atISO-8601When you completed KYC on your side.
Free-form (optional, JSON object):
  • kyc_data — any provider-specific fields you want to retain (e.g., document types, ID numbers, risk scores). Stored verbatim.

Advisory ID validation

If kyc_data includes both id_type and id_number, GRAIL looks up the country/ID-type pair in its registry and — if a regex is registered — validates id_number against it. For example, for Pakistan:
  • id_type: "CNIC"id_number must match \d{5}-\d{7}-\d (e.g., 12345-1234567-1)
This is advisory only: if the regex isn’t registered for that country/ID-type, no validation happens. GRAIL does not verify ID-number authenticity — only format.

Step 1 — Look up supported countries

Before registering users, make sure the user’s country is supported. The list is stable; you only need to do this once. If a country you need isn’t supported, contact ORO.
There is no public endpoint to list countries in the partner API today. ORO seeds the country list (e.g., PK, AE, SA on devnet). If you submit a user with an unsupported country, you’ll get 400 invalid_request: Unsupported or inactive country.

Step 2 — Create the user

BASE=https://grail-stack-dev.onrender.com

curl -s -X POST "$BASE/v1/users" \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "partner_internal_user_001",
    "wallet_address": "2u7vVGJCTtsijCqWJNCEknVkuLjeU7Rd4PMg4dXuTZGx",
    "kyc": {
      "country": "PK",
      "full_name": "Alice Example",
      "kyc_provider": "manual",
      "kyc_level": "full",
      "kyc_verified_at": "2026-04-17T00:00:00Z",
      "kyc_data": {
        "id_type": "CNIC",
        "id_number": "12345-1234567-1"
      }
    }
  }'
Response:
{
  "grail_user_id": "gu_6b60956e-a8ee-4de2-8128-04c7fdf633c3",
  "user_id": "partner_internal_user_001",
  "wallet_address": "2u7vVGJCTtsijCqWJNCEknVkuLjeU7Rd4PMg4dXuTZGx",
  "created_at": "2026-04-17T10:15:30.000Z"
}
Save the grail_user_id (prefixed gu_). You need it for every subsequent trade and redemption call. Your own user_id is retained (as partner_user_id) but GRAIL’s API always keys off grail_user_id.

Uniqueness rules

GRAIL enforces two uniqueness constraints:
  1. wallet_address is unique globally across all GRAIL users (any partner). You cannot register the same Solana wallet twice.
  2. user_id is unique within your partner. Two partners may each have a user with user_id: "user_001", but you cannot have two.
Violations return 409 user_already_exists.

Step 3 — Look up a user

curl -s "$BASE/v1/users/gu_6b60956e-a8ee-4de2-8128-04c7fdf633c3" \
  -H "x-api-key: $API_KEY"
Response:
{
  "grail_user_id": "gu_6b60956e-a8ee-4de2-8128-04c7fdf633c3",
  "user_id": "partner_internal_user_001",
  "wallet_address": "2u7vVGJCTtsijCqWJNCEknVkuLjeU7Rd4PMg4dXuTZGx",
  "status": "active",
  "created_at": "2026-04-17T10:15:30.000Z",
  "kyc": {
    "country": "PK",
    "full_name": "Alice Example",
    "kyc_level": "full",
    "kyc_provider": "manual",
    "kyc_verified_at": "2026-04-17T00:00:00.000Z",
    "kyc_data": {
      "id_type": "CNIC",
      "id_number": "12345-1234567-1"
    }
  }
}
Users belonging to a different partner return 403 partner_mismatch — each partner can only see its own users.

Trade / redemption prerequisites

The following gate every buy, sell, and redemption call. If a user fails any of them, the quote is rejected before the transaction is built:
  1. User must exist (404 user_not_found if not)
  2. User must belong to the authenticated partner (403 partner_mismatch)
  3. User status must be "active" (403 user_suspended)
  4. User kyc_level must be "full" (400 kyc_level_insufficient)
  5. Your partner must have an on-chain config set up (400 onchain_config_missing) — this is set up by ORO at onboarding
  6. Your partner must have a registered wallet (400 wallet_missing) — also set up by ORO
In practice, (5) and (6) are true from day one if you were onboarded correctly. Most user-facing failures are either (3) or (4).

Suspending users

User suspension is not a partner-facing API today — suspensions are performed by ORO’s admin interface. If you need a user blocked, contact ORO.

Next steps

With a KYC-full user registered, proceed to Buying Gold to execute your first trade.