Custodial purchase

POST /api/v1/mystery/purchase packs:purchase

The custodial alternative to prepare/submit: the pack is paid from the end-user's CardOS wallet balance (see Custodial wallet), so there is no transaction for anyone to sign and no chain awareness in your stack at all. Returns 202 — the purchase is reserved and revealed asynchronously; poll GET /purchase/:id or take the purchase.fulfilled webhook. Which model your key uses is your architectural fork in the road: custodial needs funded wallets but no signing UX, prepare/submit needs neither balance nor deposit flow but every purchase is signed by the end-user. GET /api/v1/info tells you how your key is provisioned.

Exactly ONE pack per call: quantity is fixed at 1 and anything else is rejected with unsupported_quantity. A multi-pull is N sequential calls, each with its OWN Idempotency-Key.

An Idempotency-Key is bound to its first (user, tier) pair — reusing a key with a different tier_id is a 409 idempotency_mismatch, so mint keys per (user, tier, intent), e.g. a UUID minted when the user taps buy.

Try it POST /api/v1/mystery/purchase money

These inputs are shared across all docs pages, so an id entered here carries over.

request body
object · 2 keys
{
  "external_user_id": "demo-explorer",
  "tier_id": 1
}
response

Not run yet. Press Run to make a live call against https://service.rip.fun (through this demo's server-side proxy; the API key never reaches the browser).

tracks the inputs above
curl -X POST 'https://service.rip.fun/api/v1/mystery/purchase' \
  -H 'X-API-Key: rip_…' \
  -H 'Idempotency-Key: de1424ca-1fa7-4b8a-95c2-32c7403eb532' \
  -H 'Content-Type: application/json' \
  -d '{"external_user_id":"demo-explorer","tier_id":1}'

Request fields

FieldTypeRequiredDescription
Idempotency-KeyheaderyesYour unique id for this purchase intent — replays return the original 202 instead of double-charging
external_user_id / wallet_addressstringyesWhose wallet balance pays (one of the two)
tier_idnumberyesTier to purchase
max_price_usdcstringPrice cap; defaults to the current tier price

Response fields (data)

FieldDescription
id / memo / status / custodyPurchase id, "{partner-slug}-{id}" tag, RESERVED→SUBMITTED→FULFILLED, and custody — uppercase, "CUSTODIAL" here (the prepare/submit path records "NON_CUSTODIAL"). memo is absent entirely when your key has no slug
price / price_usdcWhat was reserved from the wallet (micros + decimal)
onchain_request_id / transaction_hash / purchaser_addressOn-chain linkage, filled in as the reveal progresses
reserved_at / submitted_at / fulfilled_at / created_atLifecycle timestamps

Errors

StatusCodeWhen
400missing_idempotency_keyno Idempotency-Key header (or idempotency_key body field)
400invalid_tiertier_id missing or unknown
400unsupported_quantityquantity other than 1 — send N calls for N packs
409idempotency_mismatchthe Idempotency-Key was already used for a different tier
402insufficient_fundsthe end-user's wallet balance cannot cover the tier price
503relayer_disabledcustodial purchasing is temporarily paused

See Errors for the response envelope and the full code list.

Flow

  1. Partner: fund the end-user's wallet (POST /wallet/deposit-address → user deposits, or your own top-up flow)
  2. Partner: POST /purchase with Idempotency-Key + tier_id + the user identity → 202 RESERVED
  3. CardOS: pays from the wallet balance, submits the reveal on-chain, and fulfills
  4. Partner: poll GET /purchase/:id until FULFILLED (or take the purchase.fulfilled webhook, which includes the revealed items[])