{"servers":[{"url":"https://api.phygitals.com/api/v1","description":"Production"},{"url":"https://api.phygitals.com/api/partner/v1","description":"Production (long-form alias)"},{"url":"https://api.phygitals.com/api/partner/sandbox/v1","description":"Sandbox"},{"url":"https://api.phygitals.com/_/api/v1","description":"Legacy (back-compat)"}],"info":{"title":"Phygitals Partner API","version":"1.0.0","description":"The Phygitals Partner API lets you embed Phygitals' pack, buyback, and physical-shipping flows\ninto your own platform: browse and buy packs, look up the items inside\nthem, sell items back to the pool, and quote, request, and track physical shipments. Everything\nis REST over HTTPS with JSON request and response bodies. The sidebar groups the operations into\nStorefront (read + buy) and Events (outbound webhooks).\n\n## Platform\n\n**The infrastructure for digital collectibles.** Sell digital packs backed by real, graded physical\ncards. An end user buys a pack, watches it reveal instantly, holds the card in an insured vault, and\nlater ships it or sells it back, all through this one API. These are the same rails the main\nPhygitals storefront runs on: **$350M+** GMV, **2.8M+** transactions, **100K+** users, and a\npack-configured buyback (often around 85-90% of FMV; see each pack's `buyback_percent`).\n\n### The four-step flow\n\n1. **Buy a pack.** Debited from your **prepaid partner ledger**, not from end-user card/crypto on\n   this API. Each pack is backed by real physical cards. Start with `POST /vm/buy/init`; look it up\n   with `GET /vm/sessions/{session_id}` (or the `purchase.settled` webhook).\n2. **Instant reveal.** The picked cards are known the moment the pack opens. `buy/init` returns them\n   synchronously (there is no `pending` state to poll for).\n3. **Securely vaulted.** Cards sit in top-tier insured US facilities; the user owns the digital\n   representation until they act on it. Holdings are surfaced by `GET /inventory/{user_id}`.\n4. **Ship or sell.** Redeem for worldwide physical shipping (`POST /ship/quote`, then\n   `POST /ship/request`, then `GET /ship/order/{order_id}`), or sell back at the pack's\n   `buyback_percent` of FMV via `POST /vm/buyback`.\n\n### Vault, pricing & logistics\n\nEvery digital collectible is 1:1 backed by a real, graded card in an insured US vault. Phygitals\noperates the full physical stack end to end, so you don't store or fulfill anything yourself.\n\n|                    |                                                                             |\n| ------------------ | --------------------------------------------------------------------------- |\n| **Vault partners** | [Alt](https://www.alt.xyz/) (primary), PSA, Fanatics                        |\n| **Pricing**        | Alt's live FMV feed powers `buyback_price` on every item and every sellback |\n| **Insurance**      | Full coverage across all stored items, climate-controlled facilities        |\n| **Fulfillment**    | Worldwide shipping, tracking and insurance included on every redemption     |\n\n**Phygitals funds the buybacks.** Sellback liquidity is underwritten by Phygitals, not the partner.\nWhen a user calls `POST /vm/buyback`, the credit hits your partner ledger. You don't post capital,\nrun a secondary market, or carry sellback risk.\n\n**White-label.** Phygitals is both a direct-to-consumer platform and infrastructure for partner\nbrands. A launch can be API-only (you keep your own frontend and integrate these endpoints) or a\nfully managed build (Phygitals designs the branded storefront and backend on the same rails). Every\npartner gets a dedicated point of contact. Partnership inquiries:\n[hello@phygitals.com](mailto:hello@phygitals.com).\n\n## Concepts\n\n- **Pack**: a themed, purchasable bundle (the id the `claw_ids` filter references). Buying one\n  draws items from it provably-fair and returns them. There is no by-id `GET` for a single pack;\n  list `GET /vm/available` and filter client-side. `id` is what you pass to `POST /vm/buy/init`,\n  and `slug` is what addresses chase cards and backing inventory. Slug is typed nullable for\n  historical reasons but is **always set** on partner-visible packs, so treat it as required.\n- **Item**: an individual card pulled from a pack. You can look one up, ship it, or sell it\n  back to the pool. Every holding is shippable. Fields such as `mint_address`, `collection_address`,\n  `token_standard`, and a purchase's `tx_hash` are identifiers (or a synthetic session id): **no\n  partner API call mints, transfers, or settles anything on-chain**, and end users of this API do\n  not need a wallet.\n- **Chase card**: one of a pack's headline high-value cards, its \"top hits\". These are the marquee,\n  long-odds pulls a pack can yield, such as a numbered rookie autograph or a low-population\n  parallel. `GET /vm/chase/{slug}` returns them, so you\n  can render a \"what could I pull?\" preview before a buyer purchases. They're the best-case outcomes\n  in the pack's pool, not a guarantee that any given buy lands one. (`GET /vm/available` always\n  returns `chase: []` by design; there is no inline or bulk chase payload. Use the chase\n  endpoint for top hits.)\n- **Buyback**: selling an item back to the pool at the pack's `buyback_percent` of FMV for instant\n  liquidity, via `POST /vm/buyback` (one item per call). Phygitals funds it, and the partner posts no\n  capital. The offer on each holding expires **7 days** after purchase (`buyback_expires_at`); after\n  that `POST /vm/buyback` rejects it with `ITEM_EXPIRED`. The window is uniform and is not shortened\n  by this API, so if your storefront needs a shorter deadline, enforce it yourself before calling\n  buyback. **There is no void, refund, or cancel** for a settled purchase; a buy that returned\n  success stays settled.\n- **Odds**: one `rarity_distribution` curve per pack. To offer several risk profiles at the same\n  EV, Phygitals can configure multiple packs over the same items with different curves, and you can\n  present those as one storefront pack with multiple modes. There is no categories catalog\n  endpoint; group packs yourself from `category` / `categories[]` on `/vm/available`. Ignore\n  `variants`, `variant_of`, `pulls_per_voucher`, `type` (`CORE` / `EBAY`), and `repack`, which are\n  leftover fields and mean nothing to a partner storefront.\n- **Vault**: the insured US facility (Alt / PSA / Fanatics) holding the physical card behind each\n  item until the user ships or sells it.\n- **Partner ledger**: prepaid B2B float Phygitals debits on buy/ship and credits on buyback. Live\n  partner settlement is ledger-based: a buy debits `mint_price × amount`; a ship request debits\n  only the chosen `ShipRate.total_cost` (item FMV is not charged on ship); a buyback credits the\n  frozen `buyback_price`. Phygitals sets each partner's credit limit, typically high enough that\n  `402 INSUFFICIENT_BALANCE` does not appear in ordinary traffic. There is no partner-API endpoint\n  to read the balance, list ledger entries, or top up; that lives in the partner dashboard. These\n  webhooks never move money, and any 2xx only acknowledges the notification.\n- **Seed commitment**: a server seed we bind ourselves to _before_ a draw, via\n  `POST /vm/buy/commit`, which returns its hash and a single-use `commitment_id` to spend on the buy.\n  Show `server_seed_hash` to the end user, and keep `commitment_id` on your backend as a secret. See\n  [Provable fairness](#provable-fairness).\n\n## Getting an API key\n\nAccess is provisioned per partner: you need a **Partner** account on Phygitals before you can\ncall the API. Once your organization is set up, an **owner** of your partner mints keys from the\npartner dashboard (the \"Docs & setup\" panel):\n\n- **Sandbox keys** (`pk_sandbox_…`): a partner owner can create these self-serve. Start here.\n- **Live keys** (`pk_live_…`): issued by Phygitals only. A partner owner **cannot** self-issue\n  one, so contact your Phygitals partner manager (or support) to enable production access. Ask for a\n  small live allocation when you need to exercise the fairness protocol, live-only duplicate\n  guards, or inventory contention that [sandbox](#sandbox) does not reproduce, and do not book\n  real shipments while testing.\n\nThe secret is shown **exactly once**, at creation, so store it somewhere safe. If you lose it, revoke\nthe key and mint a new one. Don't have a partner account yet? Talk to your Phygitals contact, or\nemail [hello@phygitals.com](mailto:hello@phygitals.com), to get onboarded.\n\n## Authentication\n\nEvery request must be authenticated. Two credentials are accepted, and either one satisfies a\nrequest:\n\n- **`X-API-Key: <your-key>`**: your partner API key. This is the primary credential for\n  server-to-server integrations, and the only way to reach live data.\n- **`Authorization: Bearer <token>`**: a signed-in partner-dashboard session token for a user\n  belonging to your partner. Session auth is **sandbox-only**; live access requires a\n  `pk_live_…` API key.\n\n```sh\ncurl https://api.phygitals.com/api/v1/vm/available \\\n  -H \"X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx\"\n```\n\n## Base URLs & environments\n\n**Production is `https://api.phygitals.com/api/v1`.** That's the base URL to build against, and\nthe one every example here uses. The raw OpenAPI document behind this reference is served on it\ntoo, at [`https://api.phygitals.com/api/v1/spec.json`](https://api.phygitals.com/api/v1/spec.json).\n\nThe same operations are mounted on four prefixes; pick the one for the environment you want.\n\n| Environment            | Base URL                                           | Settlement                                      |\n| ---------------------- | -------------------------------------------------- | ----------------------------------------------- |\n| **Production (live)**  | `https://api.phygitals.com/api/v1`                 | Prepaid partner ledger + real vaulted inventory |\n| Production (long form) | `https://api.phygitals.com/api/partner/v1`         | Same as production                              |\n| Sandbox                | `https://api.phygitals.com/api/partner/sandbox/v1` | Fully simulated (no ledger / vault / chain)     |\n| Legacy                 | `https://api.phygitals.com/_/api/v1`               | Follows the key's mode                          |\n\n**Sandbox** runs the full API against a simulated backend. Buys, buybacks, and settlement are\nemulated and nothing touches the vault or chain, so you can build and test end to end for free. The\nsandbox base URL forces sandbox behavior for any credential, so even a live key is safely\ndowngraded there.\n\nThe **production** base URL requires a live key: a sandbox-scoped credential (a `pk_sandbox_…`\nkey, or a dashboard session) is rejected with `403` rather than being accepted against live data.\n`https://api.phygitals.com/api/partner/v1` is a **long-form alias** of\n`https://api.phygitals.com/api/v1`, kept for integrations that already call it; prefer the short\nform. The **legacy** `https://api.phygitals.com/_/api/v1` prefix also keeps working and follows the\nkey's own mode, but new integrations should use `https://api.phygitals.com/api/v1` (or\n`https://api.phygitals.com/api/partner/sandbox/v1` to start in sandbox).\n\n## Conventions\n\n- **Field names are snake_case** in every request and response body.\n- **Money types vary by field, and the split is intentional and stable.** Pack listing prices and\n  pack-editor rows use **decimal strings** (e.g. `\"25\"`); item `fmv` / `buyback_price`,\n  buy/buyback `amount`, and ship rate costs are **JSON numbers**. Never invent floats where the\n  schema uses a string.\n  **Timestamps are ISO-8601 / RFC-3339** UTC strings (e.g. `\"2026-01-15T09:30:00Z\"`).\n- **Idempotency** is supported on every write bar one (`POST /vm/buy/commit`); see the dedicated\n  section below.\n- **Unpaginated list ceilings.** `GET /vm/available`, `GET /inventory/{user_id}`, and\n  `GET /ship/orders` return a bare array (no cursor). Each is capped at **1000** rows. Cursor paging\n  lives on other routes (`GET /packs/{slug}/items`, `GET /vm/sessions`, `GET /vm/buybacks`,\n  `GET /vm/fairness-proofs`); these three keep their current shape for back-compat.\n\n## Rate limits\n\nTraffic is limited to **500 requests per second per API key**. Exceeding that returns `429`. Honor\na `Retry-After` header when one is present; otherwise back off exponentially. The `429` mentioned\non webhook operations is the opposite direction: it is what _your_ receiver may return so we retry\nthe delivery, not an inbound partner-API quota.\n\n## Idempotency\n\nEvery mutating request (buy, buyback, create-pack, and so on) can carry an **idempotency key** so a\ndropped connection can't double-execute. Send one on every write.\n\n- **Two equivalent channels.** Supply the key as an `Idempotency-Key: <token>` HTTP **header** or as\n  an `idempotency_key` **field** in the JSON body; they are interchangeable. If you send **both**,\n  the **body field wins** and the header is ignored.\n- **Replay.** Retrying with the same key **and** the same request body replays the original response\n  verbatim, carrying an `idempotency-replayed: true` response header, instead of executing again.\n  Stored responses stay replayable for **24 hours**.\n- **Reuse conflict.** Reusing a key with a _different_ request body is rejected `422\nIDEMPOTENCY_KEY_REUSED`, a sign a key was recycled by mistake.\n- **In-flight duplicate.** The same key retried _before_ the first call has returned is rejected\n  `409 DUPLICATE_REQUEST`. Wait for the original to finish (or look the attempt up) rather than\n  retrying immediately.\n- **Commit-reveal buys retry identically.** Passing a `commitment_id` does not change any of the\n  above: a retry under a key that already bought is still `409 DUPLICATE_REQUEST`, even though the\n  commitment it names was spent by that same purchase. You never have to special-case the\n  commit-reveal path in your retry logic. `409 BUY_COMMITMENT_ALREADY_USED` means something\n  different: that commitment was already consumed by another attempt, either yours under _another_\n  key or whoever else held the id. It is covered under [Provable fairness](#provable-fairness). What\n  does change with a `commitment_id` is that retrying a genuinely failed buy needs a fresh one.\n- **Failed attempts don't stick.** A key whose only prior attempt _failed_ is not blocked: the retry\n  executes fresh, since nothing was charged. A `commitment_id` is the one thing a failed attempt does\n  consume, so retry with a fresh one (see [Provable fairness](#provable-fairness)).\n- **Looking up a past attempt.** Prefer `GET /vm/sessions/{session_id}` /\n  `GET /vm/sessions/by-idempotency-key/{idempotency_key}` and\n  `GET /vm/buybacks/{buyback_id}` / `GET /vm/buybacks/by-idempotency-key/{idempotency_key}`,\n  which are the long-term lookup surface. The older `POST /vm/buy/status` and\n  `POST /vm/buyback/status` stay supported for back-compat (deprecated, no removal date) and\n  accept the same `idempotency_key` you sent on the original write. There the key is a lookup\n  key, not a dedup token, so passing it never trips the reuse conflict above.\n- **Two exempt writes: `POST /vm/buy/commit` and `POST /vm/buy/open`.** Both ignore the key entirely,\n  and both are exempt for the same reason: each is one half of a two-call purchase, and ONE key per\n  purchase must be able to span both calls without the second being rejected\n  `422 IDEMPOTENCY_KEY_REUSED` for carrying the same key as the first. For the commit, a retry after\n  a dropped connection really does mint a second commitment — harmless, since an unspent commitment\n  reserves nothing and costs nothing; just use the `commitment_id` from the reply you did receive.\n  Send your purchase key on the buy; the commit doesn't need one. For the open, no key is needed\n  because the `pack_id` is itself the idempotency: a repeat open of the same pack always replays the\n  same cards, with or without a key, forever — not just for the 24-hour replay window.\n\n## Provable fairness\n\nEvery live pull is drawn from two seeds, a **server seed** we hold and a **client seed** you can\nsupply, and both are published once the buy has settled, so you or your end-user can re-derive the\noutcome instead of taking our word for it. The endpoints are grouped under the **Fairness** tag.\n\nThere are two modes, and they differ only in _when_ we bind ourselves to the server seed:\n\n- **Record of the draw** (the default): omit `commitment_id` and the server seed is minted at draw\n  time. The proof shows exactly what ran and is internally consistent, but the seed and its hash were\n  recorded together, so it cannot show we were committed to that seed beforehand.\n- **Commit-reveal**: call `POST /vm/buy/commit` first and we hand you `sha256(server_seed)` **before**\n  any draw runs, revealing the seed only once the buy it backs has settled. That the revealed seed\n  hashes to a value you were already holding is what rules out a seed chosen to suit the outcome.\n\nSend your own `client_seed` on the buy in either mode. Without one the platform picks **both** halves\nof the draw input, and a commitment then proves only that the hash predated the draw, not that the\ninput was outside our control.\n\n### Commit, buy, verify\n\n**1. Commit.** `POST /vm/buy/commit` takes no request body and returns `commitment_id` and\n`server_seed_hash`. Show that hash to your end-user, or record it, **now**: its existing before the\nbuy is the whole point. Keep `commitment_id` on your backend. It does not need to leave your servers,\nand it is a secret, since anyone presenting it can attempt to spend it. Commitments are single-use,\nnever expire with a live key, and ignore `Idempotency-Key` (see [Idempotency](#idempotency)), so one\nkey can cover both this call and the buy that spends it.\n\n**2. Buy.** `POST /vm/buy/init` with that `commitment_id` and your own `client_seed`. The draw runs on\nthe seed you already hold the hash of. The buy mints its **own** `session_id` and returns it; the\ncommitment id is never reused as a session id.\n\n**One commitment, one attempt.** The buy consumes the `commitment_id` before it draws, so it is spent\nwhether or not it settles. If the buy fails, because the pack sold out or your balance was short,\nthat commitment is gone and a retry gets `409 BUY_COMMITMENT_ALREADY_USED`. Mint a fresh one and buy\nagain; `POST /vm/buy/commit` is free and takes no body, so a replacement costs you nothing. Guards\nthat don't depend on the seed run first, so ordinary integration mistakes, such as an unknown or\ndisabled pack or an `amount` over `max_per_mint`, reject **before** the commitment is touched and\ncost you nothing.\n\n**3. Verify.** `GET /vm/sessions/{session_id}/fairness-proof` reveals `server_seed`. Three checks make\nit a commit-reveal rather than a record after the fact:\n\n- `sha256(server_seed)` reproduces the proof's `server_seed_hash`, **and** the `server_seed_hash`\n  step 1 handed you. Comparing against your own stored copy is what the whole flow rests on;\n  comparing only within the response proves nothing beyond internal consistency.\n- `committed_at` matches when `POST /vm/buy/commit` answered you. It is when we recorded the seed, which\n  on a committed buy is that call, but every draw records one, so the field's presence alone proves\n  nothing. Your own stored hash is the evidence, and this is a cross-check against it rather than a\n  substitute.\n- `client_seed` is the value you sent (when you sent none, it is the `session_id` we minted).\n\n```sh\n# 1. commit: returns { commitment_id, server_seed_hash }\ncurl -X POST https://api.phygitals.com/api/v1/vm/buy/commit \\\n  -H \"X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx\"\n\n# 2. buy with it: returns { session_id, nfts }\ncurl -X POST https://api.phygitals.com/api/v1/vm/buy/init \\\n  -H \"X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"<pack_id>\",\"amount\":1,\"user_id\":\"<your_user>\",\n       \"commitment_id\":\"<commitment_id>\",\"client_seed\":\"<your_seed>\"}'\n\n# 3. reveal: sha256(server_seed) must equal the hash from step 1\ncurl https://api.phygitals.com/api/v1/vm/sessions/<session_id>/fairness-proof \\\n  -H \"X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx\"\n```\n\nThen re-derive the picks themselves. Each `selections` entry records the values both steps of a draw\ngenerated: `tierRandom` for the rarity tier, then `rowSeed` for the item within it. **How that second\nstep resolves depends on the proof's `version`**, so branch on it rather than assuming. `v1` ranks the\neligible items by `md5(item_id + rowSeed)` and takes the smallest, recording the winner's key as\n`itemRandom`. `v2` records no per-item value at all and instead takes the candidate at offset\n`int(last 15 hex chars of rowSeed) mod N` of the pool sorted ascending by ordinal, publishing that\nslot as `chosenItemOrd`.\n\nBoth steps are independently reproducible. `GET /vm/sessions/{session_id}/fairness-proof` documents\nthe exact derivation and returns `candidate_pools`, the eligible set as it stood at draw time, per\nselection, which is what makes the item-within-tier pick checkable rather than merely reported.\n`GET /vm/fairness-proofs` pages through every proof you have recorded, newest first, if you would\nrather reconcile in bulk than per session; pools are on the per-session read, not the list.\n\n### Commitment errors\n\n- **`404 BUY_COMMITMENT_NOT_FOUND`**: unknown or invalid `commitment_id`. Treat each id as a secret\n  and mint a fresh one if you no longer have a usable copy.\n- **`409 BUY_COMMITMENT_ALREADY_USED`**: that commitment has already been consumed by a\n  `POST /vm/buy/init`, and one commitment backs one attempt. Mint a fresh one with\n  `POST /vm/buy/commit`; retrying this one cannot succeed. The consuming attempt need not have\n  **succeeded**, as covered in \"One commitment, one attempt\" above. Note this is **not** what a plain\n  retry of your own settled buy returns: a repeat under an `Idempotency-Key` that already bought is\n  `409 DUPLICATE_REQUEST` exactly as it would be without a commitment (see\n  [Idempotency](#idempotency)).\n\nSandbox simulates draws rather than running the fairness protocol, so it records no proof. See\n[Sandbox](#sandbox).\n\n## Sandbox\n\nThe sandbox runs the full API against a simulation, so you can integrate and test the whole buy,\ninventory, sellback and shipping flow without consuming inventory, moving money, or shipping real\ncards. Point at `/api/partner/sandbox/v1` (or use a `pk_sandbox_…` key). It mirrors the live\nsurface, with the same routes, the same request/response shapes, and the same error `code`s, but a\nfew behaviors differ, and you should know them before you rely on them:\n\n- **State is ephemeral.** Sandbox sessions, simulated inventory, buybacks, and shipping orders do\n  not persist across restarts or deploys. Treat every sandbox run as fresh.\n- **No real fulfillment.** Nothing touches the vault (Alt / PSA / Fanatics), the chain, a payment\n  processor, or a shipping carrier: buys, buybacks, settlement, and shipping rates are all\n  simulated. (Destination address validation is the one exception, since `POST /ship/quote` runs the\n  same real address check that live does.)\n- **Not a concurrency test bed.** Sandbox does not faithfully reproduce live contention when two\n  buyers race for the same card. Use live keys when you need to validate that behavior.\n- **Idempotency replay works; some live-only duplicate guards do not.** Retrying with the same\n  `Idempotency-Key` and body still replays the first response. Hard rejection of some concurrent\n  same-key duplicates is live-only, so test that case with a live key.\n- **Seed commitments are one-shot in both modes.** A `commitment_id` is consumed by the first\n  `POST /vm/buy/init` that names it, whether or not that buy succeeds, in sandbox exactly as in\n  live, so a retry after a simulated stock-out gets `409 BUY_COMMITMENT_ALREADY_USED` in both.\n- **Sandbox commitments can expire; live ones do not.** An outstanding sandbox `commitment_id` may\n  later return `404 BUY_COMMITMENT_NOT_FOUND`, so mint a fresh one rather than treating it as a bug.\n  Live commitments stay spendable until used.\n- **No provably-fair draw.** Sandbox pulls are simulated rather than drawn against the fairness\n  protocol: a `client_seed` you send to `POST /vm/buy/init` is accepted and then ignored, no\n  fairness proof is recorded, and the fairness endpoints return nothing for a sandbox session.\n  `POST /vm/buy/commit` still works, so you can wire the commit and buy call sequence end to end, but\n  there is no seed to reveal at the end of it. Verify the proof recipe itself against a live key; see\n  [Provable fairness](#provable-fairness).\n- **Sellback pricing is frozen at buy time, in both modes.** A sandbox `POST /vm/buyback` pays the\n  `buyback_price` fixed on the item when it was bought, and live does the same: it pays the FMV and\n  the pack's buyback percent as they stood at purchase, not today's. What differs is only that\n  sandbox values come out of the simulation, so don't assert on exact sandbox payouts.\n- **Shipping orders don't progress, and only `shipping.queued` ever fires.** A sandbox\n  `POST /ship/request` creates an order and leaves it there. `shipped_at` and `delivered_at` stay\n  null for the life of the process, since no carrier actually moves the card, and\n  `tracking_number` stays null where a live order fills it in once the vault vendor cuts a label.\n  The five later `shipping.*` events are driven by real fulfillment, so **a receiver for\n  `shipping.shipped` / `.delivered` / `.label_created` / `.cancelled` / `.failed` cannot be\n  exercised in sandbox**; only `shipping.queued` is. (`tracking_url` is not a sandbox difference:\n  it is never populated on `GET /ship/order/{order_id}` in either mode.) Do not book live shipments\n  to work around this during integration testing.\n- **Live key for the rest.** Fairness proofs, live-only concurrent-duplicate `409`s, and two buyers\n  racing one card all need a `pk_live_…` key. Ask your partner manager for a small credit allocation\n  when you are ready to exercise those; see [Getting an API key](#getting-an-api-key).\n- **Scoped by `user_id` only.** Sandbox state is isolated by the `user_id` you submit, while a live\n  key is additionally partner-scoped. Webhooks fire in both modes, so register a sandbox endpoint to\n  receive sandbox events (a sandbox event never reaches a live URL) and test your receiver for free,\n  within the `shipping.*` limit noted above.\n\n## Errors\n\nErrors use one consistent JSON shape and a conventional HTTP status:\n\n```json\n{ \"error\": \"Claw machine is out of stock\", \"code\": \"OUT_OF_STOCK\" }\n```\n\n`error` is a human-readable message. **`code` is a stable, machine-readable token, so branch on\n`code`, not the message text.** Input-validation failures use `code: \"VALIDATION_ERROR\"` and add\na `details` array listing every field problem at once:\n\n```json\n{\n  \"error\": \"Amount must be a whole number\",\n  \"code\": \"VALIDATION_ERROR\",\n  \"details\": [{ \"field\": \"amount\", \"message\": \"Amount must be a whole number\" }]\n}\n```\n\nStatuses follow the usual conventions: `400` bad request, `401` unauthenticated, `402`\ninsufficient prepaid balance, `403` wrong environment for the credential, `404` not found, `409`\nidempotency conflict, `422` unprocessable, `429` rate limited ([Rate limits](#rate-limits)). Each\noperation below documents the exact codes it can return.\n\n`409 MULTIPLE_PARTNER_MEMBERSHIPS` is a dashboard-session auth error (one signed-in user belongs to\nmore than one partner). Integrations that authenticate with `X-API-Key` never receive it, so ignore\nit on this surface.\n\n## Webhooks\n\nRather than poll, subscribe to outbound events (shipping lifecycle, purchase settlement/failure,\nand buyback settlement) by registering a URL in your partner dashboard. Phygitals then POSTs\neach event to your endpoint. Deliveries are HMAC-signed (`X-Phygitals-Signature`) and sent\nat-least-once with retries, so verify the signature and dedupe on the envelope's `id`, which is\nstable across redeliveries. Don't dedupe on `data.idempotency_key`, which is null when the\noriginating request sent no key. Any 2xx acknowledges the delivery; **nothing in your response moves\nmoney**. Register or update your endpoint under your partner dashboard → **Webhooks**\n(`/partners/<your-slug>/webhooks`, owner/editor only).\n\n**What fires today.** `purchase.settled` / `purchase.failed`, `buyback.settled`, the six\n`shipping.*` transitions, and `pack.purchased` — which fires **only** from `POST /vm/buy/pack`\n(a one-shot `POST /vm/buy/init` still emits exactly one `purchase.settled`), so you will never\nreceive it unless you use the two-step flow. There is **no** `buyback.failed`, no shipping-quote\nexpiry event, and no catalog lifecycle event (`enable` / `in_stock` / price / EV changes); poll\n`GET /vm/available` for those. Keep advertising against `enable` and `in_stock` rather than caching\nthe catalog as permanently buyable. **Build your receiver to acknowledge (2xx) and ignore event\ntypes it does not recognize**: an empty `events` subscription means \"all events, including types\nadded later\", so new types can appear as you adopt new endpoints — subscribe to explicit types if\nyou need a closed set.\n\n**Shipping exceptions** (`shipping.cancelled` / `shipping.failed`, and `cancelled` / `failed` on\n`GET /ship/order/{order_id}`) are uncommon, and typically mean an invalid address, a carrier\nexception, or customs on an international order. They are resolved **manually through customer\nsupport**, with replacement, refund, or compensation as the case requires. There is no inbound\ncredit or refund call; a webhook is a notification only.\n\n**Delivery ops.** Automatic retries use exponential backoff starting at 30s and doubling, capped at\n30 minutes between attempts, for up to 24 hours from when the event occurred. After that the\ndelivery is abandoned. Missed events can be resent from the partner dashboard. There is **no**\nsource-IP allowlist, so authenticate deliveries by verifying the HMAC. Rotating the signing secret\nfrom the dashboard invalidates the previous secret immediately, so swap the verifier first; an\noverlap window (old + new secret both valid) is not available yet.\n\nThe full payloads, and the registration walkthrough, are in the **Webhooks** section below.\n\n## Next steps\n\n1. Mint a sandbox key from your partner dashboard.\n2. Call `GET /api/partner/sandbox/v1/vm/available` to list packs.\n3. Walk a full buy, item and shipment flow in sandbox (orders will not progress past creation).\n4. Register a webhook and confirm you receive events (sandbox fires `shipping.queued` only).\n5. Ask Phygitals to enable a live key, with a small credit allocation first if you still need to\n   verify fairness proofs or live-only contention, then for production.\n\nFor long-form integration guides, see the linked **Partner integration guides**."},"externalDocs":{"url":"https://phygitals.mintlify.app/","description":"Partner integration guides"},"security":[{"apiKey":[]},{"bearerAuth":[]}],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Partner API key issued by Phygitals ops."},"bearerAuth":{"type":"http","scheme":"bearer","description":"Signed-in partner-dashboard session token (sandbox only)."}},"schemas":{"ShippingDetails":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Recipient full name."},"email":{"type":"string","description":"Recipient email (optional)."},"line_1":{"type":"string","minLength":1,"description":"Street address (house number + street)."},"line_2":{"type":"string","description":"Apartment / suite / unit (optional)."},"city":{"type":"string","minLength":1,"description":"City."},"region":{"type":"string","description":"State / province / region (`\"\"` when not applicable)."},"postal_code":{"type":"string","description":"Postal / ZIP code (`\"\"` when not applicable)."},"country":{"type":"string","minLength":1,"description":"Destination country (ISO-2, e.g. \"US\"). The ISO-2 requirement is enforced when you submit a shipment, not by this schema."},"phoneNumber":{"type":"string","description":"Recipient phone number (optional)."}},"required":["name","line_1","city","region","postal_code","country"],"title":"Shipping Details","description":"A shipping address, used as the destination on a `ship/quote` and echoed back as the address on a `ship/order`.","examples":[{"name":"Ada Lovelace","line_1":"123 Market St","line_2":"Suite 400","city":"San Francisco","region":"CA","postal_code":"94103","country":"US","email":"ada@example.com","phoneNumber":"+14155550100"}]},"ShipRate":{"type":"object","properties":{"id":{"type":"string","description":"Rate id; pass it to `POST /ship/request` to book this option."},"name":{"type":"string","description":"Human-readable rate summary."},"withdrawal_fees":{"type":"number","description":"Vault withdrawal fees in USD."},"shipping_cost":{"type":"number","description":"Shipping cost in USD."},"total_cost":{"type":"number","description":"Total charged in USD (withdrawal fees + shipping)."},"estimated_delivery":{"type":"string","description":"Free-form delivery-estimate text."},"notes":{"type":"string","description":"Additional notes about this rate."},"carrier":{"type":"string","description":"Carrier / fulfiller name (e.g. Phygitals for graded vault rates)."},"service":{"type":"string","description":"Service level (e.g. Graded Vault Fulfillment)."},"estimated_days_min":{"type":"number","description":"Lower bound of the delivery estimate, in days."},"estimated_days_max":{"type":"number","description":"Upper bound of the delivery estimate, in days."}},"required":["id","withdrawal_fees","shipping_cost","total_cost","estimated_delivery","notes","carrier","service","estimated_days_min","estimated_days_max"],"title":"Shipping Rate","description":"A purchasable shipping rate returned by `POST /ship/quote` / `vault.claim.priceEstimate`.","examples":[{"id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23_rate_a1b2c3d4-e5f6-4789-abcd-ef1234567890","carrier":"Phygitals","service":"Graded Vault Fulfillment","withdrawal_fees":0,"shipping_cost":20,"total_cost":20,"estimated_delivery":"3-5 business days","notes":"Alt vault domestic shipping: $20 flat fee per order (1 card). All prices in USD.","estimated_days_min":3,"estimated_days_max":5}]},"ShipOrder":{"type":"object","properties":{"order_id":{"type":"string","description":"Shipping order id."},"status":{"enum":["processing","transit","tracking_available","delivered","cancelled","failed"],"type":"string","description":"Current fulfillment status. `cancelled` and `failed` are uncommon (invalid address, carrier exception, customs) and are resolved through customer support; they do not credit the partner ledger."},"carrier":{"type":"string","description":"Booked carrier."},"service":{"type":"string","description":"Booked service level."},"tracking_number":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Shipment tracking number once the label is cut, else null."},"tracking_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Always null; there is no carrier integration on this flow, so there is no tracking URL to give."},"amount":{"type":"number","description":"Charged shipping cost."},"currency":{"type":"string","description":"Currency of `amount` (e.g. USD)."},"destination":{"$ref":"#/components/schemas/ShippingDetails"},"items":{"type":"array","items":{"type":"object","properties":{"item_id":{"type":"string","description":"Shipped item id."},"name":{"type":"string","description":"Item display name."},"image":{"type":"string","description":"Item front image URL (empty when unresolved)."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Item back image URL, or null."}},"required":["item_id","name","image","back_image"]},"description":"Items included in this shipment."},"created_at":{"type":"string","description":"When the order was created (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"shipped_at":{"anyOf":[{"type":"string","description":"An ISO 8601 instant string with a required UTC offset (e.g. 2023-01-15T13:45:30Z)","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},{"type":"null"}],"description":"When the order shipped (ISO 8601), or null."},"delivered_at":{"anyOf":[{"type":"string","description":"An ISO 8601 instant string with a required UTC offset (e.g. 2023-01-15T13:45:30Z)","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},{"type":"null"}],"description":"When the order was delivered (ISO 8601), or null."},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Failure reason when `status` is \"failed\", else null. Support-handled; not an automatic refund."}},"required":["order_id","status","carrier","service","tracking_number","tracking_url","amount","currency","destination","items","created_at","shipped_at","delivered_at","error_message"],"title":"Shipping Order","description":"A shipping order and its fulfillment state (`GET /ship/order/{order_id}`).","examples":[{"order_id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23","status":"tracking_available","carrier":"UPS","service":"Ground","tracking_number":"1Z999AA10123456784","tracking_url":null,"amount":20,"currency":"USD","destination":{"name":"Jamie Collector","line_1":"500 Terry A Francois Blvd","line_2":"Suite 300","city":"San Francisco","region":"CA","postal_code":"94158","country":"US"},"items":[{"item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null}],"created_at":"2026-07-14T14:20:00.000Z","shipped_at":"2026-07-14T18:45:00.000Z","delivered_at":null,"error_message":null}]},"ChaseCard":{"type":"object","properties":{"id":{"type":"string","description":"Card item id."},"name":{"type":"string","description":"Card display name."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"fmv":{"type":"number","description":"Fair market value in USD, rounded to cents."}},"required":["id","name","image","fmv"],"title":"Chase Card","description":"A high-value \"chase\" card highlighted on a pack.","examples":[{"id":"Atv5Xigpcf7EvDqKJxoyp8CxZeo8m8NZdjFkER8dEi7c","name":"2000 Pokemon Gym Challenge 1st Edition Holo Brock's Ninetales #3 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2FApFiQSzv1NCRUpSZtzedkKYhuYLyo91xpLb5FcMSsdVD-cropped","fmv":2182.12}]},"InventoryItem":{"type":"object","properties":{"id":{"type":"string","description":"Item id (the `EbayListing` id); pass it to `GET /card/{item_id}` for full detail."},"name":{"type":"string","description":"Card display name."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"fmv":{"type":"number","description":"Fair market value in USD, rounded to cents. On a pack's backing inventory this is the per-item value its EV is computed from; in the supply pool it is what the item would contribute to a pack. Same number, same column: the two routes just reach it from different directions."},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The item's OWN category (e.g. `pokemon`), or null when it carries none. This is what the repeatable `category` filter on `GET /inventory/available` matches against. Not to be confused with a pack's categories, which are a property of the pack rather than of any item in it."}},"required":["id","name","image","fmv","category"],"title":"Inventory Item","description":"One inventory item: a pack's backing row, or one item of the pack-eligible supply pool.","examples":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","fmv":425.17,"category":"pokemon"}]},"Pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The effective page size applied to this request (echoed back)."},"count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total number of records matching the request across all pages (not just this page's size)."},"has_more":{"type":"boolean","description":"True when a further page exists; fetch it by passing `next_cursor` back as `cursor`."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque keyset cursor for the next page; pass it back as `cursor`. Null on the last page."}},"required":["limit","count","has_more","next_cursor"],"title":"Pagination","description":"Pagination metadata for a paginated partner-API list response.","examples":[{"limit":50,"count":137,"has_more":true,"next_cursor":null}]},"CardAttribute":{"type":"object","properties":{"trait_type":{"type":"string","description":"Attribute name, e.g. \"Set\" or \"Grade\"."},"value":{"type":"string","description":"Attribute value as a display string."}},"required":["trait_type","value"],"title":"Card Attribute","description":"A single trait on a card's metadata.","examples":[{"trait_type":"Grade","value":"PSA 10"}]},"CardMetadata":{"type":"object","properties":{"name":{"type":"string","description":"Display name of the card."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Back image URL, or null when the card has no back asset."},"attributes":{"type":"array","items":{"$ref":"#/components/schemas/CardAttribute"},"description":"Trait list shown on the card detail view."}},"required":["name","image","back_image","attributes"],"title":"Card Metadata","description":"Card metadata block returned for every partner card/NFT.","examples":[{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}]},"Item":{"type":"object","properties":{"id":{"type":"string","description":"Stable item identifier for this NFT."},"content":{"type":"object","properties":{"metadata":{"$ref":"#/components/schemas/CardMetadata"}},"required":["metadata"],"description":"Nested metadata, matching the marketplace NFT envelope."},"buyback_price":{"type":"number","description":"Current buyback price in USD (FMV × pack buyback percent)."},"type":{"type":"string","description":"Item type/kind discriminator."},"mint_address":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Identifier for the item when one exists; not an on-chain settlement from this API. Often null."},"collection_address":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Collection identifier when one exists; not an on-chain settlement from this API. Often null."},"token_standard":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Token-standard label when one exists, or null."}},"required":["id","content","buyback_price","type","mint_address","collection_address","token_standard"],"title":"Partner NFT","description":"An NFT/item as returned by the buy, buy-status and inventory endpoints.","examples":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null}]},"AvailablePack":{"type":"object","properties":{"id":{"type":"string","description":"Pack id. Pass this to `POST /vm/buy/init`. Not an on-chain mint for this API."},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"URL slug for the pack. Typed nullable for historical reasons; always set on partner-visible packs, so treat it as required. Addresses `GET /vm/chase/{slug}` and `GET /packs/{pack_slug}/items`."},"platform":{"type":"string","description":"Owning partner platform key."},"enable":{"type":"boolean","description":"Whether the pack is enabled/visible."},"type":{"enum":["CORE","EBAY"],"type":"string","description":"Leftover pack-type flag. Ignore it; not a storefront concern."},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pack display name."},"max_per_mint":{"type":"number","description":"Maximum packs purchasable in a single mint."},"mint_price":{"type":"string","description":"Price to mint one pack, in USD (decimal string). A buy of `amount` packs debits `mint_price × amount`."},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pack description."},"in_stock":{"type":"boolean","description":"True when the pack currently has pullable inventory."},"num_pulls_7d":{"type":"number","description":"Number of pulls in the last 7 days."},"chase":{"type":"array","items":{"$ref":"#/components/schemas/ChaseCard"},"description":"Always empty on this listing; use `GET /vm/chase/{slug}` for top hits."},"rarity_distribution":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"color":{"type":"string"},"lower":{"type":"number"},"upper":{"type":"number"},"weight":{"type":"number","minimum":0}},"required":["id","name","color","lower","upper","weight"]},"title":"Rarity Distribution","description":"Stored rarity-tier configuration: the pack's FMV ranges and their pull weights.","examples":[[{"id":0,"lower":13,"upper":25,"weight":80,"name":"Common","color":"#22C55E"},{"id":1,"lower":25,"upper":50,"weight":15,"name":"Uncommon","color":"#3b82f6"},{"id":2,"lower":50,"upper":150,"weight":4,"name":"Epic","color":"#EF4444"},{"id":3,"lower":150,"upper":10000,"weight":1,"name":"Mythic","color":"#F59E0B"}]]},{"type":"null"}],"description":"The pack's one odds curve, or null. Alternate risk profiles are modeled as separate packs."},"ev":{"type":"number","description":"Latest computed expected value in USD, to the cent. Comparable against the EV band below, which carries every digit."},"ev_updated_at":{"anyOf":[{"type":"string","description":"An ISO 8601 instant string with a required UTC offset (e.g. 2023-01-15T13:45:30Z)","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},{"type":"null"}],"description":"When `ev` last changed (ISO 8601), or null. A recompute landing on the same value does not move it."},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Primary category string. There is no categories catalog, so map display names and artwork yourself."},"categories":{"type":"array","items":{"type":"string"},"description":"All category strings assigned to the pack. Build your own grouping; no catalog endpoint."},"min_ev":{"type":"number","description":"Lower bound of the configured EV band, in USD. Exact: the stock gate compares against it."},"max_ev":{"type":"number","description":"Upper bound of the configured EV band, in USD. Exact: the stock gate compares against it."},"buyback_percent":{"type":"number","description":"Fraction of FMV paid on buyback (0–1)."},"repack":{"type":"boolean","description":"Leftover field. Ignore it; not a storefront concern."},"claw_image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pack artwork URL, or null."},"creator_profile":{"type":"object","properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Creator user id, or null."},"wallet_address":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Creator wallet address."},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Creator username."},"profile_picture":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Creator avatar URL."},"twitter_username":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Creator Twitter/X handle."},"socials":{"anyOf":[{"type":"object","properties":{"twitter":{"type":"string","description":"Twitter/X handle (e.g. `@flowlabs`)."}}},{"type":"null"}],"description":"Creator social links: known platform handles (e.g. `twitter`)."}},"required":["id"],"description":"Public profile of the pack creator, when available."},"rewards_amounts":{"type":"array","items":{"type":"number"},"description":"Deprecated pack token-bonus field. Always empty."},"sellback_rewards_amounts":{"type":"array","items":{"type":"number"},"description":"Deprecated pack token-bonus field. Always empty."},"rewards_mint_addresses":{"type":"array","items":{"type":"string"},"description":"Deprecated pack token-bonus field. Always empty."},"rewards_symbols":{"type":"array","items":{"type":"string"},"description":"Deprecated pack token-bonus field. Always empty."},"rewards_decimals":{"type":"array","items":{"type":"number"},"description":"Deprecated pack token-bonus field. Always empty."},"last_pull":{"type":"null","default":null,"description":"Deprecated. Always null; last-pull tracking was removed."},"variant_of":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Leftover field. Ignore it; not a storefront concern."},"variants":{"type":"array","items":{"type":"object","properties":{"mint_address":{"type":"string","description":"Pack primary id. Pass this to `POST /vm/buy/init`. Not an on-chain mint for this API."},"merkle_tree_address":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Leftover field. Ignore it; not a storefront concern."},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pack display name."},"created_at":{"type":"string","description":"Pack creation time (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"symbol":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Leftover field. Ignore it; not a storefront concern."},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Owning partner platform key; scopes the pack to your API key."},"mint_price":{"type":"string","description":"Price to mint one pack, in USD (decimal string). A buy of `amount` packs debits `mint_price × amount`."},"crossmint_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Linked Crossmint collection id, or null."},"enable":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether the pack is enabled/visible, or null if unset."},"in_stock":{"type":"boolean","description":"True when the pack currently has pullable inventory."},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pack description."},"rarity_distribution":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"color":{"type":"string"},"lower":{"type":"number"},"upper":{"type":"number"},"weight":{"type":"number","minimum":0}},"required":["id","name","color","lower","upper","weight"]},"title":"Rarity Distribution","description":"Stored rarity-tier configuration: the pack's FMV ranges and their pull weights.","examples":[[{"id":0,"lower":13,"upper":25,"weight":80,"name":"Common","color":"#22C55E"},{"id":1,"lower":25,"upper":50,"weight":15,"name":"Uncommon","color":"#3b82f6"},{"id":2,"lower":50,"upper":150,"weight":4,"name":"Epic","color":"#EF4444"},{"id":3,"lower":150,"upper":10000,"weight":1,"name":"Mythic","color":"#F59E0B"}]]},{"type":"null"}],"description":"The pack's one odds curve, or null. Alternate risk profiles are modeled as separate packs."},"max_per_mint":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Maximum packs purchasable in a single mint."},"type":{"enum":["CORE","EBAY"],"type":"string","description":"Leftover pack-type flag. Ignore it; not a storefront concern."},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"URL slug for the pack. Typed nullable for historical reasons; always set on partner-visible packs, so treat it as required."},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Primary category string. There is no categories catalog, so map display names and artwork yourself."},"categories":{"type":"array","items":{"type":"string"},"description":"All category strings assigned to the pack. Build your own grouping; no catalog endpoint."},"creator_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User id of the pack creator, or null."},"min_ev":{"type":"string","description":"Lower bound of the configured EV band, in USD (decimal string, exact)."},"max_ev":{"type":"string","description":"Upper bound of the configured EV band, in USD (decimal string, exact)."},"ev":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Latest computed expected value in USD, to the cent (decimal string), or null if never computed."},"ev_updated_at":{"anyOf":[{"type":"string","description":"An ISO 8601 instant string with a required UTC offset (e.g. 2023-01-15T13:45:30Z)","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},{"type":"null"}],"description":"When `ev` last changed (ISO 8601), or null. A recompute landing on the same value does not move it."},"buyback_percent":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fraction of FMV paid on buyback (0–1, decimal string), or null."},"buyback_wallet":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Leftover field. Ignore it; not a storefront concern."},"claw_image_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Pack artwork URL, or null."},"rewards_mint_addresses":{"type":"array","items":{"type":"string"},"default":[],"description":"Deprecated pack token-bonus field. Always empty."},"rewards_amounts":{"type":"array","items":{"type":"string","description":"An integer conveyed as a base-10 string to avoid JS number precision loss above 2^53."},"default":[],"description":"Deprecated pack token-bonus field. Always empty."},"sellback_rewards_amounts":{"type":"array","items":{"type":"string","description":"An integer conveyed as a base-10 string to avoid JS number precision loss above 2^53."},"default":[],"description":"Deprecated pack token-bonus field. Always empty."},"rewards_symbols":{"type":"array","items":{"type":"string"},"default":[],"description":"Deprecated pack token-bonus field. Always empty."},"rewards_decimals":{"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"default":[],"description":"Deprecated pack token-bonus field. Always empty."},"chase_cards":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Card item id."},"name":{"type":"string","description":"Card display name."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"fmv":{"type":"number","description":"Fair market value in USD, as stored."}},"required":["id","name","image","fmv"],"title":"Stored Chase Card","description":"A high-value \"chase\" card highlighted on a pack.","examples":[{"id":"Atv5Xigpcf7EvDqKJxoyp8CxZeo8m8NZdjFkER8dEi7c","name":"2000 Pokemon Gym Challenge 1st Edition Holo Brock's Ninetales #3 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2FApFiQSzv1NCRUpSZtzedkKYhuYLyo91xpLb5FcMSsdVD-cropped","fmv":2182.12}]}},{"type":"null"}],"description":"Stored chase-card configuration (a list of chase cards), or null."},"last_pull":{"type":"null","default":null,"description":"Deprecated. Always null; last-pull tracking was removed."},"num_pulls_7d":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pulls in the last 7 days."},"pulls_per_voucher":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Leftover field. Ignore it; not a storefront concern."},"variant_of":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Leftover field. Ignore it; not a storefront concern."}},"required":["mint_address","merkle_tree_address","name","created_at","symbol","platform","mint_price","crossmint_id","enable","in_stock","description","rarity_distribution","max_per_mint","type","slug","category","categories","creator_id","min_ev","max_ev","ev","ev_updated_at","buyback_percent","buyback_wallet","claw_image_url","chase_cards","num_pulls_7d","pulls_per_voucher","variant_of"],"title":"Pack","description":"Full `collections` row for a partner-owned pack (admin VM endpoints).","examples":[{"mint_address":"rookie-pack-1","merkle_tree_address":null,"name":"Rookie Pack","created_at":"2026-04-05T22:08:26.654Z","symbol":null,"platform":"mainnet","mint_price":"25","crossmint_id":null,"enable":true,"in_stock":false,"description":"","rarity_distribution":[{"id":0,"name":"Common","color":"#22C55E","lower":10,"upper":18,"weight":96},{"id":1,"name":"Uncommon","color":"#3B82F6","lower":18,"upper":25,"weight":0},{"id":2,"name":"Rare","color":"#A855F7","lower":25,"upper":50,"weight":0},{"id":3,"name":"Epic","color":"#EF4444","lower":50,"upper":150,"weight":0},{"id":4,"name":"Mythic","color":"#F59E0B","lower":150,"upper":10000,"weight":0}],"max_per_mint":8,"type":"EBAY","slug":"rookie-pack-1","category":null,"categories":["pokemon"],"creator_id":"did:privy:cm89av1a200kz28daow0x9bjq","min_ev":"24.75","max_ev":"26.75","ev":"0","ev_updated_at":"2026-07-14T12:00:00.000Z","buyback_percent":"0.85","buyback_wallet":"eyrkiwolf2d3qd4mwbcisdd6","claw_image_url":null,"rewards_mint_addresses":[],"rewards_amounts":[],"sellback_rewards_amounts":[],"rewards_symbols":[],"rewards_decimals":[],"chase_cards":null,"last_pull":null,"num_pulls_7d":0,"pulls_per_voucher":0,"variant_of":"13"}]},"description":"Leftover field. Ignore it; not a storefront concern."},"pack_managers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Manager user id."},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Manager username."},"profile_picture":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Manager avatar URL."},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Manager role."}},"required":["id","username","profile_picture","role"]},"description":"Users who manage this pack."}},"required":["id","slug","platform","enable","type","name","max_per_mint","mint_price","description","in_stock","num_pulls_7d","chase","rarity_distribution","ev","ev_updated_at","category","categories","min_ev","max_ev","buyback_percent","repack","rewards_amounts","sellback_rewards_amounts","rewards_mint_addresses","rewards_symbols","rewards_decimals"],"title":"Available Pack","description":"A purchasable pack as returned by `GET /vm/available`.","examples":[{"id":"13","slug":"rookie-pack","platform":"mainnet","enable":true,"type":"EBAY","name":"Rookie Pack","max_per_mint":8,"mint_price":"25","description":null,"in_stock":true,"num_pulls_7d":5200,"chase":[],"rarity_distribution":[{"id":0,"lower":13,"upper":25,"weight":80,"name":"Common","color":"#22C55E"},{"id":1,"lower":25,"upper":50,"weight":15,"name":"Uncommon","color":"#3b82f6"},{"id":2,"lower":50,"upper":150,"weight":4,"name":"Epic","color":"#EF4444"},{"id":3,"lower":150,"upper":10000,"weight":1,"name":"Mythic","color":"#F59E0B"}],"ev":26.22,"ev_updated_at":"2026-07-14T12:00:00.000Z","category":"pokemon","categories":["pokemon"],"min_ev":24.75,"max_ev":27,"buyback_percent":0.85,"repack":false,"claw_image_url":null,"creator_profile":{"id":"did:privy:cm89av1a200kz28daow0x9bjq"},"rewards_amounts":[],"sellback_rewards_amounts":[],"rewards_mint_addresses":[],"rewards_symbols":[],"rewards_decimals":[],"last_pull":null,"variant_of":null}]},"CardDetail":{"type":"object","properties":{"id":{"type":"string","description":"Card item id."},"name":{"type":"string","description":"Card display name."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Back image URL, or null."},"fmv":{"type":"number","description":"Fair market value in USD, rounded to cents."},"metadata":{"type":"object","properties":{"name":{"type":"string","description":"Display name of the card."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Back image URL, or null when the card has no back asset."},"attributes":{"type":"array","items":{"$ref":"#/components/schemas/CardAttribute"},"description":"Trait list shown on the card detail view."}},"required":["name","image","back_image","attributes"],"title":"Card Metadata","description":"Full card metadata block.","examples":[{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}]}},"required":["id","name","image","back_image","fmv","metadata"],"title":"Card Detail","description":"Card detail resolved by item id, NFT address or slug (`GET /card/{item_id}`).","examples":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"fmv":425.17,"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}}]},"RecentPull":{"type":"object","properties":{"id":{"type":"string","description":"Pull/activity id."},"claw_id":{"type":"string","description":"Id of the pack the card was pulled from."},"claw_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Slug of the pack, or null."},"value":{"type":"number","description":"Pulled card's FMV in USD, rounded to cents."},"buyback_price":{"type":"number","description":"Buyback price offered for the pulled card, in USD. Exact, derived from the unrounded FMV, so it will not equal `value` times the pack's buyback percent."},"created_at":{"type":"string","description":"When the pull happened (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"metadata":{"type":"object","properties":{"name":{"type":"string","description":"Display name of the card."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Back image URL, or null when the card has no back asset."},"attributes":{"type":"array","items":{"$ref":"#/components/schemas/CardAttribute"},"description":"Trait list shown on the card detail view."}},"required":["name","image","back_image","attributes"],"title":"Card Metadata","description":"Metadata of the pulled card.","examples":[{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}]}},"required":["id","claw_id","claw_slug","value","buyback_price","created_at","metadata"],"title":"Recent Pull","description":"A recent pull by one of the caller's end-users (`GET /vm/recent-pulls`).","examples":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","claw_id":"13","claw_slug":"rookie-pack","value":425.17,"buyback_price":361.39,"created_at":"2026-07-14T13:03:46.300Z","metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}}]},"PurchaseSession":{"type":"object","properties":{"session_id":{"type":"string","description":"Purchase session id."},"user_id":{"type":"string","description":"Your partner-defined user id the session belongs to."},"public_id":{"type":"string","description":"Public-facing session identifier."},"nfts":{"type":"array","items":{"$ref":"#/components/schemas/Item"},"description":"Cards picked in the session."},"tx_hash":{"type":"string","description":"Synthetic identifier for this purchase (the session id). Partner API buys are ledger-settled, so nothing is confirmed on-chain."}},"required":["session_id","user_id","public_id","nfts","tx_hash"],"title":"Purchase Session","description":"The stored purchase session, present only when `status` is `fulfilled`."}}},"webhooks":{"shipping":{"post":{"tags":["Webhooks"],"summary":"Shipment state changed","description":"Fires on every `shipping.*` lifecycle transition (queued → label_created → shipped → delivered, plus cancelled/failed) for a partner's physical shipment. `event` names the specific transition.\n\n**Verify the signature.** Every delivery carries `X-Phygitals-Signature: t=<unix-seconds>,v1=<hex>`, where `<hex>` is `hmac-sha256(<your-webhook-secret>, \"<t>.<rawBody>\")`: the timestamp, a literal dot, then the raw request body. Recompute it over the RAW bytes (do not re-serialize the parsed JSON; key order and spacing would change the signature), compare in constant time, and reject a `t` older than your tolerance (5 minutes is typical) so a captured delivery can't be replayed at you. `X-Phygitals-Delivery-Attempt` carries the 1-based attempt number, for logging.\n\n**Be idempotent. This is the one thing integrations get wrong.** Delivery is *at-least-once*: a timeout or a network blip between your 200 and our reading it means we retry an event you already processed. The envelope's `id` is the same on every redelivery of the same event, so record the ids you have handled and ignore a repeat. Do NOT dedupe on `delivered_at` (re-stamped per attempt) and do not rely on `data.idempotency_key` (it is null when the originating request sent no `Idempotency-Key`).\n\n**Acknowledge fast.** Any 2xx means delivered. We wait 5s for your response, so acknowledge first and do your work asynchronously; a slow handler reads as a timeout and earns you a duplicate.\n\n**Retries.** A timeout, a network error, a 5xx, or a 429/408/425 is retried with exponential backoff (30s, doubling, capped at 1800s) for up to 24h from when the event occurred, after which it is abandoned. Any OTHER 4xx is treated as a permanent client error and is NOT retried, so never return a 4xx for a transient problem you want us to resend; return a 5xx.\n\n**HTTPS only, and no redirects.** Your endpoint must be a publicly reachable `https://` URL. We do NOT follow redirects: point us straight at your handler.\n\n**No ordering guarantee.** Events are delivered concurrently and retried independently, so a retried `purchase.settled` can arrive after a later event. Order your own processing by the timestamp inside `data` (e.g. `settled_at`), never by arrival.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique id for this event, stable across redeliveries. Dedupe on this; see the Delivery section."},"event":{"enum":["shipping.queued","shipping.label_created","shipping.shipped","shipping.delivered","shipping.cancelled","shipping.failed"],"type":"string","description":"Shipping lifecycle event type."},"delivered_at":{"type":"string","format":"date-time","description":"ISO-8601 timestamp of THIS delivery attempt. Re-stamped on every retry, so it differs between redeliveries of the same event; use `id`, not this, to dedupe."},"data":{"type":"object","properties":{"type":{"enum":["shipping.queued","shipping.label_created","shipping.shipped","shipping.delivered","shipping.cancelled","shipping.failed"],"type":"string","description":"Shipping lifecycle event type."},"order_id":{"type":"string","description":"The shipment's order id."},"status":{"type":"string","description":"Human-readable shipment status."},"tracking_number":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Carrier tracking number, once assigned."},"tracking_url":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Link to this order on your Phygitals partner dashboard, on every `shipping.*` event EXCEPT `shipping.queued`, where it stays null. NOT a carrier tracking URL: partner shipments are fulfilled by the vault vendor with no carrier label bought through this API, and the vendor-side logistics links are internal. Requires a dashboard login, so it is for your operators; don't surface it to your end users."},"shipped_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"ISO-8601 timestamp the shipment left the warehouse."},"delivered_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"ISO-8601 timestamp the shipment was delivered."},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Failure detail for `shipping.failed`; null otherwise."},"carrier":{"type":"string","description":"Carrier name (e.g. `UPS`)."},"service":{"type":"string","description":"Carrier service level (e.g. `Ground`)."},"amount":{"type":"number","description":"Shipping cost charged, in USD."},"currency":{"type":"string","description":"ISO 4217 currency code; always `USD` today."},"updated_at":{"type":"string","format":"date-time","description":"ISO-8601 timestamp of this state change."}},"required":["type","order_id","status","tracking_number","tracking_url","shipped_at","delivered_at","error_message","carrier","service","amount","currency","updated_at"],"title":"Shipping Event","description":"Shipping event payload: a physical shipment changing state.","examples":[{"type":"shipping.shipped","order_id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23","status":"transit","tracking_number":"1Z999AA10123456784","tracking_url":"https://www.phygitals.com/partners/acme/claims/quotes/0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23","shipped_at":"2026-07-14T18:45:00.000Z","delivered_at":null,"error_message":null,"carrier":"UPS","service":"Ground","amount":20,"currency":"USD","updated_at":"2026-07-14T18:45:00.000Z"}]}},"required":["id","event","delivered_at","data"],"title":"Shipping Webhook","description":"The signed webhook envelope POSTed to your endpoint."}}}},"responses":{"200":{"description":"Acknowledged. Return any 2xx status; a non-2xx response (or a timeout) triggers a retry."}}}},"purchase.settled":{"post":{"tags":["Webhooks"],"summary":"Partner buy settled","description":"Fires when a partner buy completes successfully. Dedupe on the envelope's `id`.\n\n**Verify the signature.** Every delivery carries `X-Phygitals-Signature: t=<unix-seconds>,v1=<hex>`, where `<hex>` is `hmac-sha256(<your-webhook-secret>, \"<t>.<rawBody>\")`: the timestamp, a literal dot, then the raw request body. Recompute it over the RAW bytes (do not re-serialize the parsed JSON; key order and spacing would change the signature), compare in constant time, and reject a `t` older than your tolerance (5 minutes is typical) so a captured delivery can't be replayed at you. `X-Phygitals-Delivery-Attempt` carries the 1-based attempt number, for logging.\n\n**Be idempotent. This is the one thing integrations get wrong.** Delivery is *at-least-once*: a timeout or a network blip between your 200 and our reading it means we retry an event you already processed. The envelope's `id` is the same on every redelivery of the same event, so record the ids you have handled and ignore a repeat. Do NOT dedupe on `delivered_at` (re-stamped per attempt) and do not rely on `data.idempotency_key` (it is null when the originating request sent no `Idempotency-Key`).\n\n**Acknowledge fast.** Any 2xx means delivered. We wait 5s for your response, so acknowledge first and do your work asynchronously; a slow handler reads as a timeout and earns you a duplicate.\n\n**Retries.** A timeout, a network error, a 5xx, or a 429/408/425 is retried with exponential backoff (30s, doubling, capped at 1800s) for up to 24h from when the event occurred, after which it is abandoned. Any OTHER 4xx is treated as a permanent client error and is NOT retried, so never return a 4xx for a transient problem you want us to resend; return a 5xx.\n\n**HTTPS only, and no redirects.** Your endpoint must be a publicly reachable `https://` URL. We do NOT follow redirects: point us straight at your handler.\n\n**No ordering guarantee.** Events are delivered concurrently and retried independently, so a retried `purchase.settled` can arrive after a later event. Order your own processing by the timestamp inside `data` (e.g. `settled_at`), never by arrival.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique id for this event, stable across redeliveries. Dedupe on this; see the Delivery section."},"event":{"const":"purchase.settled"},"delivered_at":{"type":"string","format":"date-time","description":"ISO-8601 timestamp of THIS delivery attempt. Re-stamped on every retry, so it differs between redeliveries of the same event; use `id`, not this, to dedupe."},"data":{"type":"object","properties":{"type":{"const":"purchase.settled"},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `Idempotency-Key` from the originating buy request, echoed back so you can correlate this event to the call that caused it. Null when that request sent no key, so dedupe on the envelope's `id`, which is always present."},"session_id":{"type":"string","description":"The `vm_sessions.id` this buy created; look it up with `GET /vm/sessions/{session_id}`."},"amount":{"type":"number","description":"Total charged for the buy, in USD."},"currency":{"type":"string","description":"ISO 4217 currency code; always `USD` today."},"settled_at":{"type":"string","format":"date-time","description":"ISO-8601 timestamp the buy settled."}},"required":["type","idempotency_key","session_id","amount","currency","settled_at"],"title":"Purchase Settled Event","description":"Purchase settled event payload: a partner buy that completed successfully.","examples":[{"type":"purchase.settled","idempotency_key":"buy-rookie-2026-07-14-001","session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","amount":25,"currency":"USD","settled_at":"2026-07-14T13:03:46.300Z"}]}},"required":["id","event","delivered_at","data"],"title":"Purchase Settled Webhook","description":"The signed webhook envelope POSTed to your endpoint."}}}},"responses":{"200":{"description":"Acknowledged. Return any 2xx status; a non-2xx response (or a timeout) triggers a retry."}}}},"purchase.failed":{"post":{"tags":["Webhooks"],"summary":"Partner buy failed","description":"Fires when a partner buy fails to settle. Dedupe on the envelope's `id`.\n\n**Verify the signature.** Every delivery carries `X-Phygitals-Signature: t=<unix-seconds>,v1=<hex>`, where `<hex>` is `hmac-sha256(<your-webhook-secret>, \"<t>.<rawBody>\")`: the timestamp, a literal dot, then the raw request body. Recompute it over the RAW bytes (do not re-serialize the parsed JSON; key order and spacing would change the signature), compare in constant time, and reject a `t` older than your tolerance (5 minutes is typical) so a captured delivery can't be replayed at you. `X-Phygitals-Delivery-Attempt` carries the 1-based attempt number, for logging.\n\n**Be idempotent. This is the one thing integrations get wrong.** Delivery is *at-least-once*: a timeout or a network blip between your 200 and our reading it means we retry an event you already processed. The envelope's `id` is the same on every redelivery of the same event, so record the ids you have handled and ignore a repeat. Do NOT dedupe on `delivered_at` (re-stamped per attempt) and do not rely on `data.idempotency_key` (it is null when the originating request sent no `Idempotency-Key`).\n\n**Acknowledge fast.** Any 2xx means delivered. We wait 5s for your response, so acknowledge first and do your work asynchronously; a slow handler reads as a timeout and earns you a duplicate.\n\n**Retries.** A timeout, a network error, a 5xx, or a 429/408/425 is retried with exponential backoff (30s, doubling, capped at 1800s) for up to 24h from when the event occurred, after which it is abandoned. Any OTHER 4xx is treated as a permanent client error and is NOT retried, so never return a 4xx for a transient problem you want us to resend; return a 5xx.\n\n**HTTPS only, and no redirects.** Your endpoint must be a publicly reachable `https://` URL. We do NOT follow redirects: point us straight at your handler.\n\n**No ordering guarantee.** Events are delivered concurrently and retried independently, so a retried `purchase.settled` can arrive after a later event. Order your own processing by the timestamp inside `data` (e.g. `settled_at`), never by arrival.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique id for this event, stable across redeliveries. Dedupe on this; see the Delivery section."},"event":{"const":"purchase.failed"},"delivered_at":{"type":"string","format":"date-time","description":"ISO-8601 timestamp of THIS delivery attempt. Re-stamped on every retry, so it differs between redeliveries of the same event; use `id`, not this, to dedupe."},"data":{"type":"object","properties":{"type":{"const":"purchase.failed"},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `Idempotency-Key` from the originating buy request, echoed back so you can correlate this event to the call that caused it. Null when that request sent no key, so dedupe on the envelope's `id`, which is always present."},"failure_reason":{"type":"string","description":"Machine-readable failure reason (e.g. `INSUFFICIENT_BALANCE`, `OUT_OF_STOCK`)."},"amount":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Attempted charge, in USD; null when the failure preceded amount resolution."},"currency":{"type":"string","description":"ISO 4217 currency code; always `USD` today."},"failed_at":{"type":"string","format":"date-time","description":"ISO-8601 timestamp the buy failed."}},"required":["type","idempotency_key","failure_reason","amount","currency","failed_at"],"title":"Purchase Failed Event","description":"Purchase failed event payload: a partner buy that did not settle.","examples":[{"type":"purchase.failed","idempotency_key":"buy-rookie-2026-07-14-001","failure_reason":"INSUFFICIENT_BALANCE","amount":25,"currency":"USD","failed_at":"2026-07-14T13:03:46.300Z"}]}},"required":["id","event","delivered_at","data"],"title":"Purchase Failed Webhook","description":"The signed webhook envelope POSTed to your endpoint."}}}},"responses":{"200":{"description":"Acknowledged. Return any 2xx status; a non-2xx response (or a timeout) triggers a retry."}}}},"pack.purchased":{"post":{"tags":["Webhooks"],"summary":"Partner pack purchased","description":"Fires when a partner pack is bought and left unopened — the cards are drawn and reserved but not revealed, so this payload carries no nfts. `purchase.settled` follows when the pack is opened. Dedupe on the envelope's `id`.\n\n**Verify the signature.** Every delivery carries `X-Phygitals-Signature: t=<unix-seconds>,v1=<hex>`, where `<hex>` is `hmac-sha256(<your-webhook-secret>, \"<t>.<rawBody>\")`: the timestamp, a literal dot, then the raw request body. Recompute it over the RAW bytes (do not re-serialize the parsed JSON; key order and spacing would change the signature), compare in constant time, and reject a `t` older than your tolerance (5 minutes is typical) so a captured delivery can't be replayed at you. `X-Phygitals-Delivery-Attempt` carries the 1-based attempt number, for logging.\n\n**Be idempotent. This is the one thing integrations get wrong.** Delivery is *at-least-once*: a timeout or a network blip between your 200 and our reading it means we retry an event you already processed. The envelope's `id` is the same on every redelivery of the same event, so record the ids you have handled and ignore a repeat. Do NOT dedupe on `delivered_at` (re-stamped per attempt) and do not rely on `data.idempotency_key` (it is null when the originating request sent no `Idempotency-Key`).\n\n**Acknowledge fast.** Any 2xx means delivered. We wait 5s for your response, so acknowledge first and do your work asynchronously; a slow handler reads as a timeout and earns you a duplicate.\n\n**Retries.** A timeout, a network error, a 5xx, or a 429/408/425 is retried with exponential backoff (30s, doubling, capped at 1800s) for up to 24h from when the event occurred, after which it is abandoned. Any OTHER 4xx is treated as a permanent client error and is NOT retried, so never return a 4xx for a transient problem you want us to resend; return a 5xx.\n\n**HTTPS only, and no redirects.** Your endpoint must be a publicly reachable `https://` URL. We do NOT follow redirects: point us straight at your handler.\n\n**No ordering guarantee.** Events are delivered concurrently and retried independently, so a retried `purchase.settled` can arrive after a later event. Order your own processing by the timestamp inside `data` (e.g. `settled_at`), never by arrival.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique id for this event, stable across redeliveries. Dedupe on this; see the Delivery section."},"event":{"const":"pack.purchased"},"delivered_at":{"type":"string","format":"date-time","description":"ISO-8601 timestamp of THIS delivery attempt. Re-stamped on every retry, so it differs between redeliveries of the same event; use `id`, not this, to dedupe."},"data":{"type":"object","properties":{"type":{"const":"pack.purchased"},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `Idempotency-Key` from the originating purchase request, echoed back so you can correlate this event to the call that caused it. Null when that request sent no key, so dedupe on the envelope's `id`, which is always present."},"pack_id":{"type":"string","description":"The purchased pack's id; open it with `POST /vm/buy/open` and read it with `GET /vm/packs/by-id/{pack_id}`. Becomes the `session_id` once opened."},"claw_id":{"type":"string","description":"The claw this instance was bought from (`GET /vm/available`)."},"user_id":{"type":"string","description":"Your end-user id the pack was bought for."},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many cards the pack holds; all of them are revealed on open."},"price":{"type":"number","description":"Charged to your ledger for this pack, in USD."},"currency":{"type":"string","description":"ISO 4217 currency code; always `USD` today."},"purchased_at":{"type":"string","format":"date-time","description":"ISO-8601 timestamp the pack was bought."}},"required":["type","idempotency_key","pack_id","claw_id","user_id","amount","price","currency","purchased_at"],"title":"Pack Purchased Event","description":"Pack purchased event payload: a pack bought and still unopened — no cards until it is opened.","examples":[{"type":"pack.purchased","idempotency_key":"buy-rookie-2026-07-14-001","pack_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","claw_id":"13","user_id":"user_42","amount":1,"price":25,"currency":"USD","purchased_at":"2026-07-14T13:03:46.300Z"}]}},"required":["id","event","delivered_at","data"],"title":"Pack Purchased Webhook","description":"The signed webhook envelope POSTed to your endpoint."}}}},"responses":{"200":{"description":"Acknowledged. Return any 2xx status; a non-2xx response (or a timeout) triggers a retry."}}}},"buyback.settled":{"post":{"tags":["Webhooks"],"summary":"Partner buyback settled","description":"Fires when a partner buyback settles and credits the partner ledger. Dedupe on the envelope's `id`.\n\n**Verify the signature.** Every delivery carries `X-Phygitals-Signature: t=<unix-seconds>,v1=<hex>`, where `<hex>` is `hmac-sha256(<your-webhook-secret>, \"<t>.<rawBody>\")`: the timestamp, a literal dot, then the raw request body. Recompute it over the RAW bytes (do not re-serialize the parsed JSON; key order and spacing would change the signature), compare in constant time, and reject a `t` older than your tolerance (5 minutes is typical) so a captured delivery can't be replayed at you. `X-Phygitals-Delivery-Attempt` carries the 1-based attempt number, for logging.\n\n**Be idempotent. This is the one thing integrations get wrong.** Delivery is *at-least-once*: a timeout or a network blip between your 200 and our reading it means we retry an event you already processed. The envelope's `id` is the same on every redelivery of the same event, so record the ids you have handled and ignore a repeat. Do NOT dedupe on `delivered_at` (re-stamped per attempt) and do not rely on `data.idempotency_key` (it is null when the originating request sent no `Idempotency-Key`).\n\n**Acknowledge fast.** Any 2xx means delivered. We wait 5s for your response, so acknowledge first and do your work asynchronously; a slow handler reads as a timeout and earns you a duplicate.\n\n**Retries.** A timeout, a network error, a 5xx, or a 429/408/425 is retried with exponential backoff (30s, doubling, capped at 1800s) for up to 24h from when the event occurred, after which it is abandoned. Any OTHER 4xx is treated as a permanent client error and is NOT retried, so never return a 4xx for a transient problem you want us to resend; return a 5xx.\n\n**HTTPS only, and no redirects.** Your endpoint must be a publicly reachable `https://` URL. We do NOT follow redirects: point us straight at your handler.\n\n**No ordering guarantee.** Events are delivered concurrently and retried independently, so a retried `purchase.settled` can arrive after a later event. Order your own processing by the timestamp inside `data` (e.g. `settled_at`), never by arrival.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique id for this event, stable across redeliveries. Dedupe on this; see the Delivery section."},"event":{"const":"buyback.settled"},"delivered_at":{"type":"string","format":"date-time","description":"ISO-8601 timestamp of THIS delivery attempt. Re-stamped on every retry, so it differs between redeliveries of the same event; use `id`, not this, to dedupe."},"data":{"type":"object","properties":{"type":{"const":"buyback.settled"},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `Idempotency-Key` from the originating buyback request, echoed back so you can correlate this event to the call that caused it. Null when that request sent no key, so dedupe on the envelope's `id`, which is always present."},"buyback_id":{"type":"string","description":"This buyback's durable id, the same id `GET /vm/buybacks/{buyback_id}` looks it up by."},"item_id":{"type":"string","description":"The bought-back item id."},"amount":{"type":"number","description":"Payout credited to the partner's ledger, in USD."},"currency":{"type":"string","description":"ISO 4217 currency code; always `USD` today."},"settled_at":{"type":"string","format":"date-time","description":"ISO-8601 timestamp the buyback settled."}},"required":["type","idempotency_key","buyback_id","item_id","amount","currency","settled_at"],"title":"Buyback Settled Event","description":"Buyback settled event payload: an item sold back, crediting the partner ledger.","examples":[{"type":"buyback.settled","idempotency_key":"buyback-rookie-2026-07-14-001","buyback_id":"0194f0a3-1d4e-7b2f-8c3d-6a9f5e2b7d01","item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","amount":361.39,"currency":"USD","settled_at":"2026-07-14T14:10:00.000Z"}]}},"required":["id","event","delivered_at","data"],"title":"Buyback Settled Webhook","description":"The signed webhook envelope POSTed to your endpoint."}}}},"responses":{"200":{"description":"Acknowledged. Return any 2xx status; a non-2xx response (or a timeout) triggers a retry."}}}}},"tags":[{"name":"Packs","description":"Browse the storefront. A **pack** is a themed, purchasable bundle (the id the `claw_ids` filter references) that draws items provably-fair when bought. These endpoints list the available packs, surface each pack's **chase cards** (the headline high-value rare items, via `/vm/chase/{slug}`), and report the **recent pulls** drawn across packs."},{"name":"Items","description":"Look up an individual item, browse a user's inventory, and browse the pack-eligible supply pool."},{"name":"Purchases","description":"Buy packs and sell items back to the pool. Live keys debit/credit the prepaid partner ledger against real vaulted inventory; sandbox keys are fully simulated."},{"name":"Fairness","description":"Verify that a pull was not rigged. Every live buy records the server seed it drew with, that seed's hash, and the draw it produced, so you or your end-user can re-derive the outcome from the published seeds and confirm the recorded evidence matches. By default the server seed is minted at draw time, making this a verifiable record of the draw; commit to it in advance with `POST /vm/buy/commit` and the same proof becomes a true pre-commitment, because you were handed `sha256(server_seed)` before the draw ran. Supply your own `client_seed` on the buy either way, so the draw input isn't ours alone. Read a purchase's evidence by its session, or enumerate your recorded proofs. The end-to-end walkthrough is in the **Provable fairness** section of the introduction."},{"name":"Shipping","description":"Quote, request, and track physical shipments."},{"name":"Webhooks","description":"Outbound events Phygitals POSTs to the URL you register in your partner dashboard. Each is a signed JSON envelope, `{ id, event, delivered_at, data }`, where `event` names the type and `data`'s shape is discriminated by it (see the individual events below).\n\n**What fires, and when.** `purchase.settled` / `purchase.failed` when a `POST /vm/buy/init` does or doesn't settle; `buyback.settled` when a `POST /vm/buyback` credits your ledger (there is no `buyback.failed` or quote-expiry event); the `shipping.*` transitions as a physical shipment moves. These are pushes of the same facts the `GET /vm/sessions/{session_id}`, `GET /vm/buybacks/{buyback_id}`, and `/ship/order` endpoints report, so subscribe instead of polling. Any 2xx acknowledges the delivery; nothing in your response moves money. In the dashboard you can subscribe to a namespace with a wildcard (`purchase.*`), to an exact type (`buyback.settled`), or to everything by registering no event filter at all, and send yourself a synthetic `test` delivery to check your receiver end-to-end before you go live. Registration is per mode: your sandbox endpoint receives sandbox events, your live endpoint live ones, and a sandbox event is never delivered to a live URL, so you can build and test your receiver before a single real dollar moves.\n\n**Registering / updating your endpoint.** Manage delivery from your partner dashboard → **Webhooks** tab (`/partners/<your-slug>/webhooks`); it's **owner/editor only**, so a viewer can see the registration but not change it. Set the HTTPS **Endpoint URL** and the **subscribed events** (comma-separated: `shipping.*`, `purchase.*`, `buyback.settled`, or leave blank to receive everything) and save; the form pre-populates with your current registration, so an update just edits it in place. **Send test event** POSTs a synthetic `test` payload synchronously. It ignores your subscription filter (so it reaches your endpoint even if you've only subscribed to, say, `shipping.*`) and reports the receiver's real HTTP response so you can verify your endpoint end-to-end. The **signing secret is shown once**: when you first register the endpoint, or when you explicitly rotate it; editing the endpoint leaves the secret unchanged. Rotating it invalidates the old secret immediately, so update your verifier before you rotate. An overlap window (old + new both valid) is not available yet. There is no source-IP allowlist; authenticate deliveries by verifying the HMAC. Missed events can be resent from the partner dashboard. **Remove endpoint** stops deliveries.\n\n**A correct receiver** (Node/Express; the same shape applies anywhere):\n\n```js\nimport crypto from \"node:crypto\";\n\n// The RAW body is what was signed: parse AFTER verifying, never before.\napp.post(\"/hooks/phygitals\", express.raw({ type: \"application/json\" }), (req, res) => {\n  const header = req.get(\"X-Phygitals-Signature\") ?? \"\";\n  const parts = Object.fromEntries(header.split(\",\").map((p) => p.split(\"=\")));\n\n  // 1. Reject replays: refuse a timestamp outside a 5-minute window.\n  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return res.sendStatus(400);\n\n  // 2. Recompute the HMAC over `${t}.${rawBody}` and compare in CONSTANT time.\n  const expected = crypto\n    .createHmac(\"sha256\", process.env.PHYGITALS_WEBHOOK_SECRET)\n    .update(`${parts.t}.${req.body.toString()}`)\n    .digest(\"hex\");\n  const ok =\n    parts.v1?.length === expected.length &&\n    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));\n  if (!ok) return res.sendStatus(401);\n\n  const evt = JSON.parse(req.body.toString());\n\n  // 3. Be idempotent: `evt.id` is STABLE across redeliveries of this event.\n  //    Insert-if-absent (a unique index on the id) is the whole trick.\n  if (!markProcessedIfNew(evt.id)) return res.sendStatus(200); // already handled\n\n  // 4. Acknowledge NOW; do the slow work off the request.\n  res.sendStatus(200);\n  void handleAsync(evt);\n});\n```\n\nReturn a 5xx (not a 4xx) if you want a transient failure retried; see the delivery contract on any event below.","externalDocs":{"url":"https://phygitals.mintlify.app/","description":"Webhook integration guide"}}],"x-tagGroups":[{"name":"Storefront","tags":["Packs","Items","Purchases","Fairness","Shipping"]},{"name":"Events","tags":["Webhooks"]}],"openapi":"3.1.1","paths":{"/vm/available":{"get":{"operationId":"vm.available","summary":"List available packs","description":"Lists the packs the caller is allowed to browse and buy from, the catalog endpoint every integration starts at. There is no cursor: the response is a bare array capped at **1000** packs. There is no by-id `GET` for a single pack, so filter this list client-side. Each entry carries the two identifiers the rest of the API is addressed by: `id`, which you pass to `POST /vm/buy/init`, and `slug`, which addresses `GET /vm/chase/{slug}` and `GET /packs/{pack_slug}/items`. Slug is typed nullable for historical reasons but is always present on packs returned here, so treat it as required.\n\n`chase` is always `[]` on this listing; call `GET /vm/chase/{slug}` for top hits. `rarity_distribution` is one odds curve per pack. `category` / `categories[]` are free-form strings; there is no categories catalog, so build your own display mapping. Ignore `variants`, `variant_of`, `pulls_per_voucher`, `type`, and `repack`; they are leftover fields.\n\nWhat you see depends on how your partner is configured. A partner with platform browsing enabled sees only its own packs, and the `platform` query parameter is ignored for it. A reseller partner sees the first-party catalog instead: `mainnet` by default, or the storefront named by `platform`. Either way only enabled packs appear, so a pack missing from this list is not purchasable. Re-read `enable` and `in_stock` when advertising a pack. There is no pack-updated webhook, so a cached catalog can otherwise offer a pack that then fails on purchase.","tags":["Packs"],"parameters":[{"name":"platform","in":"query","schema":{"type":"string","description":"Storefront key override when the partner is not browse-enabled. Ignored when browse is enabled. Empty/omitted defaults to `mainnet`.","examples":["mainnet"]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AvailablePack"},"title":"Available Packs","examples":[[{"id":"13","slug":"rookie-pack","platform":"mainnet","enable":true,"type":"EBAY","name":"Rookie Pack","max_per_mint":8,"mint_price":"25","description":null,"in_stock":true,"num_pulls_7d":5200,"chase":[],"rarity_distribution":[{"id":0,"lower":13,"upper":25,"weight":80,"name":"Common","color":"#22C55E"},{"id":1,"lower":25,"upper":50,"weight":15,"name":"Uncommon","color":"#3b82f6"},{"id":2,"lower":50,"upper":150,"weight":4,"name":"Epic","color":"#EF4444"},{"id":3,"lower":150,"upper":10000,"weight":1,"name":"Mythic","color":"#F59E0B"}],"ev":26.22,"ev_updated_at":"2026-07-14T12:00:00.000Z","category":"pokemon","categories":["pokemon"],"min_ev":24.75,"max_ev":27,"buyback_percent":0.85,"repack":false,"claw_image_url":null,"creator_profile":{"id":"did:privy:cm89av1a200kz28daow0x9bjq"},"rewards_amounts":[],"sellback_rewards_amounts":[],"rewards_mint_addresses":[],"rewards_symbols":[],"rewards_decimals":[],"last_pull":null,"variant_of":null}]]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/chase/{slug}":{"get":{"operationId":"vm.chase","summary":"List a pack's chase cards","description":"Returns a pack's chase cards: its headline high-value pulls, the ones worth showing a buyer as a \"what could I pull?\" preview before they purchase. Up to 60 come back, drawn from the pack's own top rarity band, each already carrying its images so the list renders as a gallery without a follow-up call per card.\n\n`slug` is the `slug` from `GET /vm/available` (always set on packs that listing returns). There is no not-found error on this route: a slug matching no pack, and a pack with no chase cards to show, both return an empty array, so check the array's length rather than expecting a `404`.\n\nThese are the pack's best-case outcomes, not a promise about any particular buy. The odds live in the pack's `rarity_distribution` on `GET /vm/available`, and what a purchase actually drew is in the `nfts` that `POST /vm/buy/init` returns.","tags":["Packs"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","description":"Pack slug (the `slug` from `/vm/available`; always set on packs that listing returns).","examples":["rookie-pack"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Card item id."},"name":{"type":"string","description":"Card display name."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"fmv":{"type":"number","description":"Fair market value in USD, rounded to cents."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Back image URL, or null."}},"required":["id","name","image","fmv","back_image"],"title":"Chase Card With Back","description":"A chase card with its back image, as returned by `GET /vm/chase/{slug}`.","examples":[{"id":"Atv5Xigpcf7EvDqKJxoyp8CxZeo8m8NZdjFkER8dEi7c","name":"2000 Pokemon Gym Challenge 1st Edition Holo Brock's Ninetales #3 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2FApFiQSzv1NCRUpSZtzedkKYhuYLyo91xpLb5FcMSsdVD-cropped","fmv":2182.12,"back_image":null}]},"title":"Chase Cards","examples":[[{"id":"Atv5Xigpcf7EvDqKJxoyp8CxZeo8m8NZdjFkER8dEi7c","name":"2000 Pokemon Gym Challenge 1st Edition Holo Brock's Ninetales #3 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2FApFiQSzv1NCRUpSZtzedkKYhuYLyo91xpLb5FcMSsdVD-cropped","fmv":2182.12,"back_image":null}]]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/recent-pulls":{"get":{"operationId":"vm.recentPulls","summary":"List recent pulls","description":"Returns the newest pulls made by your own end-users, newest first, as a live activity ticker. Each row names the pack (`claw_id`, `claw_slug`), the card's metadata, and when it was pulled. There is no cursor: this is a \"latest N\" feed, not a paginated history.\n\nRows are scoped to the calling partner: every pull returned was made by one of your own end-users, through your own API, on live and sandbox keys alike. Pulls by other partners' users are never returned, and neither are pulls by first-party buyers — whether they bought a shared catalog pack or one of your own packs on the Phygitals marketplace. A pack that sells mostly outside your API therefore contributes little or nothing to this feed, and a partner whose end-users have not pulled yet gets an empty array rather than someone else's activity.\n\n`claw_ids` narrows the feed to specific packs and accepts either a single id or a list. On a **live** key it intersects with the packs your key can browse rather than widening them, so an id outside that set contributes nothing and returns no error, and a filter matching nothing yields an empty array; the scope is the same one `GET /vm/available` lists. On a **sandbox** key the filter is applied as given, but the real rows it can surface are still only your own end-users' pulls. There is no minimum-FMV filter, so drop rows client-side if you only want to display above a floor.\n\nThe two money fields answer different questions. `value` is what the card is worth now, while `buyback_price` is the sellback terms that pull froze at the moment of purchase, so it is `buyback_price`, not anything recomputed from `value`, that a later sellback actually honors.\n\nOn a sandbox key the feed also includes that sandbox's own simulated pulls, merged into the same newest-first ordering.","tags":["Packs"],"parameters":[{"name":"claw_ids","in":"query","schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Pack id(s) to filter pulls by: a single id or an array of ids.","examples":[["13"]]},"allowEmptyValue":true,"allowReserved":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Maximum number of pulls to return (integer 1–100; defaults to 20; junk falls back to 20).","examples":[20]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RecentPull"},"title":"Recent Pulls","examples":[[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","claw_id":"13","claw_slug":"rookie-pack","value":425.17,"buyback_price":361.39,"created_at":"2026-07-14T13:03:46.300Z","metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}}]]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/buy/init":{"post":{"operationId":"vm.buyInit","summary":"Buy a pack","description":"Buys `amount` packs and returns the picked NFTs, drawn provably-fair from the pack. Live for live keys (draws real vaulted inventory, commits ownership, and debits your prepaid partner ledger `mint_price × amount`); simulated for sandbox keys. Settlement is synchronous: `nfts` are in the init response (also look up via `GET /vm/sessions/{session_id}`, `GET /vm/sessions/by-idempotency-key/{idempotency_key}`, or the `purchase.settled` webhook). There is no void, refund, or cancel: a success stays settled. Pass your own `client_seed` to contribute the client half of the provably-fair draw; omit it and the platform seeds with the `session_id` it mints.\n\nFor a verifiable pre-commitment, call `POST /vm/buy/commit` first and pass the `commitment_id` it returns here: the draw then uses the server seed whose hash you already hold. Keep that id on your backend; showing `server_seed_hash` to the end user is enough. Pair it with your own `client_seed` so neither side alone fixes the outcome. Without one the platform picks both halves of the draw input, and the commitment proves only that the hash predated the draw. Omit `commitment_id` and the seed is minted at draw time (unchanged behavior).\n\nA `commitment_id` is consumed before the draw runs, so it backs exactly one attempt. If this call\nfails, whether sold out or short on balance, that commitment is spent and retrying it returns\n`409 BUY_COMMITMENT_ALREADY_USED`. Mint a fresh one with `POST /vm/buy/commit` (free, no body) and\nbuy again. Guards that don't depend on the seed (unknown or disabled pack, over `max_per_mint`)\nreject before the commitment is touched.\n\nRetries of a SETTLED buy are unaffected by `commitment_id`: a repeat call under an `Idempotency-Key` that already bought is `409 DUPLICATE_REQUEST` whether or not it names the commitment that buy spent, so your retry branch stays the same on both paths. Either way the settled purchase is readable with `GET /vm/sessions/{session_id}` (or the deprecated `POST /vm/buy/status`).","tags":["Purchases"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Pack id to buy from (the `id` from `/vm/available`)."},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"exclusiveMinimum":0,"description":"Number of packs to buy (a positive integer, bounded by the pack's `max_per_mint`)."},"user_id":{"type":"string","minLength":1,"description":"Your partner-defined user id to attribute the pulled items to."},"client_seed":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[\\x21-\\x39\\x3B-\\x7E]+$","description":"Optional provably-fair client seed to draw with. Supply your own (per buy, chosen before you call) so the client half of the draw is a value you fixed rather than one the platform picked: it is combined with a server seed (minted at draw time, or the one you already hold the hash of when you pass a `commitment_id`) and revealed afterwards, and both are returned by the fairness-proof endpoints so you can re-derive every pick yourself. 1 to 128 printable ASCII characters, no spaces, and no `:`, since the draw's HMAC messages use it as a field separator and a seed containing one would make them ambiguous. Defaults to the `session_id` the platform mints for the purchase. Ignored by sandbox keys, whose pulls are simulated rather than drawn and record no proof."},"idempotency_key":{"type":"string","description":"Optional idempotency key for this write. Equivalent to the `Idempotency-Key` header; if both are sent, this body value wins. A repeat call under a key that already succeeded is never re-executed: it replays the original response (with an `idempotency-replayed: true` header), or under a race fails `409 DUPLICATE_REQUEST`. Reusing a key with a different body is rejected `422 IDEMPOTENCY_KEY_REUSED`; a key whose only prior attempt failed is not blocked (the retry runs fresh, since nothing was charged). See the overview's Idempotency section."},"commitment_id":{"type":"string","format":"uuid","description":"A `commitment_id` from `POST /vm/buy/commit`, to draw with the server seed you already hold the hash of. Consumed by this call before the draw runs, so it backs exactly ONE attempt: if this buy fails, mint a fresh commitment rather than retrying this one. The resulting session gets its own `session_id`, and this id is not reused as one. Omit to have the seed minted at draw time.","examples":["0194f0a4-2e5f-7c3a-9b4e-7d0a6f3c8e12"]}},"required":["id","amount","user_id"],"examples":[{"id":"13","amount":1,"user_id":"user_42"}]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string","description":"Purchase session id; pass it to `GET /vm/sessions/{session_id}`."},"nfts":{"type":"array","items":{"$ref":"#/components/schemas/Item"},"description":"Cards picked by the purchase."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The effective idempotency key that applied to this request (the body `idempotency_key` if sent, else the `Idempotency-Key` header), or null if none. On `POST /vm/buy/open` it is the key the PURCHASE was made under, not this call's."}},"required":["session_id","nfts","idempotency_key"],"title":"Buy Result","description":"Result of a `POST /vm/buy/init` (or `POST /vm/buy/open`): the picked cards drawn for real (live keys) or simulated (sandbox keys).","examples":[{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","nfts":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null}],"idempotency_key":"buy-rookie-2026-07-14-001"}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","OUT_OF_STOCK","PACK_DISABLED","MAX_PER_MINT_EXCEEDED","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","OUT_OF_STOCK":"Claw machine is out of stock","PACK_DISABLED":"Pack is disabled","MAX_PER_MINT_EXCEEDED":"Amount exceeds the pack's max per mint","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"OUT_OF_STOCK":{"summary":"Pack out of stock","value":{"error":"Claw machine is out of stock","code":"OUT_OF_STOCK"}},"PACK_DISABLED":{"summary":"Pack is disabled","value":{"error":"Pack is disabled","code":"PACK_DISABLED"}},"MAX_PER_MINT_EXCEEDED":{"summary":"Amount over the pack's max per mint","value":{"error":"Amount must be less than or equal to 8","code":"MAX_PER_MINT_EXCEEDED"}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"402":{"description":"Payment required: the platform's prepaid balance is too low.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INSUFFICIENT_BALANCE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INSUFFICIENT_BALANCE":"Insufficient prepaid balance"}}}},"examples":{"INSUFFICIENT_BALANCE":{"value":{"error":"Insufficient prepaid balance","code":"INSUFFICIENT_BALANCE"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","VM_NOT_FOUND","BUY_COMMITMENT_NOT_FOUND","PARTNER_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","VM_NOT_FOUND":"Pack not found","BUY_COMMITMENT_NOT_FOUND":"Seed commitment not found","PARTNER_NOT_FOUND":"Partner not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"VM_NOT_FOUND":{"summary":"Pack not found or not visible to you","value":{"error":"Pack not found","code":"VM_NOT_FOUND"}},"BUY_COMMITMENT_NOT_FOUND":{"summary":"No seed commitment for this commitment_id","value":{"error":"Seed commitment not found","code":"BUY_COMMITMENT_NOT_FOUND"}},"PARTNER_NOT_FOUND":{"value":{"error":"Partner not found","code":"PARTNER_NOT_FOUND"}}}}}},"409":{"description":"Conflict: an in-flight request already used this Idempotency-Key, or the quote/items were already claimed by a concurrent request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["DUPLICATE_REQUEST","BUY_COMMITMENT_ALREADY_USED","DUPLICATE_LEDGER_ENTRY","CONFLICT"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"DUPLICATE_REQUEST":"Duplicate request: this Idempotency-Key is already in flight","BUY_COMMITMENT_ALREADY_USED":"Seed commitment has already been used","DUPLICATE_LEDGER_ENTRY":"duplicate ledger reference","CONFLICT":"Generic conflict fallback."}}}},"examples":{"DUPLICATE_REQUEST":{"value":{"error":"Duplicate request: this Idempotency-Key is already in flight","code":"DUPLICATE_REQUEST"}},"BUY_COMMITMENT_ALREADY_USED":{"summary":"Seed commitment already spent by a settled buy","value":{"error":"Seed commitment has already been used","code":"BUY_COMMITMENT_ALREADY_USED"}},"DUPLICATE_LEDGER_ENTRY":{"value":{"error":"duplicate ledger reference","code":"DUPLICATE_LEDGER_ENTRY"}},"CONFLICT":{"summary":"Quote already consumed by a concurrent request","value":{"error":"This claim has already been submitted; request a new estimate","code":"CONFLICT"}}}}}},"422":{"description":"Unprocessable: the request was well-formed but could not be fulfilled, or the Idempotency-Key was reused with a different body.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["BUY_NOT_ALLOWED","IDEMPOTENCY_KEY_REUSED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"BUY_NOT_ALLOWED":"This pack cannot be purchased through the standard buy flow","IDEMPOTENCY_KEY_REUSED":"Idempotency-Key already used with different request parameters"}}}},"examples":{"BUY_NOT_ALLOWED":{"summary":"Pack cannot be bought through the standard flow","value":{"error":"This pack cannot be purchased through the standard buy flow","code":"BUY_NOT_ALLOWED"}},"IDEMPOTENCY_KEY_REUSED":{"summary":"Idempotency-Key replayed with a different body","value":{"error":"Idempotency-Key already used with different request parameters","code":"IDEMPOTENCY_KEY_REUSED"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional idempotency key for this write, equivalent to the body's `idempotency_key` (which takes precedence if both are sent). Retrying with the same key and body replays the original response. See the overview's Idempotency section."}]}},"/vm/buy/commit":{"post":{"operationId":"commitBuySeed","summary":"Commit a buy seed","description":"Commits us to a server seed BEFORE any draw runs, returning only its hash. Show `server_seed_hash` to your end-user now; that hash is the public half. Keep `commitment_id` on your backend; it is a secret and does not need to reach the browser. Pass that id, and your own `client_seed` so both halves of the draw input aren't ours, to `POST /vm/buy/init`. That call mints and returns its own `session_id`;\n`GET /vm/sessions/{session_id}/fairness-proof` then reveals the seed, and `sha256(server_seed)` must\nequal the hash this call gave you, which is what proves the commitment predated the draw.\n\nTreat `commitment_id` like a secret and store it securely. The end-to-end walkthrough, with the\nchecks that make the revealed seed evidence, is in the Provable fairness section of the introduction.","tags":["Fairness"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"commitment_id":{"type":"string","format":"uuid","description":"Id of this seed commitment. Pass it back as `commitment_id` on `POST /vm/buy/init` to draw with the seed hashed below. Keep it on your backend; it is a secret and does not need to reach the end user (showing `server_seed_hash` is enough). It is NOT the resulting session id: the buy mints its own and returns it as `session_id`; fetch that session's fairness proof to see the revealed seed."},"server_seed_hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"`sha256` of the server seed we have committed to, issued BEFORE any draw runs, as 64 lowercase hex characters. After the buy settles, fetch the fairness proof and check `sha256(server_seed)` reproduces this exact value."}},"required":["commitment_id","server_seed_hash"],"title":"Buy Seed Commitment","description":"A pre-draw server-seed commitment (`POST /vm/buy/commit`).","examples":[{"commitment_id":"0194f0a4-2e5f-7c3a-9b4e-7d0a6f3c8e12","server_seed_hash":"6c2f8ab3d95174e0bf3a7c6d2e9018b45f7a3c1d8e6b204f9a7c3e5d1b8f0426"}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/buy/pack":{"post":{"operationId":"buyPack","summary":"Buy an unopened pack","description":"Buys a pack and leaves it UNOPENED: the cards are drawn and reserved for your end user, your prepaid partner ledger is debited `mint_price × amount`, and nothing is revealed. Returns just `{ pack_id }`. Open it later with `POST /vm/buy/open`, which returns the same body `POST /vm/buy/init` does. The request body is identical to `POST /vm/buy/init` — same `id`, `amount`, `user_id`, `client_seed`, `commitment_id`, `idempotency_key` — so a storefront that already buys one-shot can split the reveal in two without changing what it sends.\n\nReserved means reserved: the drawn cards leave the pool the moment this call commits, so no other buy can pull them. There is no void, refund, cancel, or expiry — an unopened pack stays yours until you open it. Everything you can read about it before that deliberately withholds the contents too: `GET /vm/packs/{user_id}` and `GET /vm/packs/by-id/{pack_id}` return the pack's claw, price, and purchase time, never its cards, listing ids, or seeds. Inventory, sessions, fairness proofs, buyback eligibility, and recent pulls all stay dark until the open commits.\n\nThe provably-fair inputs belong to THIS call, not to the open: `client_seed` and `commitment_id` are consumed here, at draw time. Pass a `commitment_id` from `POST /vm/buy/commit` to draw with the server seed you already hold the hash of; the resulting fairness proof becomes readable once the pack is opened, under the opened session's id (which is this `pack_id`).\n\nRetries follow the same idempotency rules as `POST /vm/buy/init`: inside the 24-hour replay cache, the same key and body replay the original `200`; a settled key past that window (or a concurrent twin) is `409 DUPLICATE_REQUEST`. Recover a lost `pack_id` with `GET /vm/sessions/by-idempotency-key/{idempotency_key}` (or the deprecated `POST /vm/buy/status`) — an unopened purchase resolves as `purchased` and carries the id. A key that already settled a buy through `POST /vm/buy/init` is refused the same `409` before anything is charged. Sending a key is what makes a retry safe after a timeout you never saw the response to.\n\nFires `pack.purchased` on commit, a `pack.*` event carrying no cards. Subscribers to `purchase.*` are untouched: they hear `purchase.settled` when the pack is opened, exactly as they do for a one-shot buy.","tags":["Purchases"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Pack id to buy from (the `id` from `/vm/available`)."},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"exclusiveMinimum":0,"description":"Number of packs to buy (a positive integer, bounded by the pack's `max_per_mint`)."},"user_id":{"type":"string","minLength":1,"description":"Your partner-defined user id to attribute the pulled items to."},"client_seed":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[\\x21-\\x39\\x3B-\\x7E]+$","description":"Optional provably-fair client seed to draw with. Supply your own (per buy, chosen before you call) so the client half of the draw is a value you fixed rather than one the platform picked: it is combined with a server seed (minted at draw time, or the one you already hold the hash of when you pass a `commitment_id`) and revealed afterwards, and both are returned by the fairness-proof endpoints so you can re-derive every pick yourself. 1 to 128 printable ASCII characters, no spaces, and no `:`, since the draw's HMAC messages use it as a field separator and a seed containing one would make them ambiguous. Defaults to the `session_id` the platform mints for the purchase. Ignored by sandbox keys, whose pulls are simulated rather than drawn and record no proof."},"idempotency_key":{"type":"string","description":"Optional idempotency key for this write. Equivalent to the `Idempotency-Key` header; if both are sent, this body value wins. A repeat call under a key that already succeeded is never re-executed: it replays the original response (with an `idempotency-replayed: true` header), or under a race fails `409 DUPLICATE_REQUEST`. Reusing a key with a different body is rejected `422 IDEMPOTENCY_KEY_REUSED`; a key whose only prior attempt failed is not blocked (the retry runs fresh, since nothing was charged). See the overview's Idempotency section."},"commitment_id":{"type":"string","format":"uuid","description":"A `commitment_id` from `POST /vm/buy/commit`, to draw with the server seed you already hold the hash of. Consumed by this call before the draw runs, so it backs exactly ONE attempt: if this buy fails, mint a fresh commitment rather than retrying this one. The resulting session gets its own `session_id`, and this id is not reused as one. Omit to have the seed minted at draw time.","examples":["0194f0a4-2e5f-7c3a-9b4e-7d0a6f3c8e12"]}},"required":["id","amount","user_id"],"examples":[{"id":"13","amount":1,"user_id":"user_42"}]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"pack_id":{"type":"string","description":"The purchased pack's id. Open it with `POST /vm/buy/open`, read it with `GET /vm/packs/by-id/{pack_id}`, and expect it back as the `session_id` of the session the open creates."}},"required":["pack_id"],"title":"Buy Pack Result","description":"Result of a `POST /vm/buy/pack`: the unopened pack's id, and deliberately nothing about its cards.","examples":[{"pack_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","OUT_OF_STOCK","PACK_DISABLED","MAX_PER_MINT_EXCEEDED","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","OUT_OF_STOCK":"Claw machine is out of stock","PACK_DISABLED":"Pack is disabled","MAX_PER_MINT_EXCEEDED":"Amount exceeds the pack's max per mint","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"OUT_OF_STOCK":{"summary":"Pack out of stock","value":{"error":"Claw machine is out of stock","code":"OUT_OF_STOCK"}},"PACK_DISABLED":{"summary":"Pack is disabled","value":{"error":"Pack is disabled","code":"PACK_DISABLED"}},"MAX_PER_MINT_EXCEEDED":{"summary":"Amount over the pack's max per mint","value":{"error":"Amount must be less than or equal to 8","code":"MAX_PER_MINT_EXCEEDED"}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"402":{"description":"Payment required: the platform's prepaid balance is too low.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INSUFFICIENT_BALANCE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INSUFFICIENT_BALANCE":"Insufficient prepaid balance"}}}},"examples":{"INSUFFICIENT_BALANCE":{"value":{"error":"Insufficient prepaid balance","code":"INSUFFICIENT_BALANCE"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","VM_NOT_FOUND","BUY_COMMITMENT_NOT_FOUND","PARTNER_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","VM_NOT_FOUND":"Pack not found","BUY_COMMITMENT_NOT_FOUND":"Seed commitment not found","PARTNER_NOT_FOUND":"Partner not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"VM_NOT_FOUND":{"summary":"Pack not found or not visible to you","value":{"error":"Pack not found","code":"VM_NOT_FOUND"}},"BUY_COMMITMENT_NOT_FOUND":{"summary":"No seed commitment for this commitment_id","value":{"error":"Seed commitment not found","code":"BUY_COMMITMENT_NOT_FOUND"}},"PARTNER_NOT_FOUND":{"value":{"error":"Partner not found","code":"PARTNER_NOT_FOUND"}}}}}},"409":{"description":"Conflict: an in-flight request already used this Idempotency-Key, or the quote/items were already claimed by a concurrent request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["DUPLICATE_REQUEST","BUY_COMMITMENT_ALREADY_USED","DUPLICATE_LEDGER_ENTRY","CONFLICT"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"DUPLICATE_REQUEST":"Duplicate request: this Idempotency-Key is already in flight","BUY_COMMITMENT_ALREADY_USED":"Seed commitment has already been used","DUPLICATE_LEDGER_ENTRY":"duplicate ledger reference","CONFLICT":"Generic conflict fallback."}}}},"examples":{"DUPLICATE_REQUEST":{"value":{"error":"Duplicate request: this Idempotency-Key is already in flight","code":"DUPLICATE_REQUEST"}},"BUY_COMMITMENT_ALREADY_USED":{"summary":"Seed commitment already spent by a settled buy","value":{"error":"Seed commitment has already been used","code":"BUY_COMMITMENT_ALREADY_USED"}},"DUPLICATE_LEDGER_ENTRY":{"value":{"error":"duplicate ledger reference","code":"DUPLICATE_LEDGER_ENTRY"}},"CONFLICT":{"summary":"Quote already consumed by a concurrent request","value":{"error":"This claim has already been submitted; request a new estimate","code":"CONFLICT"}}}}}},"422":{"description":"Unprocessable: the request was well-formed but could not be fulfilled, or the Idempotency-Key was reused with a different body.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["BUY_NOT_ALLOWED","IDEMPOTENCY_KEY_REUSED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"BUY_NOT_ALLOWED":"This pack cannot be purchased through the standard buy flow","IDEMPOTENCY_KEY_REUSED":"Idempotency-Key already used with different request parameters"}}}},"examples":{"BUY_NOT_ALLOWED":{"summary":"Pack cannot be bought through the standard flow","value":{"error":"This pack cannot be purchased through the standard buy flow","code":"BUY_NOT_ALLOWED"}},"IDEMPOTENCY_KEY_REUSED":{"summary":"Idempotency-Key replayed with a different body","value":{"error":"Idempotency-Key already used with different request parameters","code":"IDEMPOTENCY_KEY_REUSED"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional idempotency key for this write, equivalent to the body's `idempotency_key` (which takes precedence if both are sent). Retrying with the same key and body replays the original response. See the overview's Idempotency section."}]}},"/vm/buy/open":{"post":{"operationId":"openPack","summary":"Open a pack","description":"Opens a pack bought with `POST /vm/buy/pack` and reveals its cards. The response is byte-identical to `POST /vm/buy/init`'s — `{ session_id, nfts, idempotency_key }` — so one reveal renderer serves both the one-shot and the two-step flow. The `session_id` IS the `pack_id` you passed in: opening creates the buy session under the pack's own id, so every downstream lookup (`GET /vm/sessions/{session_id}`, its fairness proof, the ledger entry, buyback) keys off the id you have held since the purchase.\n\nOpening is what commits ownership. Until it does, the drawn cards are reserved but unowned and no session exists; after it, they are your end user's, readable through `GET /inventory/{user_id}`, and the pack drops out of `GET /vm/packs/{user_id}`. Buyback's window starts here, not at purchase, so a pack left unopened for a week does not eat into it.\n\nNo money moves: the ledger was debited at purchase. Nothing about this call can fail for balance reasons, and there is nothing to price — it takes only `pack_id`.\n\nOpening is idempotent by nature rather than by key: a second open of the same pack returns `200` with the same `session_id` and the same `nfts`, so a retry after a lost response is safe with or without an `Idempotency-Key`. Any key you do send is ignored — this route is exempt from the replay cache (see the guide's Idempotency section), so carrying the purchase's own key onto its open is fine and never trips `422 IDEMPOTENCY_KEY_REUSED`. A `pack_id` that is unknown, or belongs to another partner, is `404 PACK_NOT_FOUND` — a 404 rather than a 403, so probing ids tells you nothing about another partner's packs.\n\nFires `purchase.settled` on commit, with the unchanged payload every one-shot buy emits.","tags":["Purchases"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"pack_id":{"type":"string","minLength":1,"description":"The `pack_id` returned by `POST /vm/buy/pack`."},"idempotency_key":{"type":"string","description":"Accepted for symmetry with the purchase calls and safe to carry the purchase's own key, but ignored: opening is idempotent by `pack_id` itself, so a repeat open replays the same body with no key at all."}},"required":["pack_id"],"examples":[{"pack_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"}]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string","description":"Purchase session id; pass it to `GET /vm/sessions/{session_id}`."},"nfts":{"type":"array","items":{"$ref":"#/components/schemas/Item"},"description":"Cards picked by the purchase."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The effective idempotency key that applied to this request (the body `idempotency_key` if sent, else the `Idempotency-Key` header), or null if none. On `POST /vm/buy/open` it is the key the PURCHASE was made under, not this call's."}},"required":["session_id","nfts","idempotency_key"],"title":"Buy Result","description":"Result of a `POST /vm/buy/init` (or `POST /vm/buy/open`): the picked cards drawn for real (live keys) or simulated (sandbox keys).","examples":[{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","nfts":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null}],"idempotency_key":"buy-rookie-2026-07-14-001"}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","PACK_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","PACK_NOT_FOUND":"Pack not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"PACK_NOT_FOUND":{"summary":"No purchased pack for this pack_id","value":{"error":"Pack not found","code":"PACK_NOT_FOUND"}}}}}},"409":{"description":"Conflict: an in-flight request already used this Idempotency-Key, or the quote/items were already claimed by a concurrent request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["DUPLICATE_REQUEST","CONFLICT"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"DUPLICATE_REQUEST":"Duplicate request: this Idempotency-Key is already in flight","CONFLICT":"Generic conflict fallback."}}}},"examples":{"DUPLICATE_REQUEST":{"value":{"error":"Duplicate request: this Idempotency-Key is already in flight","code":"DUPLICATE_REQUEST"}},"CONFLICT":{"summary":"Quote already consumed by a concurrent request","value":{"error":"This claim has already been submitted; request a new estimate","code":"CONFLICT"}}}}}},"422":{"description":"Unprocessable: the request was well-formed but could not be fulfilled, or the Idempotency-Key was reused with a different body.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["IDEMPOTENCY_KEY_REUSED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"IDEMPOTENCY_KEY_REUSED":"Idempotency-Key already used with different request parameters"}}}},"examples":{"IDEMPOTENCY_KEY_REUSED":{"summary":"Idempotency-Key replayed with a different body","value":{"error":"Idempotency-Key already used with different request parameters","code":"IDEMPOTENCY_KEY_REUSED"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional idempotency key for this write, equivalent to the body's `idempotency_key` (which takes precedence if both are sent). Retrying with the same key and body replays the original response. See the overview's Idempotency section."}]}},"/vm/buy/status":{"post":{"operationId":"vm.buyStatus","summary":"Look up a purchase (deprecated)","description":"**Deprecated.** Prefer `GET /vm/sessions/{session_id}` or `GET /vm/sessions/by-idempotency-key/{idempotency_key}`, which expose each single-key lookup as its own URL so you don't have to construct the request-body union, and `GET /vm/sessions` to enumerate your sessions. This POST route stays supported for back-compat (no removal date) and resolves the identical outcome.\n\nLooks up a `POST /vm/buy/init` or `POST /vm/buy/pack` attempt by `session_id` and/or by the idempotency key you sent with it, scoped to your partner. At least one is required; supplying neither is a validation error. Only the key path can resolve a _failed_ attempt, since a failure never mints a `session_id` to look up by — and only the key path can resolve a `purchased` (paid but still unopened) pack, which has no session until it is opened; that result carries the `pack_id` and deliberately nothing about the cards.\n\nSending both is allowed and cross-checks them: `session_id` is the lookup key, and `idempotency_key` must be the key that purchase was made under, so a pair naming two different purchases resolves `not_found` rather than quietly answering about one of them.\n\nThere is no `pending` state. Partner buys are synchronous, so a lookup resolves the instant the originating call returns.","deprecated":true,"tags":["Purchases"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"session_id":{"type":"string","description":"Purchase session id returned by `/vm/buy/init`."},"idempotency_key":{"type":"string","description":"Optional cross-check when looking up by `session_id`: the Idempotency-Key the purchase was made under. The lookup resolves `not_found` if the session was made under a different key.","examples":["buy-rookie-2026-07-14-001"]}},"required":["session_id"]},{"type":"object","properties":{"idempotency_key":{"type":"string","description":"The Idempotency-Key (header or body) sent with the original `/vm/buy/init` call, scoped to your partner.","examples":["buy-rookie-2026-07-14-001"]}},"required":["idempotency_key"]}],"examples":[{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"}]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"status":{"const":"fulfilled","description":"A completed buy exists; see `result`."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"By `session_id`: the Idempotency-Key that session was created with, or null when none was sent. By `idempotency_key`: the key this result was resolved for (echoed back)."},"result":{"$ref":"#/components/schemas/PurchaseSession"},"failure_reason":{"type":"null","description":"Always null for a fulfilled buy."}},"required":["status","idempotency_key","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"purchased","description":"The buy under this `idempotency_key` settled as a pack that is still unopened — the ledger was debited and the cards are reserved, but nothing is revealed. NOT a retry invitation: the purchase already happened."},"idempotency_key":{"type":"string","description":"The `idempotency_key` this unopened pack was resolved for (echoed back)."},"pack_id":{"type":"string","description":"The unopened pack's id. Open it with `POST /vm/buy/open`, read it with `GET /vm/packs/by-id/{pack_id}`; it becomes the `session_id` once opened."},"result":{"type":"null","description":"Always null while the pack is unopened; the cards are revealed by the open, never this lookup."},"failure_reason":{"type":"null","description":"Always null; the purchase settled."}},"required":["status","idempotency_key","pack_id","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"failed","description":"The buy attempt that used this `idempotency_key` failed; see `failure_reason`."},"idempotency_key":{"type":"string","description":"The `idempotency_key` this failed result was resolved for (echoed back)."},"result":{"type":"null","description":"Always null for a failed buy."},"failure_reason":{"enum":["VM_NOT_FOUND","PACK_DISABLED","MAX_PER_MINT_EXCEEDED","BUY_NOT_ALLOWED","OUT_OF_STOCK","INSUFFICIENT_BALANCE","BUY_COMMITMENT_NOT_FOUND","BUY_COMMITMENT_ALREADY_USED","INTERNAL_ERROR"],"type":"string","title":"Buy Failure Reason","description":"Why the buy failed: a granular code (e.g. `OUT_OF_STOCK`, `MAX_PER_MINT_EXCEEDED`, `INSUFFICIENT_BALANCE`)."}},"required":["status","idempotency_key","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"unknown_key","description":"No buy attempt is on record for this `idempotency_key`."},"idempotency_key":{"type":"string","description":"The `idempotency_key` that resolved to no buy attempt (echoed back)."},"result":{"type":"null","description":"Always null; no buy is on record for this key."},"failure_reason":{"type":"null","description":"Always null; no buy is on record for this key."}},"required":["status","idempotency_key","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"not_found","description":"Unknown `session_id`, or one owned by a different partner; never leaked as a different status."},"idempotency_key":{"type":"null","description":"Always null; a not-found lookup echoes no key."},"result":{"type":"null","description":"Always null; no session was found."},"failure_reason":{"type":"null","description":"Always null; no session was found."}},"required":["status","idempotency_key","result","failure_reason"]}],"title":"Purchase Lookup Result","description":"Durable purchase lookup, by `session_id` or `idempotency_key` (`POST /vm/buy/status`).","examples":[{"status":"fulfilled","idempotency_key":"buy-rookie-2026-07-14-001","result":{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","public_id":"_k7m2n9p4qxw","nfts":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null}],"tx_hash":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"},"failure_reason":null},{"status":"purchased","idempotency_key":"buy-rookie-2026-07-14-001","pack_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","result":null,"failure_reason":null}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/packs/{user_id}":{"get":{"operationId":"listPacks","summary":"List unopened packs","description":"Pages through one end user's UNOPENED packs, newest first — what they have bought and not yet opened. Opening a pack removes it from this list, so a user who has opened everything gets `200` with an empty page, not a 404. A `user_id` with no packs and no buy sessions at all is `404 USER_NOT_FOUND`.\n\nEvery row is unopened: the claw it was bought from (`claw_id`, `claw_slug`), how many cards it holds, what it cost, the idempotency key it was bought under, and when. Never the cards, their listing ids, or the draw's seeds — those exist only after `POST /vm/buy/open`. `amount` is the card count in one pack, and opening reveals all of them at once.\n\nScoped to your own partner, and to the `user_id` you passed at buy time. Page with `limit` and `cursor`, following `pagination.next_cursor` until it comes back null; `pagination.count` is the exact total of unopened packs so you can render a badge without walking the list.","tags":["Purchases"],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Your partner-defined end-user id (the `user_id` you passed at buy time).","examples":["user_42"]}},{"name":"cursor","in":"query","schema":{"type":"string","description":"Opaque pagination cursor from a previous response's `pagination.next_cursor`. Omit for the first (newest) page; pass the exact value back to continue. Junk degrades to the first page.","examples":["0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"]},"allowEmptyValue":true,"allowReserved":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Maximum number of packs to return (integer 1–100; defaults to 20; junk falls back to 20).","examples":[20]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"pack_id":{"type":"string","description":"This pack instance's id — what `POST /vm/buy/pack` returned, and the `session_id` once it is opened."},"claw_id":{"type":"string","description":"The claw this instance was bought from (the `id` from `GET /vm/available`)."},"claw_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The claw's slug as it was at purchase, or null."},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Cards in this pack; opening reveals all of them at once."},"price":{"type":"number","description":"Charged to your ledger for this pack, in USD, rounded to cents."},"user_id":{"type":"string","description":"Your partner-defined end-user id the pack belongs to."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The Idempotency-Key the purchase was made under, or null when none was sent."},"purchased_at":{"type":"string","description":"When the purchase committed (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"}},"required":["pack_id","claw_id","claw_slug","amount","price","user_id","idempotency_key","purchased_at"],"title":"Unopened Pack","description":"One purchased pack, unopened — its claw, size and price, and nothing about the cards inside it.","examples":[{"pack_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","claw_id":"13","claw_slug":"rookie-pack","amount":1,"price":25,"user_id":"user_42","idempotency_key":"buy-rookie-2026-07-14-001","purchased_at":"2026-07-14T13:03:46.300Z"}]},"description":"The page of items, in the endpoint's stable order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"title":"Unopened Pack List","description":"A page of one end-user's unopened packs (`GET /vm/packs/{user_id}`).","examples":[{"data":[{"pack_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","claw_id":"13","claw_slug":"rookie-pack","amount":1,"price":25,"user_id":"user_42","idempotency_key":"buy-rookie-2026-07-14-001","purchased_at":"2026-07-14T13:03:46.300Z"}],"pagination":{"limit":20,"count":1,"has_more":false,"next_cursor":null}}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","USER_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","USER_NOT_FOUND":"User not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"USER_NOT_FOUND":{"value":{"error":"User not found","code":"USER_NOT_FOUND"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/packs/by-id/{pack_id}":{"get":{"operationId":"getPack","summary":"Get a pack","description":"Looks up one purchased pack by its `pack_id`, unopened or opened. Use it to poll a pack you bought with `POST /vm/buy/pack` before deciding to open it, or to confirm after the fact that an open landed.\n\nThe row is the same shape `GET /vm/packs/{user_id}` returns, plus `status` and `session_id`. `status` is `purchased` while unopened and `opened` afterwards; `session_id` is null until then, and equal to `pack_id` after — the buy session is created under the pack's own id. This response never carries the pack's cards, in either state: read them from the open's own response, or from `GET /vm/sessions/{session_id}` once `status` is `opened`.\n\nAn unknown `pack_id`, or one belonging to a different partner, is `404 PACK_NOT_FOUND`. Both cases answer identically on purpose, so walking ids cannot confirm that another partner's pack exists.","tags":["Purchases"],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"The `pack_id` returned by `POST /vm/buy/pack`.","examples":["0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"pack_id":{"type":"string","description":"This pack instance's id — what `POST /vm/buy/pack` returned, and the `session_id` once it is opened."},"claw_id":{"type":"string","description":"The claw this instance was bought from (the `id` from `GET /vm/available`)."},"claw_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The claw's slug as it was at purchase, or null."},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Cards in this pack; opening reveals all of them at once."},"price":{"type":"number","description":"Charged to your ledger for this pack, in USD, rounded to cents."},"user_id":{"type":"string","description":"Your partner-defined end-user id the pack belongs to."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The Idempotency-Key the purchase was made under, or null when none was sent."},"purchased_at":{"type":"string","description":"When the purchase committed (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"status":{"enum":["purchased","opened"],"type":"string","x-enum-descriptions":{"purchased":"Unopened. The cards are drawn and reserved and the buy is paid for, but nothing is revealed and the buyer does not own them yet. Open the pack to reveal.","opened":"Opened. The cards are owned by the buyer and the pack's `session_id` (=== its `pack_id`) is readable through the session, inventory and fairness-proof endpoints."},"title":"Pack Status","description":"The pack's state: `purchased` (unopened) or `opened` (revealed).","examples":["purchased","opened"]},"session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The buy session created when this pack was opened, equal to `pack_id`; null while `status` is `purchased`. Read the revealed cards from `GET /vm/sessions/{session_id}` — they are never on this response."}},"required":["pack_id","claw_id","claw_slug","amount","price","user_id","idempotency_key","purchased_at","status","session_id"],"title":"Pack","description":"One purchased pack, unopened or opened. Carries the pack's state, never its cards.","examples":[{"pack_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","claw_id":"13","claw_slug":"rookie-pack","amount":1,"price":25,"user_id":"user_42","idempotency_key":"buy-rookie-2026-07-14-001","purchased_at":"2026-07-14T13:03:46.300Z","status":"purchased","session_id":null},{"pack_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","claw_id":"13","claw_slug":"rookie-pack","amount":1,"price":25,"user_id":"user_42","idempotency_key":"buy-rookie-2026-07-14-001","purchased_at":"2026-07-14T13:03:46.300Z","status":"opened","session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","PACK_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","PACK_NOT_FOUND":"Pack not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"PACK_NOT_FOUND":{"summary":"No purchased pack for this pack_id","value":{"error":"Pack not found","code":"PACK_NOT_FOUND"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/sessions":{"get":{"operationId":"vm.sessions.list","summary":"List buy sessions","description":"Pages through your buy sessions, newest first: the historical companion to the single-session lookups, and the replacement for the deprecated `POST /vm/buy/status`. Use it to reconcile a day's purchases in bulk rather than resolving them one id at a time.\n\nA `fulfilled` row is a completed `POST /vm/buy/init`, carrying under `result` the same session `GET /vm/sessions/{session_id}` returns. A `failed` row is a recorded failed attempt. Only attempts made with an idempotency key are recorded, so a buy sent without one leaves no trace here when it fails. That is the practical argument for sending a key on every buy.\n\n`status` and `user_id` filter the list and apply together when you send both. It is always scoped to your own partner.","tags":["Purchases"],"parameters":[{"name":"status","in":"query","schema":{"enum":["fulfilled","failed"],"type":"string","description":"Filter to sessions in this state: `fulfilled` (completed buys) or `failed` (recorded failed attempts). Omit to list both.","examples":["fulfilled"]},"allowEmptyValue":true,"allowReserved":true},{"name":"user_id","in":"query","schema":{"type":"string","description":"Filter to sessions for this partner-defined end-user id (the `user_id` you passed at buy time).","examples":["user_42"]},"allowEmptyValue":true,"allowReserved":true},{"name":"cursor","in":"query","schema":{"type":"string","description":"Opaque pagination cursor from a previous response's `pagination.next_cursor`. Omit for the first (newest) page; pass the exact value back to continue. Junk degrades to the first page.","examples":["YjYzMmFjY2YtNzY0YS00MTA2LTk5OTItYmY2NjBhMTI5N2Y1fDAxOTRmMGEyLTdjM2ItN2UxMC05YTJiLTNjNGQ1ZTZmN2E4Yg"]},"allowEmptyValue":true,"allowReserved":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Maximum number of sessions to return (integer 1–100; defaults to 20; junk falls back to 20).","examples":[20]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"status":{"enum":["fulfilled","failed"],"type":"string","title":"Session Status","description":"The session's terminal state: `fulfilled` (a completed buy) or `failed` (a recorded failed attempt)."},"user_id":{"type":"string","description":"Your partner-defined user id the session belongs to."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The Idempotency-Key this session/attempt was made under, or null when none was sent."},"result":{"anyOf":[{"$ref":"#/components/schemas/PurchaseSession"},{"type":"null"}],"description":"The stored purchase session, present when `status` is `fulfilled`, else null."},"failure_reason":{"anyOf":[{"enum":["VM_NOT_FOUND","PACK_DISABLED","MAX_PER_MINT_EXCEEDED","BUY_NOT_ALLOWED","OUT_OF_STOCK","INSUFFICIENT_BALANCE","BUY_COMMITMENT_NOT_FOUND","BUY_COMMITMENT_ALREADY_USED","INTERNAL_ERROR"],"type":"string","title":"Buy Failure Reason","description":"Why the buy failed: a granular, machine-branchable reason code."},{"type":"null"}],"description":"A granular code for why the buy failed, when `status` is `failed`, else null."},"created_at":{"type":"string","description":"When the session was created / the attempt was recorded (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"}},"required":["status","user_id","idempotency_key","result","failure_reason","created_at"],"title":"Session","description":"One partner buy session (`fulfilled`) or recorded failed attempt (`failed`).","examples":[{"status":"fulfilled","user_id":"user_42","idempotency_key":"buy-rookie-2026-07-14-001","result":{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","public_id":"_k7m2n9p4qxw","nfts":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null}],"tx_hash":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"},"failure_reason":null,"created_at":"2026-07-14T13:03:46.300Z"}]},"description":"The page of items, in the endpoint's stable order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"title":"Session List","description":"A page of the caller's buy sessions (`GET /vm/sessions`).","examples":[{"data":[{"status":"fulfilled","user_id":"user_42","idempotency_key":"buy-rookie-2026-07-14-001","result":{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","public_id":"_k7m2n9p4qxw","nfts":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null}],"tx_hash":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"},"failure_reason":null,"created_at":"2026-07-14T13:03:46.300Z"},{"status":"failed","user_id":"user_42","idempotency_key":"buy-rookie-2026-07-13-009","result":null,"failure_reason":"OUT_OF_STOCK","created_at":"2026-07-13T09:20:11.000Z"}],"pagination":{"limit":20,"count":2,"has_more":false,"next_cursor":null}}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/sessions/{session_id}":{"get":{"operationId":"getSession","summary":"Look up a session by id","description":"Resolves one buy session by the `session_id` that `POST /vm/buy/init` returned: the durable record of what a purchase drew, for reconciling after the fact or re-rendering a reveal. This is the GET replacement for `POST /vm/buy/status`'s `session_id` path, addressed by URL so you don't have to construct the request-body union.\n\nBecause partner buys settle synchronously, a session exists only for a purchase that completed; there is no pending state to poll through, and a failed attempt has no `session_id` to look up. Find those by their idempotency key with `GET /vm/sessions/by-idempotency-key/{idempotency_key}`, or list them with `GET /vm/sessions`.\n\n`SESSION_NOT_FOUND` when no session is available for that id under your partner. Note this differs\nfrom the deprecated `POST /vm/buy/status`, which answers a miss with `200` and a `not_found`\nenvelope instead of a `404`.","tags":["Purchases"],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Purchase session id returned by `POST /vm/buy/init`.","examples":["0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"status":{"const":"fulfilled","description":"A completed buy exists; see `result`."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"By `session_id`: the Idempotency-Key that session was created with, or null when none was sent. By `idempotency_key`: the key this result was resolved for (echoed back)."},"result":{"$ref":"#/components/schemas/PurchaseSession"},"failure_reason":{"type":"null","description":"Always null for a fulfilled buy."}},"required":["status","idempotency_key","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"purchased","description":"The buy under this `idempotency_key` settled as a pack that is still unopened — the ledger was debited and the cards are reserved, but nothing is revealed. NOT a retry invitation: the purchase already happened."},"idempotency_key":{"type":"string","description":"The `idempotency_key` this unopened pack was resolved for (echoed back)."},"pack_id":{"type":"string","description":"The unopened pack's id. Open it with `POST /vm/buy/open`, read it with `GET /vm/packs/by-id/{pack_id}`; it becomes the `session_id` once opened."},"result":{"type":"null","description":"Always null while the pack is unopened; the cards are revealed by the open, never this lookup."},"failure_reason":{"type":"null","description":"Always null; the purchase settled."}},"required":["status","idempotency_key","pack_id","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"failed","description":"The buy attempt that used this `idempotency_key` failed; see `failure_reason`."},"idempotency_key":{"type":"string","description":"The `idempotency_key` this failed result was resolved for (echoed back)."},"result":{"type":"null","description":"Always null for a failed buy."},"failure_reason":{"enum":["VM_NOT_FOUND","PACK_DISABLED","MAX_PER_MINT_EXCEEDED","BUY_NOT_ALLOWED","OUT_OF_STOCK","INSUFFICIENT_BALANCE","BUY_COMMITMENT_NOT_FOUND","BUY_COMMITMENT_ALREADY_USED","INTERNAL_ERROR"],"type":"string","title":"Buy Failure Reason","description":"Why the buy failed: a granular code (e.g. `OUT_OF_STOCK`, `MAX_PER_MINT_EXCEEDED`, `INSUFFICIENT_BALANCE`)."}},"required":["status","idempotency_key","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"unknown_key","description":"No buy attempt is on record for this `idempotency_key`."},"idempotency_key":{"type":"string","description":"The `idempotency_key` that resolved to no buy attempt (echoed back)."},"result":{"type":"null","description":"Always null; no buy is on record for this key."},"failure_reason":{"type":"null","description":"Always null; no buy is on record for this key."}},"required":["status","idempotency_key","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"not_found","description":"Unknown `session_id`, or one owned by a different partner; never leaked as a different status."},"idempotency_key":{"type":"null","description":"Always null; a not-found lookup echoes no key."},"result":{"type":"null","description":"Always null; no session was found."},"failure_reason":{"type":"null","description":"Always null; no session was found."}},"required":["status","idempotency_key","result","failure_reason"]}],"title":"Purchase Lookup Result","description":"Durable purchase lookup, by `session_id` or `idempotency_key` (`POST /vm/buy/status`).","examples":[{"status":"fulfilled","idempotency_key":"buy-rookie-2026-07-14-001","result":{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","public_id":"_k7m2n9p4qxw","nfts":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null}],"tx_hash":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"},"failure_reason":null},{"status":"purchased","idempotency_key":"buy-rookie-2026-07-14-001","pack_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","result":null,"failure_reason":null}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","SESSION_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","SESSION_NOT_FOUND":"Session not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"SESSION_NOT_FOUND":{"summary":"No session for this id / idempotency key","value":{"error":"Session not found","code":"SESSION_NOT_FOUND"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/sessions/by-idempotency-key/{idempotency_key}":{"get":{"operationId":"getSessionByIdempotencyKey","summary":"Look up a session by idempotency key","description":"Resolves a buy attempt by the idempotency key you sent with the original `POST /vm/buy/init` or `POST /vm/buy/pack`. This is the lookup to reach for when a buy's response never arrived: you always hold the key you sent, even when you never learned the `session_id` (or `pack_id` — a settled-but-unopened pack resolves as `purchased`, carrying the `pack_id` to open and nothing about its cards). It is the GET replacement for `POST /vm/buy/status`'s `idempotency_key` path.\n\nUnlike the by-id lookup, this one resolves failures as well as successes, so a `failed` result with its `failure_reason` is enough to decide whether retrying is worthwhile. Failed attempts are recorded only when a key was sent, which is the practical argument for sending one on every buy.\n\n`SESSION_NOT_FOUND` means no attempt is on record for this key under your partner. Keys are partner-scoped, so another partner's key never resolves here.","tags":["Purchases"],"parameters":[{"name":"idempotency_key","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"The Idempotency-Key sent with the original `/vm/buy/init` call, scoped to your partner.","examples":["buy-rookie-2026-07-14-001"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"status":{"const":"fulfilled","description":"A completed buy exists; see `result`."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"By `session_id`: the Idempotency-Key that session was created with, or null when none was sent. By `idempotency_key`: the key this result was resolved for (echoed back)."},"result":{"$ref":"#/components/schemas/PurchaseSession"},"failure_reason":{"type":"null","description":"Always null for a fulfilled buy."}},"required":["status","idempotency_key","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"purchased","description":"The buy under this `idempotency_key` settled as a pack that is still unopened — the ledger was debited and the cards are reserved, but nothing is revealed. NOT a retry invitation: the purchase already happened."},"idempotency_key":{"type":"string","description":"The `idempotency_key` this unopened pack was resolved for (echoed back)."},"pack_id":{"type":"string","description":"The unopened pack's id. Open it with `POST /vm/buy/open`, read it with `GET /vm/packs/by-id/{pack_id}`; it becomes the `session_id` once opened."},"result":{"type":"null","description":"Always null while the pack is unopened; the cards are revealed by the open, never this lookup."},"failure_reason":{"type":"null","description":"Always null; the purchase settled."}},"required":["status","idempotency_key","pack_id","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"failed","description":"The buy attempt that used this `idempotency_key` failed; see `failure_reason`."},"idempotency_key":{"type":"string","description":"The `idempotency_key` this failed result was resolved for (echoed back)."},"result":{"type":"null","description":"Always null for a failed buy."},"failure_reason":{"enum":["VM_NOT_FOUND","PACK_DISABLED","MAX_PER_MINT_EXCEEDED","BUY_NOT_ALLOWED","OUT_OF_STOCK","INSUFFICIENT_BALANCE","BUY_COMMITMENT_NOT_FOUND","BUY_COMMITMENT_ALREADY_USED","INTERNAL_ERROR"],"type":"string","title":"Buy Failure Reason","description":"Why the buy failed: a granular code (e.g. `OUT_OF_STOCK`, `MAX_PER_MINT_EXCEEDED`, `INSUFFICIENT_BALANCE`)."}},"required":["status","idempotency_key","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"unknown_key","description":"No buy attempt is on record for this `idempotency_key`."},"idempotency_key":{"type":"string","description":"The `idempotency_key` that resolved to no buy attempt (echoed back)."},"result":{"type":"null","description":"Always null; no buy is on record for this key."},"failure_reason":{"type":"null","description":"Always null; no buy is on record for this key."}},"required":["status","idempotency_key","result","failure_reason"]},{"type":"object","properties":{"status":{"const":"not_found","description":"Unknown `session_id`, or one owned by a different partner; never leaked as a different status."},"idempotency_key":{"type":"null","description":"Always null; a not-found lookup echoes no key."},"result":{"type":"null","description":"Always null; no session was found."},"failure_reason":{"type":"null","description":"Always null; no session was found."}},"required":["status","idempotency_key","result","failure_reason"]}],"title":"Purchase Lookup Result","description":"Durable purchase lookup, by `session_id` or `idempotency_key` (`POST /vm/buy/status`).","examples":[{"status":"fulfilled","idempotency_key":"buy-rookie-2026-07-14-001","result":{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","public_id":"_k7m2n9p4qxw","nfts":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null}],"tx_hash":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"},"failure_reason":null},{"status":"purchased","idempotency_key":"buy-rookie-2026-07-14-001","pack_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","result":null,"failure_reason":null}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","SESSION_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","SESSION_NOT_FOUND":"Session not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"SESSION_NOT_FOUND":{"summary":"No session for this id / idempotency key","value":{"error":"Session not found","code":"SESSION_NOT_FOUND"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/sessions/{session_id}/fairness-proof":{"get":{"operationId":"getSessionFairnessProof","summary":"Get a session's fairness proof","description":"The provably-fair evidence for one buy session's draw — the seeds and per-draw detail that let you (or your end-user) re-run the pull yourself and arrive at the same card we handed over. Addressed by the `session_id` `POST /vm/buy/init` returned, and scoped to your partner.\n\n### Verifying the seeds\n\n`sha256(server_seed)` must equal `server_seed_hash` in every case. What that proves depends on how the buy was made:\n\n- **You committed first** via `POST /vm/buy/commit` — the hash was issued before the draw ran, so checking it against the value THAT call returned (the copy you stored) is what makes it a genuine commit-reveal.\n- **The seed was minted on demand** at draw time — both columns were recorded together, so the check confirms the record is internally consistent rather than pre-committed.\n\n`committed_at` is when we recorded the seed. A buy made today carries one on either path, so it identifies neither case on its own — your own stored hash does. It is null only on proofs drawn before we began recording commitments. `client_seed` is the seed you sent on `POST /vm/buy/init` — or, when you sent none, the session id itself.\n\n### How a draw picks each item\n\nA draw picks each item in two steps, and every `selections` entry records what BOTH steps produced. Step one re-derives from the revealed seeds alone; how step two is checked depends on the proof's `version` — always verify under the version the proof itself names.\n\n**Step one — the rarity tier.** `tierRandom` is `HMAC_SHA256(server_seed, client_seed + \":\" + index)`, read as a float in [0,1) from its first 13 hex characters, walked against `rarity_distribution` on the proof itself: the tier bands and weights as they were configured when this draw ran, in the order the cumulative-weight walk consumed them.\n\n- Check `tierRandom` against those, **not** against the pack's current weights from `GET /packs/{slug}` — a pack's tiers are re-tuned over time, so the live configuration is not evidence about a past draw.\n- `rarity_distribution` is null when no distribution is available for the draw — typically one recorded before the capture existed. When it is null, the pack's published weights are the best available reference.\n- Each selection's `tierLower` / `tierUpper` are the matching band's configured bounds from `rarity_distribution`, the final tier included — the draw respects the pack's stated top bound. One historical mismatch to expect: proofs recorded before August 2026 treated the final tier as open-ended and carry `tierUpper: 999999999` on last-tier selections. A proof is read back as recorded, so compare against the recorded value rather than re-deriving that substitution.\n\n**Step two — the item within that tier.** `rowSeed` is `HMAC_SHA256(server_seed, client_seed + \":row:\" + index)`. The recipe depends on `version`:\n\n- **`v1`** ranks every eligible item by `md5(item_id + rowSeed)` and the smallest wins; `itemRandom` is that value for the item the step landed on, so `md5(chosenItemId + rowSeed)` must reproduce it.\n- **`v2`** hashes nothing per item. Take the LAST 15 hex characters of `rowSeed` as an integer (60 bits, so it fits positive in any signed 64-bit type — everything before those 15 characters is deliberately irrelevant), reduce it modulo the candidate pool's size, and the winner is the pool's entry at that offset — the pool being `ords` on `candidate_pools`, ascending, exactly as published. The winner's ordinal is `chosenItemOrd`, and `item_ids` (parallel to `ords`) resolves it back to an item. There is no `itemRandom` on a `v2` proof at all: the offset generates no per-item value.\n\nThe two schemes are the two variants of the response body, discriminated on `version`, so narrowing on it in a typed client gives you exactly the fields that scheme defines. A `version` this API does not publish is an error rather than a passthrough.\n\n### Checking the pick itself with `candidate_pools`\n\nReproducing the item-within-tier pick needs one more thing — the set of items the draw actually chose from — and that is what `candidate_pools` on THIS endpoint gives you: for each selection that recorded one, every candidate the draw considered as parallel `item_ids` and `ords` arrays (`ords[i]` is `item_ids[i]`'s ordinal), captured inside the draw's own transaction so it is the pool as it stood at that instant rather than a later re-query.\n\nOn a `v1` proof, recompute `md5(item_id + rowSeed)` across `item_ids` — the array's order plays no part there. On a `v2` proof, one modulo over the pool's length names a position in `ords` directly, and that entry must equal `chosenItemOrd`; the ascending order of `ords` is itself part of the recipe, which is why it is published rather than left as a claim about `item_ids`. Either way you are checking the pick itself and not merely the seeds. That is what the pools add: the seed checks establish that the randomness was honest, and the pool establishes that it was applied to the recorded set of candidates.\n\nTwo properties every honest draw has, so build your checker to assert them rather than to flag them:\n\n1. **Membership is the hard check** — `chosenItemId` appears in its pool. That is what a proof settles per pull, and it holds on every draw.\n2. **The winner is the first candidate the recipe names that was still free.** Two buyers cannot be handed the same physical card, so when a concurrent buy is already claiming a candidate, the pick moves past it — under `v1` to the next-smallest key, under `v2` to the next offset (`k+1`, `k+2`, … wrapping around the pool). That probe order is part of the published recipe rather than a departure from it: re-run it over the recorded pool and the winner's distance from the starting point is exactly how many candidates were locked at that instant.\n\n`candidate_pools` is keyed by `index`, matching `selections[].index` — match on that, not on array position.\n\nA selection is absent from it when:\n\n- the draw predates pool capture;\n- its tier had no stock; or\n- the eligible set was too large to record.\n\nEntries are never truncated: a pool is published whole or not at all.\n\nThis is also the only endpoint that carries pools — `GET /vm/fairness-proofs` returns the same evidence without them, so page that to find the draws you care about and fetch them here to check them.\n\n### What a verified proof establishes\n\nA proof that passes the checks above settles, for every pull in the session: the revealed server seed is the one behind `server_seed_hash`; the rarity tier follows from that seed chain and the weights actually in force when the draw ran; and the card you received is the one the published recipe selects from the recorded pool. The pool is captured inside the draw's own transaction, from the same definition of eligibility the pick used, so the set you check against is the set the draw indexed into — not a later re-query that could have moved.\n\nOne thing a proof cannot settle on its own: the pool shows the pick was made against the set we recorded, not that the recorded set was every item the pack held. Check that separately by comparing a pool against what `GET /packs/{slug}/items` reports for the pack.\n\n### Errors\n\nReturns `404 FAIRNESS_PROOF_NOT_FOUND` when no proof is available for that session under your partner — including sandbox buys, which are simulated rather than drawn and so record no proof.","tags":["Fairness"],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Purchase session id returned by `POST /vm/buy/init`.","examples":["0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"session_id":{"type":"string","description":"The buy session this proof is the evidence for — the `session_id` from `POST /vm/buy/init`."},"user_id":{"type":"string","description":"Your partner-defined user id the drawing session belongs to."},"claw_id":{"type":"string","description":"Id of the pack the draw drew from."},"version":{"const":"v1","description":"Fairness protocol version — it selects the within-tier recipe. `v1` ranks item id text: `md5(item_id + rowSeed)` over the candidate pool, smallest key wins."},"algorithm":{"const":"hmac_sha256","description":"Algorithm identifier: hmac-sha256 seed chain, with the id-keyed md5 ranking as the selection step."},"server_seed":{"type":"string","description":"The revealed server seed. Only ever readable after the pull it backs has settled — hash it to check it against `server_seed_hash`."},"server_seed_hash":{"type":"string","description":"The hash of `server_seed` — `sha256(server_seed)` must reproduce this value exactly."},"committed_at":{"anyOf":[{"type":"string","description":"An ISO 8601 instant string with a required UTC offset (e.g. 2023-01-15T13:45:30Z)","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},{"type":"null"}],"description":"When we recorded the server seed for this draw. If you called `POST /vm/buy/commit` and passed the resulting `commitment_id`, this is when that call issued your `server_seed_hash` — before the draw ran. Otherwise the seed was minted as the draw ran and this is that moment. It does NOT by itself distinguish the two: what proves a pre-commitment is that YOU were holding `server_seed_hash` before you bought, so check the revealed seed against the hash you stored, not against this timestamp."},"client_seed":{"type":"string","description":"The client seed the draw was combined with — the `client_seed` you sent on `POST /vm/buy/init`, or, when you sent none, the `session_id` the platform minted for the purchase."},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of items drawn in this pull — the length of `selections`."},"selections":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Draw index within the pull (`0` … `amount - 1`) — the RNG nonce for this item."},"tierId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Id of the rarity tier this draw landed in."},"tierLower":{"type":"number","description":"Lower bound of the tier's FMV band, in USD."},"tierUpper":{"type":"number","description":"Upper bound of the tier's FMV band, in USD, AS THE DRAW APPLIED IT — the tier's configured `upper` from `rarity_distribution`, the final tier included: the draw respects the pack's stated top bound. Proofs recorded before August 2026 treated the final tier as open-ended and carry a sentinel `999999999` here on last-tier pulls; compare against the recorded value, never re-derive that substitution."},"tierRandom":{"type":"number","description":"The RNG value that selected the tier, derived from the two seeds and this `index` — the number to re-derive when checking the draw."},"rowSeed":{"type":"string","description":"Deterministic per-item seed the within-tier pick is derived from, itself derived from the seeds."},"chosenItemId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The item this draw resolved to, or null if the tier had no stock."},"itemRandom":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"The key the winner was ranked by — `md5(chosenItemId + rowSeed)`, the smallest key in the tier winning. Recompute it to check the within-tier pick against the seeds. Null when the tier had no stock, or on draws recorded before this value was published."}},"required":["index","tierId","tierLower","tierUpper","tierRandom","rowSeed","chosenItemId"],"title":"Fairness Selection (v1)","description":"The recorded detail for one drawn item within a `v1` pull."},"description":"Per-draw detail, one entry per drawn item, in draw order — reproduced exactly as recorded."},"rarity_distribution":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"lower":{"type":"number"},"upper":{"type":"number"},"weight":{"type":"number","minimum":0}},"required":["id","lower","upper","weight"],"title":"Fairness Rarity Tier","description":"One rarity band the draw could land in, as configured when the draw ran."}},{"type":"null"}],"title":"Fairness Rarity Distribution","description":"The pack's rarity tiers as configured when this draw ran, in the order the weighted pick walked them — the bands and weights to check `tierRandom` against. Trimmed to what the draw consumes, so display-only tier fields are absent. Null when no distribution is available for the draw, typically one recorded before the capture existed.","examples":[[{"id":0,"lower":0,"upper":10,"weight":20},{"id":1,"lower":10,"upper":25,"weight":15},{"id":2,"lower":25,"upper":100,"weight":60},{"id":3,"lower":100,"upper":1000,"weight":5}]]},"created_at":{"type":"string","description":"When the draw was recorded (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"candidate_pools":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The `selections[].index` this pool belongs to — match on this, not on array position."},"item_ids":{"type":"array","items":{"type":"string"},"description":"Every item the draw considered for this selection, ascending by internal ordinal. The ranking is yours to derive over this set — `md5(item_id + rowSeed)`, smallest wins — and the array's order plays no part in it."},"ords":{"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"description":"The same candidates as their internal ordinals, parallel to `item_ids` (`ords[i]` is `item_ids[i]`'s ordinal) and ascending. On a `v1` proof these identify candidates; they do not order the ranking."}},"required":["index","item_ids","ords"],"title":"Fairness Candidate Pool (v1)","description":"The full set of candidates one `v1` selection was drawn from."},"description":"The candidate pools this draw selected from, one entry per selection that recorded one, keyed by `index`. A selection with no entry cannot be reconstructed, for one of three reasons: the draw predates pool recording; the tier had no stock, so there was nothing to pick from; or the eligible set was too large to record. An entry is always the COMPLETE pool — pools are never partially published, so a missing entry never means a truncated one. Empty array when no selection in this proof recorded a pool."}},"required":["session_id","user_id","claw_id","version","algorithm","server_seed","server_seed_hash","committed_at","client_seed","amount","selections","rarity_distribution","created_at","candidate_pools"]},{"type":"object","properties":{"session_id":{"type":"string","description":"The buy session this proof is the evidence for — the `session_id` from `POST /vm/buy/init`."},"user_id":{"type":"string","description":"Your partner-defined user id the drawing session belongs to."},"claw_id":{"type":"string","description":"Id of the pack the draw drew from."},"version":{"const":"v2","description":"Fairness protocol version — it selects the within-tier recipe. `v2` hashes nothing per item: the winner is the candidate pool's entry at offset `int(last 15 hex of rowSeed) mod pool size`, the pool being ascending by internal ordinal."},"algorithm":{"const":"hmac_sha256_ord_offset","description":"Algorithm identifier: the same hmac-sha256 seed chain as `v1`; the suffix names the ordinal-offset selection step that replaced the ranking."},"server_seed":{"type":"string","description":"The revealed server seed. Only ever readable after the pull it backs has settled — hash it to check it against `server_seed_hash`."},"server_seed_hash":{"type":"string","description":"The hash of `server_seed` — `sha256(server_seed)` must reproduce this value exactly."},"committed_at":{"anyOf":[{"type":"string","description":"An ISO 8601 instant string with a required UTC offset (e.g. 2023-01-15T13:45:30Z)","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},{"type":"null"}],"description":"When we recorded the server seed for this draw. If you called `POST /vm/buy/commit` and passed the resulting `commitment_id`, this is when that call issued your `server_seed_hash` — before the draw ran. Otherwise the seed was minted as the draw ran and this is that moment. It does NOT by itself distinguish the two: what proves a pre-commitment is that YOU were holding `server_seed_hash` before you bought, so check the revealed seed against the hash you stored, not against this timestamp."},"client_seed":{"type":"string","description":"The client seed the draw was combined with — the `client_seed` you sent on `POST /vm/buy/init`, or, when you sent none, the `session_id` the platform minted for the purchase."},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of items drawn in this pull — the length of `selections`."},"selections":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Draw index within the pull (`0` … `amount - 1`) — the RNG nonce for this item."},"tierId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Id of the rarity tier this draw landed in."},"tierLower":{"type":"number","description":"Lower bound of the tier's FMV band, in USD."},"tierUpper":{"type":"number","description":"Upper bound of the tier's FMV band, in USD, AS THE DRAW APPLIED IT — the tier's configured `upper` from `rarity_distribution`, the final tier included: the draw respects the pack's stated top bound. Proofs recorded before August 2026 treated the final tier as open-ended and carry a sentinel `999999999` here on last-tier pulls; compare against the recorded value, never re-derive that substitution."},"tierRandom":{"type":"number","description":"The RNG value that selected the tier, derived from the two seeds and this `index` — the number to re-derive when checking the draw."},"rowSeed":{"type":"string","description":"Deterministic per-item seed the within-tier pick is derived from, itself derived from the seeds."},"chosenItemId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The item this draw resolved to, or null if the tier had no stock."},"chosenItemOrd":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"The winner's internal ordinal, and what the offset recipe must land on: reduce the LAST 15 hex chars of `rowSeed` (as an integer — 60 bits, positive in any signed 64-bit type) modulo the matching candidate pool's size, and that pool's `ords` entry at the offset — or `k` slots past it, wrapping, `k` being concurrently-locked candidates the draw skipped — is this value. Null when the tier had no stock."}},"required":["index","tierId","tierLower","tierUpper","tierRandom","rowSeed","chosenItemId","chosenItemOrd"],"title":"Fairness Selection (v2)","description":"The recorded detail for one drawn item within a `v2` pull."},"description":"Per-draw detail, one entry per drawn item, in draw order — reproduced exactly as recorded."},"rarity_distribution":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"lower":{"type":"number"},"upper":{"type":"number"},"weight":{"type":"number","minimum":0}},"required":["id","lower","upper","weight"],"title":"Fairness Rarity Tier","description":"One rarity band the draw could land in, as configured when the draw ran."}},{"type":"null"}],"title":"Fairness Rarity Distribution","description":"The pack's rarity tiers as configured when this draw ran, in the order the weighted pick walked them — the bands and weights to check `tierRandom` against. Trimmed to what the draw consumes, so display-only tier fields are absent. Null when no distribution is available for the draw, typically one recorded before the capture existed.","examples":[[{"id":0,"lower":0,"upper":10,"weight":20},{"id":1,"lower":10,"upper":25,"weight":15},{"id":2,"lower":25,"upper":100,"weight":60},{"id":3,"lower":100,"upper":1000,"weight":5}]]},"created_at":{"type":"string","description":"When the draw was recorded (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"candidate_pools":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The `selections[].index` this pool belongs to — match on this, not on array position."},"item_ids":{"type":"array","items":{"type":"string"},"description":"Every item the draw considered for this selection, parallel to `ords` — this is how you resolve the ordinal the offset lands on back to an item."},"ords":{"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"description":"The same candidates as their internal ordinals, ascending — and on a `v2` proof that ascending order IS the selection order, which is why it is published rather than left implicit. The winner is the entry at offset `int(last 15 hex of rowSeed) mod length` (or `k` slots past it, wrapping, for `k` skipped locks), and it must equal the selection's `chosenItemOrd`."}},"required":["index","item_ids","ords"],"title":"Fairness Candidate Pool (v2)","description":"The full set of candidates one `v2` selection was drawn from."},"description":"The candidate pools this draw selected from, one entry per selection that recorded one, keyed by `index`. A selection with no entry cannot be reconstructed, for one of three reasons: the draw predates pool recording; the tier had no stock, so there was nothing to pick from; or the eligible set was too large to record. An entry is always the COMPLETE pool — pools are never partially published, so a missing entry never means a truncated one. Empty array when no selection in this proof recorded a pool."}},"required":["session_id","user_id","claw_id","version","algorithm","server_seed","server_seed_hash","committed_at","client_seed","amount","selections","rarity_distribution","created_at","candidate_pools"]}],"title":"Fairness Proof Detail","description":"The provably-fair evidence for one buy session's draw, including the candidate pools it drew from.","examples":[{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","claw_id":"13","version":"v1","algorithm":"hmac_sha256","server_seed":"99d3312e91112bf5ecffd1bed393be5b6c8dd9c70d1939d5fa13fde9dfdf9188","server_seed_hash":"eb0e1f282ab61ab6011c06739731577910f6216f00921841707ecd0abbab86e1","client_seed":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","amount":1,"selections":[{"index":0,"tierId":2,"tierLower":25,"tierUpper":100,"tierRandom":0.36728045887217675,"rowSeed":"770bf172820f5fb60d4a6df56c133ee2b0253a929585a09c33cf64d8e29a3d27","chosenItemId":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","itemRandom":"0443e9d5108e9da5cf714a94a0f16091"}],"rarity_distribution":[{"id":0,"lower":0,"upper":10,"weight":20},{"id":1,"lower":10,"upper":25,"weight":15},{"id":2,"lower":25,"upper":100,"weight":60},{"id":3,"lower":100,"upper":1000,"weight":5}],"committed_at":null,"created_at":"2026-07-14T13:03:46.300Z","candidate_pools":[{"index":0,"item_ids":["9c14a7e2-3b6d-4f80-a512-8ed0c93b47fa","6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","297455676695","9xQeWvG4kR2mYt7BnZpL5cHdA3sJf1UvNkXrT8ePqMwZ","did:privy:cm4t7q0xz009ab21ekwn3lp8v___shadowless-base-set-booster-box-Qm3xTa___R7dLPq"],"ords":[48117,48293,48551,49120,49604]}]},{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","claw_id":"13","version":"v2","algorithm":"hmac_sha256_ord_offset","server_seed":"99d3312e91112bf5ecffd1bed393be5b6c8dd9c70d1939d5fa13fde9dfdf9188","server_seed_hash":"eb0e1f282ab61ab6011c06739731577910f6216f00921841707ecd0abbab86e1","client_seed":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","amount":1,"selections":[{"index":0,"tierId":2,"tierLower":25,"tierUpper":100,"tierRandom":0.36728045887217675,"rowSeed":"770bf172820f5fb60d4a6df56c133ee2b0253a929585a09c33cf64d8e29a3d27","chosenItemId":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","chosenItemOrd":48293}],"rarity_distribution":[{"id":0,"lower":0,"upper":10,"weight":20},{"id":1,"lower":10,"upper":25,"weight":15},{"id":2,"lower":25,"upper":100,"weight":60},{"id":3,"lower":100,"upper":1000,"weight":5}],"committed_at":null,"created_at":"2026-07-14T13:03:46.300Z","candidate_pools":[{"index":0,"item_ids":["9c14a7e2-3b6d-4f80-a512-8ed0c93b47fa","6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","297455676695","9xQeWvG4kR2mYt7BnZpL5cHdA3sJf1UvNkXrT8ePqMwZ","did:privy:cm4t7q0xz009ab21ekwn3lp8v___shadowless-base-set-booster-box-Qm3xTa___R7dLPq"],"ords":[48117,48293,48551,49120,49604]}]}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/fairness-proofs":{"get":{"operationId":"listFairnessProofs","summary":"List fairness proofs","description":"Pages through your recorded provably-fair draw proofs, newest first, one per buy session. Each row is the same proof `GET /vm/sessions/{session_id}/fairness-proof` returns, and that endpoint documents the full derivation, including what `committed_at` does and does not prove. This one is for reconciling in bulk rather than session by session.\n\nCheck each draw's `tierRandom` against the `rarity_distribution` carried on the proof itself (the bands and weights as they stood when that draw ran) rather than against the pack's current configuration, which is re-tuned over time.\n\n`session_id` narrows to one purchase's proof and `user_id` to one end-user's; sending both applies them together. The list is always scoped to your own partner, so another partner's proofs and first-party marketplace pulls are never listed. Sandbox buys are simulated rather than drawn against the fairness protocol, so they record no proof and a sandbox key always reads an empty page.","tags":["Fairness"],"parameters":[{"name":"session_id","in":"query","schema":{"type":"string","description":"Filter to the proof for this buy session id (the `session_id` from `POST /vm/buy/init`).","examples":["0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"]},"allowEmptyValue":true,"allowReserved":true},{"name":"user_id","in":"query","schema":{"type":"string","description":"Filter to proofs for this partner-defined end-user id (the `user_id` you passed at buy time).","examples":["user_42"]},"allowEmptyValue":true,"allowReserved":true},{"name":"cursor","in":"query","schema":{"type":"string","description":"Opaque pagination cursor from a previous response's `pagination.next_cursor`. Omit for the first (newest) page; pass the exact value back to continue. Junk degrades to the first page.","examples":["0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90"]},"allowEmptyValue":true,"allowReserved":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Maximum number of proofs to return (integer 1–100; defaults to 20; junk falls back to 20).","examples":[20]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"session_id":{"type":"string","description":"The buy session this proof is the evidence for — the `session_id` from `POST /vm/buy/init`."},"user_id":{"type":"string","description":"Your partner-defined user id the drawing session belongs to."},"claw_id":{"type":"string","description":"Id of the pack the draw drew from."},"version":{"const":"v1","description":"Fairness protocol version — it selects the within-tier recipe. `v1` ranks item id text: `md5(item_id + rowSeed)` over the candidate pool, smallest key wins."},"algorithm":{"const":"hmac_sha256","description":"Algorithm identifier: hmac-sha256 seed chain, with the id-keyed md5 ranking as the selection step."},"server_seed":{"type":"string","description":"The revealed server seed. Only ever readable after the pull it backs has settled — hash it to check it against `server_seed_hash`."},"server_seed_hash":{"type":"string","description":"The hash of `server_seed` — `sha256(server_seed)` must reproduce this value exactly."},"committed_at":{"anyOf":[{"type":"string","description":"An ISO 8601 instant string with a required UTC offset (e.g. 2023-01-15T13:45:30Z)","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},{"type":"null"}],"description":"When we recorded the server seed for this draw. If you called `POST /vm/buy/commit` and passed the resulting `commitment_id`, this is when that call issued your `server_seed_hash` — before the draw ran. Otherwise the seed was minted as the draw ran and this is that moment. It does NOT by itself distinguish the two: what proves a pre-commitment is that YOU were holding `server_seed_hash` before you bought, so check the revealed seed against the hash you stored, not against this timestamp."},"client_seed":{"type":"string","description":"The client seed the draw was combined with — the `client_seed` you sent on `POST /vm/buy/init`, or, when you sent none, the `session_id` the platform minted for the purchase."},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of items drawn in this pull — the length of `selections`."},"selections":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Draw index within the pull (`0` … `amount - 1`) — the RNG nonce for this item."},"tierId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Id of the rarity tier this draw landed in."},"tierLower":{"type":"number","description":"Lower bound of the tier's FMV band, in USD."},"tierUpper":{"type":"number","description":"Upper bound of the tier's FMV band, in USD, AS THE DRAW APPLIED IT — the tier's configured `upper` from `rarity_distribution`, the final tier included: the draw respects the pack's stated top bound. Proofs recorded before August 2026 treated the final tier as open-ended and carry a sentinel `999999999` here on last-tier pulls; compare against the recorded value, never re-derive that substitution."},"tierRandom":{"type":"number","description":"The RNG value that selected the tier, derived from the two seeds and this `index` — the number to re-derive when checking the draw."},"rowSeed":{"type":"string","description":"Deterministic per-item seed the within-tier pick is derived from, itself derived from the seeds."},"chosenItemId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The item this draw resolved to, or null if the tier had no stock."},"itemRandom":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"The key the winner was ranked by — `md5(chosenItemId + rowSeed)`, the smallest key in the tier winning. Recompute it to check the within-tier pick against the seeds. Null when the tier had no stock, or on draws recorded before this value was published."}},"required":["index","tierId","tierLower","tierUpper","tierRandom","rowSeed","chosenItemId"],"title":"Fairness Selection (v1)","description":"The recorded detail for one drawn item within a `v1` pull."},"description":"Per-draw detail, one entry per drawn item, in draw order — reproduced exactly as recorded."},"rarity_distribution":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"lower":{"type":"number"},"upper":{"type":"number"},"weight":{"type":"number","minimum":0}},"required":["id","lower","upper","weight"],"title":"Fairness Rarity Tier","description":"One rarity band the draw could land in, as configured when the draw ran."}},{"type":"null"}],"title":"Fairness Rarity Distribution","description":"The pack's rarity tiers as configured when this draw ran, in the order the weighted pick walked them — the bands and weights to check `tierRandom` against. Trimmed to what the draw consumes, so display-only tier fields are absent. Null when no distribution is available for the draw, typically one recorded before the capture existed.","examples":[[{"id":0,"lower":0,"upper":10,"weight":20},{"id":1,"lower":10,"upper":25,"weight":15},{"id":2,"lower":25,"upper":100,"weight":60},{"id":3,"lower":100,"upper":1000,"weight":5}]]},"created_at":{"type":"string","description":"When the draw was recorded (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"}},"required":["session_id","user_id","claw_id","version","algorithm","server_seed","server_seed_hash","committed_at","client_seed","amount","selections","rarity_distribution","created_at"]},{"type":"object","properties":{"session_id":{"type":"string","description":"The buy session this proof is the evidence for — the `session_id` from `POST /vm/buy/init`."},"user_id":{"type":"string","description":"Your partner-defined user id the drawing session belongs to."},"claw_id":{"type":"string","description":"Id of the pack the draw drew from."},"version":{"const":"v2","description":"Fairness protocol version — it selects the within-tier recipe. `v2` hashes nothing per item: the winner is the candidate pool's entry at offset `int(last 15 hex of rowSeed) mod pool size`, the pool being ascending by internal ordinal."},"algorithm":{"const":"hmac_sha256_ord_offset","description":"Algorithm identifier: the same hmac-sha256 seed chain as `v1`; the suffix names the ordinal-offset selection step that replaced the ranking."},"server_seed":{"type":"string","description":"The revealed server seed. Only ever readable after the pull it backs has settled — hash it to check it against `server_seed_hash`."},"server_seed_hash":{"type":"string","description":"The hash of `server_seed` — `sha256(server_seed)` must reproduce this value exactly."},"committed_at":{"anyOf":[{"type":"string","description":"An ISO 8601 instant string with a required UTC offset (e.g. 2023-01-15T13:45:30Z)","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},{"type":"null"}],"description":"When we recorded the server seed for this draw. If you called `POST /vm/buy/commit` and passed the resulting `commitment_id`, this is when that call issued your `server_seed_hash` — before the draw ran. Otherwise the seed was minted as the draw ran and this is that moment. It does NOT by itself distinguish the two: what proves a pre-commitment is that YOU were holding `server_seed_hash` before you bought, so check the revealed seed against the hash you stored, not against this timestamp."},"client_seed":{"type":"string","description":"The client seed the draw was combined with — the `client_seed` you sent on `POST /vm/buy/init`, or, when you sent none, the `session_id` the platform minted for the purchase."},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of items drawn in this pull — the length of `selections`."},"selections":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Draw index within the pull (`0` … `amount - 1`) — the RNG nonce for this item."},"tierId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Id of the rarity tier this draw landed in."},"tierLower":{"type":"number","description":"Lower bound of the tier's FMV band, in USD."},"tierUpper":{"type":"number","description":"Upper bound of the tier's FMV band, in USD, AS THE DRAW APPLIED IT — the tier's configured `upper` from `rarity_distribution`, the final tier included: the draw respects the pack's stated top bound. Proofs recorded before August 2026 treated the final tier as open-ended and carry a sentinel `999999999` here on last-tier pulls; compare against the recorded value, never re-derive that substitution."},"tierRandom":{"type":"number","description":"The RNG value that selected the tier, derived from the two seeds and this `index` — the number to re-derive when checking the draw."},"rowSeed":{"type":"string","description":"Deterministic per-item seed the within-tier pick is derived from, itself derived from the seeds."},"chosenItemId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The item this draw resolved to, or null if the tier had no stock."},"chosenItemOrd":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"The winner's internal ordinal, and what the offset recipe must land on: reduce the LAST 15 hex chars of `rowSeed` (as an integer — 60 bits, positive in any signed 64-bit type) modulo the matching candidate pool's size, and that pool's `ords` entry at the offset — or `k` slots past it, wrapping, `k` being concurrently-locked candidates the draw skipped — is this value. Null when the tier had no stock."}},"required":["index","tierId","tierLower","tierUpper","tierRandom","rowSeed","chosenItemId","chosenItemOrd"],"title":"Fairness Selection (v2)","description":"The recorded detail for one drawn item within a `v2` pull."},"description":"Per-draw detail, one entry per drawn item, in draw order — reproduced exactly as recorded."},"rarity_distribution":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"lower":{"type":"number"},"upper":{"type":"number"},"weight":{"type":"number","minimum":0}},"required":["id","lower","upper","weight"],"title":"Fairness Rarity Tier","description":"One rarity band the draw could land in, as configured when the draw ran."}},{"type":"null"}],"title":"Fairness Rarity Distribution","description":"The pack's rarity tiers as configured when this draw ran, in the order the weighted pick walked them — the bands and weights to check `tierRandom` against. Trimmed to what the draw consumes, so display-only tier fields are absent. Null when no distribution is available for the draw, typically one recorded before the capture existed.","examples":[[{"id":0,"lower":0,"upper":10,"weight":20},{"id":1,"lower":10,"upper":25,"weight":15},{"id":2,"lower":25,"upper":100,"weight":60},{"id":3,"lower":100,"upper":1000,"weight":5}]]},"created_at":{"type":"string","description":"When the draw was recorded (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"}},"required":["session_id","user_id","claw_id","version","algorithm","server_seed","server_seed_hash","committed_at","client_seed","amount","selections","rarity_distribution","created_at"]}],"title":"Fairness Proof","description":"The provably-fair commit-reveal evidence for one buy session's draw.","examples":[{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","claw_id":"13","version":"v1","algorithm":"hmac_sha256","server_seed":"99d3312e91112bf5ecffd1bed393be5b6c8dd9c70d1939d5fa13fde9dfdf9188","server_seed_hash":"eb0e1f282ab61ab6011c06739731577910f6216f00921841707ecd0abbab86e1","client_seed":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","amount":1,"selections":[{"index":0,"tierId":2,"tierLower":25,"tierUpper":100,"tierRandom":0.36728045887217675,"rowSeed":"770bf172820f5fb60d4a6df56c133ee2b0253a929585a09c33cf64d8e29a3d27","chosenItemId":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","itemRandom":"0443e9d5108e9da5cf714a94a0f16091"}],"rarity_distribution":[{"id":0,"lower":0,"upper":10,"weight":20},{"id":1,"lower":10,"upper":25,"weight":15},{"id":2,"lower":25,"upper":100,"weight":60},{"id":3,"lower":100,"upper":1000,"weight":5}],"committed_at":null,"created_at":"2026-07-14T13:03:46.300Z"},{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","claw_id":"13","version":"v2","algorithm":"hmac_sha256_ord_offset","server_seed":"99d3312e91112bf5ecffd1bed393be5b6c8dd9c70d1939d5fa13fde9dfdf9188","server_seed_hash":"eb0e1f282ab61ab6011c06739731577910f6216f00921841707ecd0abbab86e1","client_seed":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","amount":1,"selections":[{"index":0,"tierId":2,"tierLower":25,"tierUpper":100,"tierRandom":0.36728045887217675,"rowSeed":"770bf172820f5fb60d4a6df56c133ee2b0253a929585a09c33cf64d8e29a3d27","chosenItemId":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","chosenItemOrd":48293}],"rarity_distribution":[{"id":0,"lower":0,"upper":10,"weight":20},{"id":1,"lower":10,"upper":25,"weight":15},{"id":2,"lower":25,"upper":100,"weight":60},{"id":3,"lower":100,"upper":1000,"weight":5}],"committed_at":null,"created_at":"2026-07-14T13:03:46.300Z"}]},"description":"The page of items, in the endpoint's stable order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"title":"Fairness Proof List","description":"A page of the caller's fairness proofs (`GET /vm/fairness-proofs`).","examples":[{"data":[{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","claw_id":"13","version":"v1","algorithm":"hmac_sha256","server_seed":"99d3312e91112bf5ecffd1bed393be5b6c8dd9c70d1939d5fa13fde9dfdf9188","server_seed_hash":"eb0e1f282ab61ab6011c06739731577910f6216f00921841707ecd0abbab86e1","client_seed":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","amount":1,"selections":[{"index":0,"tierId":2,"tierLower":25,"tierUpper":100,"tierRandom":0.36728045887217675,"rowSeed":"770bf172820f5fb60d4a6df56c133ee2b0253a929585a09c33cf64d8e29a3d27","chosenItemId":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","itemRandom":"0443e9d5108e9da5cf714a94a0f16091"}],"rarity_distribution":[{"id":0,"lower":0,"upper":10,"weight":20},{"id":1,"lower":10,"upper":25,"weight":15},{"id":2,"lower":25,"upper":100,"weight":60},{"id":3,"lower":100,"upper":1000,"weight":5}],"committed_at":null,"created_at":"2026-07-14T13:03:46.300Z"},{"session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","user_id":"user_42","claw_id":"13","version":"v2","algorithm":"hmac_sha256_ord_offset","server_seed":"99d3312e91112bf5ecffd1bed393be5b6c8dd9c70d1939d5fa13fde9dfdf9188","server_seed_hash":"eb0e1f282ab61ab6011c06739731577910f6216f00921841707ecd0abbab86e1","client_seed":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","amount":1,"selections":[{"index":0,"tierId":2,"tierLower":25,"tierUpper":100,"tierRandom":0.36728045887217675,"rowSeed":"770bf172820f5fb60d4a6df56c133ee2b0253a929585a09c33cf64d8e29a3d27","chosenItemId":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","chosenItemOrd":48293}],"rarity_distribution":[{"id":0,"lower":0,"upper":10,"weight":20},{"id":1,"lower":10,"upper":25,"weight":15},{"id":2,"lower":25,"upper":100,"weight":60},{"id":3,"lower":100,"upper":1000,"weight":5}],"committed_at":null,"created_at":"2026-07-14T13:03:46.300Z"}],"pagination":{"limit":20,"count":2,"has_more":false,"next_cursor":null}}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/buyback/status":{"post":{"operationId":"vm.buybackStatus","summary":"Look up a buyback (deprecated)","description":"**Deprecated.** Prefer `GET /vm/buybacks/{buyback_id}` or `GET /vm/buybacks/by-idempotency-key/{idempotency_key}`, which expose each single-key lookup as its own URL so you don't have to construct the request-body union. This POST route stays supported for back-compat (no removal date) and resolves the identical outcome.\n\nLooks up a `POST /vm/buyback` by `buyback_id` and/or by the idempotency key you sent with it, scoped to your partner. At least one is required; supplying neither is a validation error. Sending both cross-checks them: `buyback_id` is the lookup key and `idempotency_key` must be the key that buyback was credited under, so a pair naming two different buybacks resolves `not_found`.\n\nAn `item_id` is deliberately not a key here. A sold-back card returns to the shared pool and can be bought and sold back again, so one item accumulates many buybacks and no single one of them is _the_ buyback for that item.","deprecated":true,"tags":["Purchases"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"buyback_id":{"type":"string","description":"Buyback id returned by `POST /vm/buyback`.","examples":["0194f0a3-1d4e-7b2f-8c3d-6a9f5e2b7d01"]},"idempotency_key":{"type":"string","description":"Optional cross-check when looking up by `buyback_id`: the Idempotency-Key the buyback was credited under. The lookup resolves `not_found` if it was credited under a different key.","examples":["buy-rookie-2026-07-14-001"]}},"required":["buyback_id"]},{"type":"object","properties":{"idempotency_key":{"type":"string","description":"The Idempotency-Key (header or body) sent with the original `/vm/buyback` call, scoped to your partner.","examples":["buy-rookie-2026-07-14-001"]}},"required":["idempotency_key"]}],"examples":[{"buyback_id":"0194f0a3-1d4e-7b2f-8c3d-6a9f5e2b7d01"}]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"status":{"const":"credited","description":"The buyback completed; see `result`."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"By `buyback_id`: the Idempotency-Key that buyback was made with, or null when none was sent. By `idempotency_key`: the key this result was resolved for (echoed back)."},"result":{"type":"object","properties":{"buyback_id":{"type":"string","description":"Platform-minted id for this buyback: the key `POST /vm/buyback/status` takes."},"item_id":{"type":"string","description":"The item that was sold back."},"amount":{"type":"number","description":"Amount credited for this buyback, in USD."},"buy_session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `session_id` of the `/vm/buy/init` that bought the item in THIS round, recorded when the buyback was written, so a later re-purchase of the same item never rewrites it. Every buyback credited today carries one; null only for a buyback credited before the round was recorded, or one whose buy session has since been purged."},"credited_at":{"type":"string","description":"When the buyback was credited (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"card":{"anyOf":[{"$ref":"#/components/schemas/Item"},{"type":"null"}],"description":"The card sold back. A buyback is always a single item, in the standard partner NFT/item shape. Null only when the card record can't be resolved."},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The partner-defined user id that sold the item back: the id you passed at buy time."}},"required":["buyback_id","item_id","amount","buy_session_id","credited_at","card","user_id"],"title":"Buyback","description":"A single credited buyback."}},"required":["status","idempotency_key","result"]},{"type":"object","properties":{"status":{"const":"not_found","description":"Unknown `buyback_id`, or one belonging to a different partner. Never leaked as a different status."},"idempotency_key":{"type":"null","description":"Always null; no buyback was found."},"result":{"type":"null","description":"Always null; no buyback was found."}},"required":["status","idempotency_key","result"]},{"type":"object","properties":{"status":{"const":"unknown_key","description":"No buyback is on record for this `idempotency_key`."},"idempotency_key":{"type":"string","description":"The `idempotency_key` that resolved to no buyback (echoed back)."},"result":{"type":"null","description":"Always null; no buyback is on record for this key."}},"required":["status","idempotency_key","result"]}],"title":"Buyback Lookup Result","description":"Durable buyback lookup, by `buyback_id` or `idempotency_key` (`POST /vm/buyback/status`).","examples":[{"status":"credited","idempotency_key":"buy-rookie-2026-07-14-001","result":{"buyback_id":"0194f0a3-1d4e-7b2f-8c3d-6a9f5e2b7d01","item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","amount":361.39,"buy_session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","credited_at":"2026-07-14T14:10:00.000Z","card":{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null},"user_id":"user_42"}}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/buybacks":{"get":{"operationId":"vm.buybacks","summary":"List buybacks","description":"Pages through your credited buybacks, newest first. Each row is the same shape the single-buyback lookups return, so the list and `GET /vm/buybacks/{buyback_id}` never drift.\n\n`item_id` selects buybacks of one card, and legitimately returns several, because a sold-back card goes back into the shared pool and can be bought and sold back again, one buyback per round. `user_id` selects buybacks for one of your end-users, using the partner-defined id you passed at buy time. Sending both applies them together. The list is always scoped to your own partner.","tags":["Purchases"],"parameters":[{"name":"item_id","in":"query","schema":{"type":"string","description":"Filter to buybacks of this item id. One item can have many buybacks (one per round it is bought).","examples":["6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK"]},"allowEmptyValue":true,"allowReserved":true},{"name":"user_id","in":"query","schema":{"type":"string","description":"Filter to buybacks made for this partner-defined end-user id (the `user_id` you passed at buy time).","examples":["user_42"]},"allowEmptyValue":true,"allowReserved":true},{"name":"cursor","in":"query","schema":{"type":"string","description":"Pagination cursor: pass the `next_cursor` from the previous page. Omit for the first (newest) page. Opaque: treat it as a handle to pass back verbatim.","examples":["128"]},"allowEmptyValue":true,"allowReserved":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Maximum number of buybacks to return (integer 1–100; defaults to 20; junk falls back to 20).","examples":[20]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"buyback_id":{"type":"string","description":"Platform-minted id for this buyback: the key `POST /vm/buyback/status` takes."},"item_id":{"type":"string","description":"The item that was sold back."},"amount":{"type":"number","description":"Amount credited for this buyback, in USD."},"buy_session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `session_id` of the `/vm/buy/init` that bought the item in THIS round, recorded when the buyback was written, so a later re-purchase of the same item never rewrites it. Every buyback credited today carries one; null only for a buyback credited before the round was recorded, or one whose buy session has since been purged."},"credited_at":{"type":"string","description":"When the buyback was credited (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"card":{"anyOf":[{"$ref":"#/components/schemas/Item"},{"type":"null"}],"description":"The card sold back. A buyback is always a single item, in the standard partner NFT/item shape. Null only when the card record can't be resolved."},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The partner-defined user id that sold the item back: the id you passed at buy time."}},"required":["buyback_id","item_id","amount","buy_session_id","credited_at","card","user_id"],"title":"Buyback","description":"A single credited buyback."},"description":"The page of items, in the endpoint's stable order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"title":"Buyback List","description":"A page of credited buybacks (`GET /vm/buybacks`).","examples":[{"data":[{"buyback_id":"0194f0a3-1d4e-7b2f-8c3d-6a9f5e2b7d01","item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","amount":361.39,"buy_session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","credited_at":"2026-07-14T14:10:00.000Z","card":{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null},"user_id":"user_42"}],"pagination":{"limit":20,"count":1,"has_more":false,"next_cursor":null}}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/buybacks/{buyback_id}":{"get":{"operationId":"getBuyback","summary":"Look up a buyback by id","description":"Resolves one buyback by the `buyback_id` that `POST /vm/buyback` returned, scoped to your partner. Together with `GET /vm/buybacks/by-idempotency-key/{idempotency_key}` this is the long-term lookup surface; the deprecated `POST /vm/buyback/status` stays supported for back-compat and resolves the identical outcome.\n\nA miss is still an HTTP `200` carrying a `not_found` envelope, not a `404`, so branch on the `status` field rather than the status code. This is the opposite convention from the session lookups, which do `404`.\n\nUse `GET /vm/buybacks/by-idempotency-key/{idempotency_key}` when you hold your own key but never learned our id, and `GET /vm/buybacks` to page through them all.","tags":["Purchases"],"parameters":[{"name":"buyback_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Buyback id returned by `POST /vm/buyback`.","examples":["0194f0a3-1d4e-7b2f-8c3d-6a9f5e2b7d01"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"status":{"const":"credited","description":"The buyback completed; see `result`."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"By `buyback_id`: the Idempotency-Key that buyback was made with, or null when none was sent. By `idempotency_key`: the key this result was resolved for (echoed back)."},"result":{"type":"object","properties":{"buyback_id":{"type":"string","description":"Platform-minted id for this buyback: the key `POST /vm/buyback/status` takes."},"item_id":{"type":"string","description":"The item that was sold back."},"amount":{"type":"number","description":"Amount credited for this buyback, in USD."},"buy_session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `session_id` of the `/vm/buy/init` that bought the item in THIS round, recorded when the buyback was written, so a later re-purchase of the same item never rewrites it. Every buyback credited today carries one; null only for a buyback credited before the round was recorded, or one whose buy session has since been purged."},"credited_at":{"type":"string","description":"When the buyback was credited (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"card":{"anyOf":[{"$ref":"#/components/schemas/Item"},{"type":"null"}],"description":"The card sold back. A buyback is always a single item, in the standard partner NFT/item shape. Null only when the card record can't be resolved."},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The partner-defined user id that sold the item back: the id you passed at buy time."}},"required":["buyback_id","item_id","amount","buy_session_id","credited_at","card","user_id"],"title":"Buyback","description":"A single credited buyback."}},"required":["status","idempotency_key","result"]},{"type":"object","properties":{"status":{"const":"not_found","description":"Unknown `buyback_id`, or one belonging to a different partner. Never leaked as a different status."},"idempotency_key":{"type":"null","description":"Always null; no buyback was found."},"result":{"type":"null","description":"Always null; no buyback was found."}},"required":["status","idempotency_key","result"]},{"type":"object","properties":{"status":{"const":"unknown_key","description":"No buyback is on record for this `idempotency_key`."},"idempotency_key":{"type":"string","description":"The `idempotency_key` that resolved to no buyback (echoed back)."},"result":{"type":"null","description":"Always null; no buyback is on record for this key."}},"required":["status","idempotency_key","result"]}],"title":"Buyback Lookup Result","description":"Durable buyback lookup, by `buyback_id` or `idempotency_key` (`POST /vm/buyback/status`).","examples":[{"status":"credited","idempotency_key":"buy-rookie-2026-07-14-001","result":{"buyback_id":"0194f0a3-1d4e-7b2f-8c3d-6a9f5e2b7d01","item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","amount":361.39,"buy_session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","credited_at":"2026-07-14T14:10:00.000Z","card":{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null},"user_id":"user_42"}}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/buybacks/by-idempotency-key/{idempotency_key}":{"get":{"operationId":"getBuybackByIdempotencyKey","summary":"Look up a buyback by idempotency key","description":"Resolves a buyback by the idempotency key you sent with the original `POST /vm/buyback`, scoped to your partner. This is the lookup for the case where the buyback's response never reached you: you still hold the key you sent, even without our `buyback_id`. Together with `GET /vm/buybacks/{buyback_id}` this is the long-term lookup surface; the deprecated `POST /vm/buyback/status` stays supported for back-compat.\n\nOne key resolves to at most one buyback, since reusing a key on a new buyback is rejected at write time with `409 DUPLICATE_REQUEST`, so this lookup is never ambiguous. A miss is still an HTTP `200` carrying an `unknown_key` envelope rather than a `404`, so branch on the `status` field.","tags":["Purchases"],"parameters":[{"name":"idempotency_key","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"The Idempotency-Key sent with the original `/vm/buyback` call, scoped to your partner.","examples":["buy-rookie-2026-07-14-001"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"status":{"const":"credited","description":"The buyback completed; see `result`."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"By `buyback_id`: the Idempotency-Key that buyback was made with, or null when none was sent. By `idempotency_key`: the key this result was resolved for (echoed back)."},"result":{"type":"object","properties":{"buyback_id":{"type":"string","description":"Platform-minted id for this buyback: the key `POST /vm/buyback/status` takes."},"item_id":{"type":"string","description":"The item that was sold back."},"amount":{"type":"number","description":"Amount credited for this buyback, in USD."},"buy_session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `session_id` of the `/vm/buy/init` that bought the item in THIS round, recorded when the buyback was written, so a later re-purchase of the same item never rewrites it. Every buyback credited today carries one; null only for a buyback credited before the round was recorded, or one whose buy session has since been purged."},"credited_at":{"type":"string","description":"When the buyback was credited (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"card":{"anyOf":[{"$ref":"#/components/schemas/Item"},{"type":"null"}],"description":"The card sold back. A buyback is always a single item, in the standard partner NFT/item shape. Null only when the card record can't be resolved."},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The partner-defined user id that sold the item back: the id you passed at buy time."}},"required":["buyback_id","item_id","amount","buy_session_id","credited_at","card","user_id"],"title":"Buyback","description":"A single credited buyback."}},"required":["status","idempotency_key","result"]},{"type":"object","properties":{"status":{"const":"not_found","description":"Unknown `buyback_id`, or one belonging to a different partner. Never leaked as a different status."},"idempotency_key":{"type":"null","description":"Always null; no buyback was found."},"result":{"type":"null","description":"Always null; no buyback was found."}},"required":["status","idempotency_key","result"]},{"type":"object","properties":{"status":{"const":"unknown_key","description":"No buyback is on record for this `idempotency_key`."},"idempotency_key":{"type":"string","description":"The `idempotency_key` that resolved to no buyback (echoed back)."},"result":{"type":"null","description":"Always null; no buyback is on record for this key."}},"required":["status","idempotency_key","result"]}],"title":"Buyback Lookup Result","description":"Durable buyback lookup, by `buyback_id` or `idempotency_key` (`POST /vm/buyback/status`).","examples":[{"status":"credited","idempotency_key":"buy-rookie-2026-07-14-001","result":{"buyback_id":"0194f0a3-1d4e-7b2f-8c3d-6a9f5e2b7d01","item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","amount":361.39,"buy_session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","credited_at":"2026-07-14T14:10:00.000Z","card":{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null},"user_id":"user_42"}}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/packs/{pack_slug}/items":{"get":{"operationId":"getPackInventory","summary":"List a pack's backing inventory","description":"Enumerates the live backing inventory of a pack: every item currently in the pool that backs the pack's expected value, each with its own `fmv` and `category`. Items come back in the same shape `GET /inventory/available` returns, so one renderer handles both. This is the item-level breakdown behind the EV band `GET /vm/available` reports (the same set of items, under the same availability and rarity-tier eligibility, that the EV is computed from), so it is what to use when you want to show buyers what is actually in a pack rather than a summary number. The whole backing set is paginated in a stable order.\n\nAccess is scoped to the packs you can browse. `pack_slug` is the `slug` from `GET /vm/available`\n(always set on packs that listing returns). `VM_NOT_FOUND` when the pack is not available to\nyour partner.","tags":["Packs"],"parameters":[{"name":"pack_slug","in":"path","required":true,"schema":{"type":"string","description":"Pack slug (the `slug` from `/vm/available`; always set on packs that listing returns).","examples":["rookie-pack"]}},{"name":"cursor","in":"query","schema":{"type":"string","description":"Opaque pagination cursor from a previous response's `pagination.next_cursor`. Omit for the first page; pass back the exact value to continue. An absent, empty, or oversized cursor reads as the first page, but a corrupted-but-plausible one is not reset, so it may return an empty page.","examples":["6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK"]},"allowEmptyValue":true,"allowReserved":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50,"description":"Maximum items per page (integer 1–100; defaults to 50; junk falls back to 50).","examples":[50]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InventoryItem"},"description":"The page of items, in the endpoint's stable order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"title":"Pack Backing Inventory","description":"A page of a pack's backing inventory (`GET /packs/{pack_slug}/items`).","examples":[{"data":[{"id":"Atv5Xigpcf7EvDqKJxoyp8CxZeo8m8NZdjFkER8dEi7c","name":"2000 Pokemon Gym Challenge 1st Edition Holo Brock's Ninetales #3 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2FApFiQSzv1NCRUpSZtzedkKYhuYLyo91xpLb5FcMSsdVD-cropped","fmv":2182.12,"category":"pokemon"},{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","fmv":425.17,"category":"pokemon"}],"pagination":{"limit":50,"count":128,"has_more":true,"next_cursor":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK"}}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","VM_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","VM_NOT_FOUND":"Pack not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"VM_NOT_FOUND":{"summary":"Pack not found or not visible to you","value":{"error":"Pack not found","code":"VM_NOT_FOUND"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/inventory/available":{"get":{"operationId":"listAvailableInventory","summary":"Browse available inventory","description":"The pool of vault items currently available to back a pack: every in-stock, unsold, vault-custodied deposit that is eligible to be added to a pack, each with its per-item `fmv` and `category`. This is the same eligibility the pack builder applies, so an item listed here is one that can actually go into a pack, never a sold, reserved, or out-of-custody card.\n\nNarrow it with `category` (repeat the parameter for several), order it with `sort`, and scope it to one pack with `pack_slug`: `pack_filter=exclude` (the default) lists what could still be ADDED to that pack, `pack_filter=include` lists that pack's members that are still pack-eligible. Both modes apply the eligibility gate above, so `include` is a view of the pack's currently-addable contents, not a full manifest. A member that has gone ineligible, whether reserved or sold by a draw, out of custody, or missing a title/FMV, is omitted, and `pagination.count` counts only what is listed. For a pack's complete backing inventory use `GET /packs/{pack_slug}/items` instead.\n\nWithout `pack_slug` the whole eligible pool is browsed. Every partner credential may do this, and the page it returns is the same first-party supply, with the same per-item `fmv`, whichever key asks. That is deliberate: the pool is the first-party catalog you build packs from, so it is published to partners rather than scoped per account.\n\nResults come back in the standard cursor-native page envelope (`data` + `pagination`). Read the first page without a `cursor`, then follow `pagination.next_cursor` until `pagination.has_more` is false, keeping `sort` and the filters identical for the whole walk (a cursor is an anchor into ONE ordering; changing the sort or filters mid-walk makes the remaining pages meaningless). `pack_slug` is scoped to packs you can browse: an unknown pack, or one you're not authorized to see, returns `404 VM_NOT_FOUND` (it never discloses another partner's pack). Unknown `category` / `sort` / `pack_filter` values are rejected as `400` validation errors, listing the accepted values; an out-of-range or junk `limit` degrades to the default instead.","tags":["Items"],"parameters":[{"name":"category","in":"query","schema":{"anyOf":[{"enum":["pokemon","basketball","football","baseball","one piece","soccer","watches","streetwear","riftbound","dragon ball","yugioh","pop culture","azuki","fwog","koda","neuko","vibes"],"type":"string"},{"type":"array","items":{"enum":["pokemon","basketball","football","baseball","one piece","soccer","watches","streetwear","riftbound","dragon ball","yugioh","pop culture","azuki","fwog","koda","neuko","vibes"],"type":"string"}}],"description":"Category to restrict the pool to; repeat the parameter for several. Omit for every category.","examples":[["pokemon"]]},"allowEmptyValue":true,"allowReserved":true},{"name":"sort","in":"query","schema":{"enum":["fmv-high-low","fmv-low-high","name-asc","name-desc","category-asc","category-desc"],"type":"string","default":"fmv-high-low","description":"Result ordering. `fmv-high-low` (the default) and `fmv-low-high` order by value, `name-asc` / `name-desc` by the item's stored title, `category-asc` / `category-desc` by category. Every ordering breaks ties on item id, so a page walk is stable.","examples":["fmv-high-low"]},"allowEmptyValue":true,"allowReserved":true},{"name":"pack_slug","in":"query","schema":{"type":"string","description":"Optional pack to scope the pool to, by slug (the `slug` from `/vm/available`; always set on packs that listing returns). Combine with `pack_filter`. Omit to browse the whole eligible pool.","examples":["rookie-pack"]},"allowEmptyValue":true,"allowReserved":true},{"name":"pack_filter","in":"query","schema":{"enum":["exclude","include"],"type":"string","default":"exclude","description":"How `pack_slug` scopes the pool: `exclude` (the default) lists items NOT yet in that pack (what could still be added to it, narrowed to the vaults that pack accepts), while `include` lists that pack's members that are still pack-eligible (not its full manifest; use `GET /packs/{pack_slug}/items` for that). Ignored when `pack_slug` is omitted.","examples":["exclude"]},"allowEmptyValue":true,"allowReserved":true},{"name":"cursor","in":"query","schema":{"type":"string","description":"Opaque pagination cursor from a previous response's `pagination.next_cursor`. Omit for the first page; pass back the exact value to continue, keeping `sort` and the filters unchanged. An absent, empty, or oversized cursor reads as the first page, but a corrupted-but-plausible one is not reset, so it may return an empty page.","examples":["6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK"]},"allowEmptyValue":true,"allowReserved":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":1000,"default":50,"description":"Maximum items per page (integer 1–1000; defaults to 50; junk falls back to 50).","examples":[50]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InventoryItem"},"description":"The page of items, in the endpoint's stable order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"title":"Available Inventory","description":"A page of the pack-eligible supply pool (`GET /inventory/available`).","examples":[{"data":[{"id":"Atv5Xigpcf7EvDqKJxoyp8CxZeo8m8NZdjFkER8dEi7c","name":"2000 Pokemon Gym Challenge 1st Edition Holo Brock's Ninetales #3 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2FApFiQSzv1NCRUpSZtzedkKYhuYLyo91xpLb5FcMSsdVD-cropped","fmv":2182.12,"category":"pokemon"},{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","fmv":425.17,"category":"pokemon"}],"pagination":{"limit":50,"count":1284,"has_more":true,"next_cursor":"ord:1287431"}}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","VM_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","VM_NOT_FOUND":"Pack not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"VM_NOT_FOUND":{"summary":"Pack not found or not visible to you","value":{"error":"Pack not found","code":"VM_NOT_FOUND"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/inventory/{user_id}":{"get":{"operationId":"inventory","summary":"List a user's items","description":"Lists the cards one of your end-users currently holds: everything they have bought and neither sold back nor shipped. `user_id` is your own opaque id for that person, the same one you passed to `POST /vm/buy/init`; it is scoped to your partner, so the same string under a different partner is a different user. There is no cursor: the response is a bare array capped at **1000** items.\n\nTwo fields drive the sellback decision and are worth reading together: `buyback_price` is the exact amount `POST /vm/buyback` will credit for the card, and `buyback_expires_at` is **7 days** after purchase. Once it has passed, that call rejects the card with `ITEM_EXPIRED`. If your users expect sellback at any time, surface that deadline; a shorter forced-buyback window is something you enforce in your storefront, not this API.\n\nEvery holding is shippable, graded or ungraded, so pass any item id to `POST /ship/quote`. The optional `shipping` block (live only) describes internal fulfillment routing; do not treat `eligible: false` as user-facing \"you cannot ship this card\". `mint_address` / `collection_address` / `token_standard` are identifiers, not evidence of an on-chain payment.\n\n`404 USER_NOT_FOUND` means this user has no purchase history with you at all; it is not the empty-inventory response. A user who bought and then sold back or shipped everything returns `200` with an empty `items` array, so branch on the array's length rather than on the status code.","tags":["Items"],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","description":"Your partner-defined user id whose items to list.","examples":["user_42"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string","description":"User id the inventory belongs to."},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Item id."},"content":{"type":"object","properties":{"metadata":{"type":"object","properties":{"name":{"type":"string","description":"Display name of the card."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Back image URL, or null when the card has no back asset."},"attributes":{"type":"array","items":{"$ref":"#/components/schemas/CardAttribute"},"description":"Trait list shown on the card detail view."}},"required":["name","image","back_image","attributes"],"title":"Card Metadata","description":"Card metadata block.","examples":[{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}]},"links":{"type":"object","properties":{"image":{"type":"string","description":"Front image URL."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Back image URL, or null."}},"required":["image","back_image"],"description":"Convenience image links."}},"required":["metadata","links"],"description":"Item content envelope."},"buyback_price":{"type":"number","description":"Current buyback price in USD."},"type":{"type":"string","description":"Item type/kind."},"mint_address":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Identifier for the item when one exists; not an on-chain settlement. Often null."},"collection_address":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Collection identifier when one exists; not an on-chain settlement. Often null."},"token_standard":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Token-standard label when one exists, or null."},"claw_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pack id the item was pulled from, or null."},"claw_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pack slug the item was pulled from. Null only for historical rows; treat it as present."},"purchased_at":{"type":"string","description":"When the item was acquired (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"buyback_expires_at":{"type":"string","description":"When the Phygitals buyback offer expires (ISO 8601), 7 days after purchase, uniform across packs. After this, `POST /vm/buyback` returns `ITEM_EXPIRED`.","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"shipping":{"type":"object","properties":{"eligible":{"type":"boolean","description":"Whether this item can currently be shipped/claimed (e.g. false when its ship method is disabled)."},"method":{"anyOf":[{"enum":["psa_cc","fanatics","alt","fwog","neuko","sealed","external","rip"],"type":"string"},{"type":"null"}],"description":"The fulfillment method that would handle this item, or null when it can't be classified."},"reason":{"type":"string","description":"A human-readable explanation of the eligibility + method decision."}},"required":["eligible","method","reason"],"description":"Internal fulfillment routing (live inventory only; omitted by sandbox). Every holding is shippable, so do not use `eligible` as a user-facing 'cannot ship' gate. `method` names the vault vendor path (psa_cc, fanatics, alt, fwog, neuko, sealed, external, rip)."}},"required":["id","content","buyback_price","type","mint_address","collection_address","token_standard","claw_id","claw_slug","purchased_at","buyback_expires_at"]},"description":"The user's current (unsold) items."}},"required":["user_id","items"],"title":"Inventory","description":"A user's inventory (`GET /inventory/{user_id}`).","examples":[{"user_id":"user_42","items":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","content":{"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]},"links":{"image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null}},"buyback_price":361.39,"type":"enft","mint_address":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","collection_address":null,"token_standard":null,"claw_id":"13","claw_slug":"rookie-pack","purchased_at":"2026-07-14T13:03:46.300Z","buyback_expires_at":"2026-07-21T13:03:46.300Z","shipping":{"eligible":true,"method":"alt","reason":"Graded vault item fulfilled via the alt vault."}}]}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","USER_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","USER_NOT_FOUND":"User not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"USER_NOT_FOUND":{"value":{"error":"User not found","code":"USER_NOT_FOUND"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/card/{item_id}":{"get":{"operationId":"card","summary":"Get an item","description":"Returns the public detail for a single card. This is a catalog read, not an ownership read: it resolves any card Phygitals knows about, whether or not one of your end-users holds it, so use it to render a card page from an id you got out of a pull, a pack's backing inventory, or a chase list. For what a specific user actually owns, call `GET /inventory/{user_id}`.\n\n`item_id` accepts three kinds of identifier and resolves them in order: first as an `EbayListing` id, then, if that misses, as an NFT mint address or a slug. The `id` in the response is the identifier the lookup landed on, which is the mint address when you looked the card up by slug, so echo back what the response gives you rather than the string you sent.\n\nA card with no valuation on file reads as `fmv: 0` rather than null, so treat a zero FMV as \"not priced\" rather than \"worthless\".","tags":["Items"],"parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","description":"Item identifier: an EbayListing id, NFT mint address, or slug.","examples":["6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Card item id."},"name":{"type":"string","description":"Card display name."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Back image URL, or null."},"fmv":{"type":"number","description":"Fair market value in USD, rounded to cents."},"metadata":{"type":"object","properties":{"name":{"type":"string","description":"Display name of the card."},"image":{"type":"string","description":"Front image URL (CDN-cropped)."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Back image URL, or null when the card has no back asset."},"attributes":{"type":"array","items":{"$ref":"#/components/schemas/CardAttribute"},"description":"Trait list shown on the card detail view."}},"required":["name","image","back_image","attributes"],"title":"Card Metadata","description":"Full card metadata block.","examples":[{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}]}},"required":["id","name","image","back_image","fmv","metadata"],"title":"Card Detail","description":"Card detail resolved by item id, NFT address or slug (`GET /card/{item_id}`).","examples":[{"id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"fmv":425.17,"metadata":{"name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null,"attributes":[{"trait_type":"Grade","value":"PSA 10"},{"trait_type":"Category","value":"Pokemon"}]}}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","CARD_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","CARD_NOT_FOUND":"Card not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"CARD_NOT_FOUND":{"value":{"error":"Card not found","code":"CARD_NOT_FOUND"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/vm/buyback":{"post":{"operationId":"buyback","summary":"Sell an item back","description":"Sells one item back to Phygitals and credits your prepaid partner ledger with the payout. One item per call. The card returns to the shared pool, where it becomes drawable again by a later buy, which is why an item can accumulate several buybacks over its life, one per round it is bought. Settlement is synchronous: when this call returns `success`, the credit has landed. Live for live keys; simulated for sandbox keys. There is no `buyback.failed` webhook; only `buyback.settled` fires.\n\nThe payout is fixed by the terms of the purchase, not by today's market: `amount` is the item's FMV as recorded when your user bought it, multiplied by the pack's buyback percent as it stood at that same moment. A pack repriced afterwards does not reprice an offer already earned, in either direction. That amount is the `buyback_price` already shown on the item by `GET /inventory/{user_id}`, so you can display the exact figure before the user commits. The Phygitals offer lasts **7 days** from purchase (`buyback_expires_at`); after that this call rejects the item with `ITEM_EXPIRED`. A shorter \"must sell back by\" rule is yours to enforce.\n\n`BUYBACK_ITEM_NOT_FOUND` covers three distinct situations behind one code: the item is unknown, it is not held by one of your end-users, or it lost a race with a concurrent buyback of the same card. A short prepaid balance is not a failure mode here, since a buyback credits the ledger rather than debiting it.","tags":["Purchases"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"item_id":{"type":"string","minLength":1,"description":"Id of the item to sell back."},"idempotency_key":{"type":"string","description":"Optional idempotency key for this write. Equivalent to the `Idempotency-Key` header; if both are sent, this body value wins. A repeat call under a key that already succeeded is never re-executed: it replays the original response (with an `idempotency-replayed: true` header), or under a race fails `409 DUPLICATE_REQUEST`. Reusing a key with a different body is rejected `422 IDEMPOTENCY_KEY_REUSED`; a key whose only prior attempt failed is not blocked (the retry runs fresh, since nothing was charged). See the overview's Idempotency section."}},"required":["item_id"],"examples":[{"item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK"}]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"True when the buyback completed."},"buyback_id":{"type":"string","description":"Platform-minted id for THIS buyback: the durable key to `POST /vm/buyback/status`. An item can be sold back once per round it is bought, so the item id does not identify a buyback; this does."},"amount":{"type":"number","description":"Amount credited for the buyback, in USD."},"buy_session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `session_id` of the `/vm/buy/init` that bought this item, which lets a caller reconcile the sale against the original purchase. Only an item bought through `/vm/buy/init` can be sold back, so this is populated in practice; it is nullable only as a fallback for a card whose buy round cannot be named (a buyback is still credited in that case)."},"wallet_game_code":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Wallet game code the originating buy was debited under, when the purchase came through an embedded player session that carried one (a wallet round only resolves inside the game it was opened in, so the buyback's win is settled under this code). Null for direct API purchases."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The effective idempotency key that applied to this request (the body `idempotency_key` if sent, else the `Idempotency-Key` header), or null if none."}},"required":["success","buyback_id","amount","buy_session_id","wallet_game_code","idempotency_key"],"title":"Buyback Result","description":"Result of selling an item back (`POST /vm/buyback`).","examples":[{"success":true,"buyback_id":"0194f0a3-1d4e-7b2f-8c3d-6a9f5e2b7d01","amount":361.39,"buy_session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","wallet_game_code":null,"idempotency_key":"buy-rookie-2026-07-14-001"}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","ITEM_EXPIRED","BUYBACK_ITEM_NOT_FOUND","ITEM_NOT_ELIGIBLE_FOR_BUYBACK","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","ITEM_EXPIRED":"Item has expired","BUYBACK_ITEM_NOT_FOUND":"Item not found","ITEM_NOT_ELIGIBLE_FOR_BUYBACK":"Item is not eligible for buyback","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"ITEM_EXPIRED":{"value":{"error":"Item has expired","code":"ITEM_EXPIRED"}},"BUYBACK_ITEM_NOT_FOUND":{"summary":"Buyback item not found","value":{"error":"Item not found","code":"BUYBACK_ITEM_NOT_FOUND"}},"ITEM_NOT_ELIGIBLE_FOR_BUYBACK":{"value":{"error":"Item is not eligible for buyback","code":"ITEM_NOT_ELIGIBLE_FOR_BUYBACK"}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","PARTNER_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","PARTNER_NOT_FOUND":"Partner not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"PARTNER_NOT_FOUND":{"value":{"error":"Partner not found","code":"PARTNER_NOT_FOUND"}}}}}},"409":{"description":"Conflict: an in-flight request already used this Idempotency-Key, or the quote/items were already claimed by a concurrent request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["DUPLICATE_REQUEST","DUPLICATE_LEDGER_ENTRY","CONFLICT"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"DUPLICATE_REQUEST":"Duplicate request: this Idempotency-Key is already in flight","DUPLICATE_LEDGER_ENTRY":"duplicate ledger reference","CONFLICT":"Generic conflict fallback."}}}},"examples":{"DUPLICATE_REQUEST":{"value":{"error":"Duplicate request: this Idempotency-Key is already in flight","code":"DUPLICATE_REQUEST"}},"DUPLICATE_LEDGER_ENTRY":{"value":{"error":"duplicate ledger reference","code":"DUPLICATE_LEDGER_ENTRY"}},"CONFLICT":{"summary":"Quote already consumed by a concurrent request","value":{"error":"This claim has already been submitted; request a new estimate","code":"CONFLICT"}}}}}},"422":{"description":"Unprocessable: the request was well-formed but could not be fulfilled, or the Idempotency-Key was reused with a different body.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["IDEMPOTENCY_KEY_REUSED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"IDEMPOTENCY_KEY_REUSED":"Idempotency-Key already used with different request parameters"}}}},"examples":{"IDEMPOTENCY_KEY_REUSED":{"summary":"Idempotency-Key replayed with a different body","value":{"error":"Idempotency-Key already used with different request parameters","code":"IDEMPOTENCY_KEY_REUSED"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INVALID_BUYER_ACCOUNT","INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INVALID_BUYER_ACCOUNT":"Buyer account is invalid","INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INVALID_BUYER_ACCOUNT":{"value":{"error":"Buyer account is invalid","code":"INVALID_BUYER_ACCOUNT"}},"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional idempotency key for this write, equivalent to the body's `idempotency_key` (which takes precedence if both are sent). Retrying with the same key and body replays the original response. See the overview's Idempotency section."}]}},"/vm/buyback/batch":{"post":{"operationId":"buybackBatch","summary":"Sell items back in bulk","description":"Sells up to 100 items back in one call — the batch form of `POST /vm/buyback`. Items are processed independently, in request order: each item settles (or fails) on its own, so one ineligible item never blocks the rest, and the response carries one entry per requested item, in the same order. A well-formed batch always returns 200, even when every item fails — read each entry's `success`; a failed entry's `error_code` uses the same vocabulary `POST /vm/buyback` rejects with (`BUYBACK_ITEM_NOT_FOUND`, `ITEM_EXPIRED`, `ITEM_NOT_ELIGIBLE_FOR_BUYBACK`, …). Every settled entry credits your prepaid ledger and emits its own `buyback.settled` webhook, exactly as the single-item call does; the payout terms per item are identical to `POST /vm/buyback` (the item's FMV at purchase × the pack's buyback percent at that same moment, within the 7-day window).\n\nRetrying is always safe: an item that already sold back reports `BUYBACK_ITEM_NOT_FOUND` on the retry — never a second credit. Two idempotency levels are available. The top-level `idempotency_key` (or the `Idempotency-Key` header) replays a **fully successful** batch: when every entry settled, a repeat call under the same key returns the original response without re-executing anything. A response that contains any failed entry is not stored for replay — a retry under the same key runs the batch again, and each item that settled the first time comes back as a `BUYBACK_ITEM_NOT_FOUND` entry rather than a second credit, so treat that code on a retry as \"already sold back\" and reconcile it rather than alarming on it. For reliable per-item reconciliation, give each item its own `idempotency_key`: it is persisted on that item's buyback and becomes a durable lookup key for `GET /vm/buybacks/by-idempotency-key/{idempotency_key}` (any later buyback of a **different** item under the same key fails that entry with `DUPLICATE_REQUEST`, whether or not it overlaps the first in time). Item ids and per-item keys must be unique within one batch.","tags":["Purchases"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","properties":{"item_id":{"type":"string","minLength":1,"description":"Id of the item to sell back."},"idempotency_key":{"type":"string","description":"Optional durable idempotency key for THIS item's buyback, persisted on the credited buyback exactly like the single call's `idempotency_key` — it becomes the key `GET /vm/buybacks/by-idempotency-key/{idempotency_key}` resolves, and any later buyback of a DIFFERENT item under the same key fails this entry with `DUPLICATE_REQUEST`. Unlike the top-level `idempotency_key`, it does not drive whole-response replay. Must be unique within the batch."}},"required":["item_id"]},"description":"The items to sell back — at most 100 per call, each processed independently in this order."},"idempotency_key":{"type":"string","description":"Optional idempotency key for the whole batch. Equivalent to the `Idempotency-Key` header; if both are sent, this body value wins. Replay applies only to a fully successful batch: when every entry settled, a repeat call under the same key returns the original response without re-executing anything (with an `idempotency-replayed: true` header). A response containing any failed entry is NOT stored for replay, so a retry under the same key re-runs the batch — safely: an item that already settled reports `BUYBACK_ITEM_NOT_FOUND` instead of crediting twice. Reusing a key with a different body is rejected `422 IDEMPOTENCY_KEY_REUSED`. For per-item reconciliation, send per-item `idempotency_key`s and resolve them via `GET /vm/buybacks/by-idempotency-key/{idempotency_key}`."}},"required":["items"],"examples":[{"items":[{"item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK"}]}]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"True when EVERY item settled; false when any entry failed."},"succeeded":{"type":"number","description":"How many items settled."},"failed":{"type":"number","description":"How many items failed."},"results":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"item_id":{"type":"string","description":"The item this entry answers for (as sent in `items`)."},"success":{"const":true,"description":"This item settled — the credit has landed."},"buyback_id":{"type":"string","description":"Platform-minted id for THIS buyback — the durable key to `GET /vm/buybacks/{buyback_id}`. An item can be sold back once per round it is bought, so the item id does not identify a buyback; this does."},"amount":{"type":"number","description":"Amount credited for this buyback, in USD."},"buy_session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The `session_id` of the `/vm/buy/init` that bought this item — same semantics as the single call's field."},"wallet_game_code":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Wallet game code the originating buy was debited under, when the purchase came through an embedded player session that carried one. Null for direct API purchases."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The per-item `idempotency_key` sent for this item, or null when none was."}},"required":["item_id","success","buyback_id","amount","buy_session_id","wallet_game_code","idempotency_key"]},{"type":"object","properties":{"item_id":{"type":"string","description":"The item this entry answers for (as sent in `items`)."},"success":{"const":false,"description":"This item was NOT sold back — nothing was credited for it."},"error_code":{"enum":["BUYBACK_ITEM_NOT_FOUND","ITEM_EXPIRED","ITEM_NOT_ELIGIBLE_FOR_BUYBACK","INVALID_BUYER_ACCOUNT","DUPLICATE_REQUEST","DUPLICATE_LEDGER_ENTRY","PARTNER_NOT_FOUND","INTERNAL_SERVER_ERROR"],"type":"string","description":"Why this item was not sold back — the same codes `POST /vm/buyback` rejects with (`BUYBACK_ITEM_NOT_FOUND`, `ITEM_EXPIRED`, `ITEM_NOT_ELIGIBLE_FOR_BUYBACK`, `INVALID_BUYER_ACCOUNT`, `DUPLICATE_REQUEST`, `DUPLICATE_LEDGER_ENTRY`, `PARTNER_NOT_FOUND`), or `INTERNAL_SERVER_ERROR` for an unexpected fault."},"error":{"type":"string","description":"Human-readable failure message."}},"required":["item_id","success","error_code","error"]}],"title":"Batch Buyback Entry","description":"The outcome for one requested item of a batch sellback."},"description":"One entry per requested item, in request order."},"idempotency_key":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The effective BATCH-level idempotency key that applied to this request (the top-level body `idempotency_key` if sent, else the `Idempotency-Key` header), or null if none."}},"required":["success","succeeded","failed","results","idempotency_key"],"title":"Batch Buyback Result","description":"Result of a batch sellback (`POST /vm/buyback/batch`).","examples":[{"success":true,"succeeded":1,"failed":0,"results":[{"item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","success":true,"buyback_id":"0194f0a3-1d4e-7b2f-8c3d-6a9f5e2b7d01","amount":361.39,"buy_session_id":"0194f0a2-7c3b-7a1e-9d2c-5b8e4f1a6c90","wallet_game_code":null,"idempotency_key":null}],"idempotency_key":"buy-rookie-2026-07-14-001"}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"409":{"description":"Conflict: an in-flight request already used this Idempotency-Key, or the quote/items were already claimed by a concurrent request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["DUPLICATE_REQUEST","CONFLICT"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"DUPLICATE_REQUEST":"Duplicate request: this Idempotency-Key is already in flight","CONFLICT":"Generic conflict fallback."}}}},"examples":{"DUPLICATE_REQUEST":{"value":{"error":"Duplicate request: this Idempotency-Key is already in flight","code":"DUPLICATE_REQUEST"}},"CONFLICT":{"summary":"Quote already consumed by a concurrent request","value":{"error":"This claim has already been submitted; request a new estimate","code":"CONFLICT"}}}}}},"422":{"description":"Unprocessable: the request was well-formed but could not be fulfilled, or the Idempotency-Key was reused with a different body.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["IDEMPOTENCY_KEY_REUSED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"IDEMPOTENCY_KEY_REUSED":"Idempotency-Key already used with different request parameters"}}}},"examples":{"IDEMPOTENCY_KEY_REUSED":{"summary":"Idempotency-Key replayed with a different body","value":{"error":"Idempotency-Key already used with different request parameters","code":"IDEMPOTENCY_KEY_REUSED"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional idempotency key for this write, equivalent to the body's `idempotency_key` (which takes precedence if both are sent). Retrying with the same key and body replays the original response. See the overview's Idempotency section."}]}},"/ship/quote":{"post":{"operationId":"shipQuote","summary":"Quote shipping for items","description":"Opens a shipping quote for one end-user's cards: it validates the destination address, confirms every id in `item_ids` is currently held by `user_id`, classifies the cards for fulfillment, and returns the rates available for that shipment. Nothing is booked and no money moves. This is the pricing step, and `POST /ship/request` is the booking step.\n\nThe quote is all-or-nothing across `item_ids`: a single id the user does not hold rejects the whole request rather than quoting the rest. Every holding is shippable, so you do not need to filter on `shipping.eligible` before quoting.\n\nThe response's `session_id` identifies the quote and is what `GET /ship/quote/{session_id}` reads back. What you pass to `POST /ship/request` as `quote_id` is a rate's own id from the `quotes` array, not the `session_id`.\n\n`SHIP_ITEM_NOT_FOUND` when an item in `item_ids` cannot be quoted for that user. Address validation\nis real in sandbox as well as live; it is the one part of the shipping flow the sandbox does not\nsimulate.","tags":["Shipping"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"item_ids":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string"},"description":"Ids of the items to ship."},"destination":{"$ref":"#/components/schemas/ShippingDetails"},"user_id":{"type":"string","minLength":1,"description":"Your partner-defined user id who owns the items to ship."},"idempotency_key":{"type":"string","description":"Optional idempotency key for this write. Equivalent to the `Idempotency-Key` header; if both are sent, this body value wins. A repeat call under a key that already succeeded is never re-executed: it replays the original response (with an `idempotency-replayed: true` header), or under a race fails `409 DUPLICATE_REQUEST`. Reusing a key with a different body is rejected `422 IDEMPOTENCY_KEY_REUSED`; a key whose only prior attempt failed is not blocked (the retry runs fresh, since nothing was charged). See the overview's Idempotency section."}},"required":["item_ids","destination","user_id"],"examples":[{"item_ids":["6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK"],"destination":{"name":"Jamie Collector","line_1":"500 Terry A Francois Blvd","line_2":"Suite 300","city":"San Francisco","region":"CA","postal_code":"94158","country":"US"},"user_id":"user_42"}]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string","description":"Quote session id; pass a chosen rate id to `POST /ship/request`."},"item_ids":{"type":"array","items":{"type":"string"},"description":"Ids of the items this quote is bound to: the exact set `POST /ship/request` ships. Echoed back so a quote read by session id is self-describing (the request takes only a rate id)."},"expires_at":{"type":"string","description":"When this quote expires (ISO 8601, ~15 min).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"quotes":{"type":"array","items":{"$ref":"#/components/schemas/ShipRate"},"description":"Available rates for the destination."}},"required":["session_id","item_ids","expires_at","quotes"],"title":"Shipping Quote","description":"Shipping quote with available rates (`POST /ship/quote`).","examples":[{"session_id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23","item_ids":["6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK"],"expires_at":"2026-07-14T14:35:00.000Z","quotes":[{"id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23_rate_a1b2c3d4-e5f6-4789-abcd-ef1234567890","carrier":"Phygitals","service":"Graded Vault Fulfillment","withdrawal_fees":0,"shipping_cost":20,"total_cost":20,"estimated_delivery":"3-5 business days","notes":"Alt vault domestic shipping: $20 flat fee per order (1 card). All prices in USD.","estimated_days_min":3,"estimated_days_max":5}]}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","SHIP_ITEM_NOT_FOUND","INVALID_ADDRESS","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","SHIP_ITEM_NOT_FOUND":"Item not found","INVALID_ADDRESS":"Invalid destination address","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"SHIP_ITEM_NOT_FOUND":{"summary":"Shipping item not found or not owned","value":{"error":"Item not found","code":"SHIP_ITEM_NOT_FOUND"}},"INVALID_ADDRESS":{"summary":"Destination address failed validation","value":{"error":"Invalid destination address","code":"INVALID_ADDRESS","details":"Street address not found","suggested":{"line1":"123 Market St","city":"San Francisco","state":"CA","postal_code":"94103"}}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"409":{"description":"Conflict: an in-flight request already used this Idempotency-Key, or the quote/items were already claimed by a concurrent request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["DUPLICATE_REQUEST","CONFLICT"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"DUPLICATE_REQUEST":"Duplicate request: this Idempotency-Key is already in flight","CONFLICT":"Generic conflict fallback."}}}},"examples":{"DUPLICATE_REQUEST":{"value":{"error":"Duplicate request: this Idempotency-Key is already in flight","code":"DUPLICATE_REQUEST"}},"CONFLICT":{"summary":"Quote already consumed by a concurrent request","value":{"error":"This claim has already been submitted; request a new estimate","code":"CONFLICT"}}}}}},"422":{"description":"Unprocessable: the request was well-formed but could not be fulfilled, or the Idempotency-Key was reused with a different body.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["COUNTRY_NOT_SUPPORTED","IDEMPOTENCY_KEY_REUSED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"COUNTRY_NOT_SUPPORTED":"Country not supported","IDEMPOTENCY_KEY_REUSED":"Idempotency-Key already used with different request parameters"}}}},"examples":{"COUNTRY_NOT_SUPPORTED":{"value":{"error":"Country not supported","code":"COUNTRY_NOT_SUPPORTED"}},"IDEMPOTENCY_KEY_REUSED":{"summary":"Idempotency-Key replayed with a different body","value":{"error":"Idempotency-Key already used with different request parameters","code":"IDEMPOTENCY_KEY_REUSED"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional idempotency key for this write, equivalent to the body's `idempotency_key` (which takes precedence if both are sent). Retrying with the same key and body replays the original response. See the overview's Idempotency section."}]}},"/ship/quote/{session_id}":{"get":{"operationId":"getShipQuote","summary":"Get a shipping quote","description":"Reads back a quote created by `POST /ship/quote`, returning the identical shape. Use it to re-price a rate the user picked earlier, server-side against the stored quote, instead of trusting a rate id and cost that have been sitting in a client since the quote was made.\n\nThe quote must have been created for the same end-user named by `user_id`, under your partner. A mismatch on either reads as `QUOTE_NOT_FOUND`, exactly like an id that never existed, so a caller cannot probe for another user's quotes. A malformed session id resolves the same way rather than erroring.\n\nExpiry here is a hard cutoff with no grace period: once the quote has lapsed the only remedy is a fresh `POST /ship/quote`. Booking is more forgiving, since `POST /ship/request` applies its own still-priced allowance, so a quote that fails to read back may still book, while one that reads back cleanly is certainly still bookable.","tags":["Shipping"],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","description":"Quote session id from `POST /ship/quote`."}},{"name":"user_id","in":"query","required":true,"schema":{"type":"string","minLength":1,"description":"Your partner-defined user id the quote was requested for (same as `POST /ship/quote`)."},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string","description":"Quote session id; pass a chosen rate id to `POST /ship/request`."},"item_ids":{"type":"array","items":{"type":"string"},"description":"Ids of the items this quote is bound to: the exact set `POST /ship/request` ships. Echoed back so a quote read by session id is self-describing (the request takes only a rate id)."},"expires_at":{"type":"string","description":"When this quote expires (ISO 8601, ~15 min).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"quotes":{"type":"array","items":{"$ref":"#/components/schemas/ShipRate"},"description":"Available rates for the destination."}},"required":["session_id","item_ids","expires_at","quotes"],"title":"Shipping Quote","description":"Shipping quote with available rates (`POST /ship/quote`).","examples":[{"session_id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23","item_ids":["6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK"],"expires_at":"2026-07-14T14:35:00.000Z","quotes":[{"id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23_rate_a1b2c3d4-e5f6-4789-abcd-ef1234567890","carrier":"Phygitals","service":"Graded Vault Fulfillment","withdrawal_fees":0,"shipping_cost":20,"total_cost":20,"estimated_delivery":"3-5 business days","notes":"Alt vault domestic shipping: $20 flat fee per order (1 card). All prices in USD.","estimated_days_min":3,"estimated_days_max":5}]}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","QUOTE_EXPIRED","INVALID_RATE","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","QUOTE_EXPIRED":"Quote has expired; request a new estimate","INVALID_RATE":"Invalid pricing estimate","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"QUOTE_EXPIRED":{"summary":"Quote-lifecycle rejection","value":{"error":"Quote has expired; request a new estimate","code":"QUOTE_EXPIRED"}},"INVALID_RATE":{"value":{"error":"Invalid pricing estimate","code":"INVALID_RATE"}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","QUOTE_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","QUOTE_NOT_FOUND":"Session not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"QUOTE_NOT_FOUND":{"value":{"error":"Session not found","code":"QUOTE_NOT_FOUND"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/ship/request":{"post":{"operationId":"shipRequest","summary":"Request a shipping order","description":"Books one of the rates a quote offered and opens the shipping order. `quote_id` is a rate id from the `quotes` array `POST /ship/quote` returned, and `user_id` must be the end-user the quote was created for. The call consumes the quote, claims the items, debits your prepaid partner ledger for the chosen rate's `total_cost` only (item FMV is not charged), and records the order, all before it answers, so a success response means the shipment is durably booked and paid for.\n\nThe returned `order_id` is the same value as the quote's `session_id`, so the order is addressable by `GET /ship/order/{order_id}` immediately. Booking also emits a `shipping.queued` webhook. The claimed cards leave the user's holdings permanently: they stop appearing in `GET /inventory/{user_id}` and can no longer be sold back.\n\nA quote backs exactly one booking, and once consumed it cannot be re-booked, so every quote-lifecycle rejection means the same thing operationally: request a new quote. `CONCURRENT_SHIP_CONFLICT` is the one to watch, because a concurrent request took one or more of the items first and this quote was already consumed by the time the conflict surfaced; the retry is a fresh quote rather than a repeat of this call.\n\n`INSUFFICIENT_BALANCE` means the debit would take your prepaid ledger below its floor. Nothing is charged and the booking unwinds itself (the items are released back to the user and the quote is un-consumed), so the same `quote_id` can be retried after a top-up, provided it has not expired in the meantime. No carrier label is bought at this point; the vault vendor handles fulfillment from here, and progress is reported through `GET /ship/order/{order_id}`.","tags":["Shipping"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"quote_id":{"type":"string","minLength":1,"description":"A rate/quote id from the `quotes` array returned by `/ship/quote`."},"user_id":{"type":"string","minLength":1,"description":"Your partner-defined user id the quote was requested for (same as `/ship/quote`)."},"idempotency_key":{"type":"string","description":"Optional idempotency key for this write. Equivalent to the `Idempotency-Key` header; if both are sent, this body value wins. A repeat call under a key that already succeeded is never re-executed: it replays the original response (with an `idempotency-replayed: true` header), or under a race fails `409 DUPLICATE_REQUEST`. Reusing a key with a different body is rejected `422 IDEMPOTENCY_KEY_REUSED`; a key whose only prior attempt failed is not blocked (the retry runs fresh, since nothing was charged). See the overview's Idempotency section."}},"required":["quote_id","user_id"],"examples":[{"quote_id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23_rate_a1b2c3d4-e5f6-4789-abcd-ef1234567890","user_id":"user_42"}]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"order_id":{"type":"string","description":"Newly-created shipping order id."},"status":{"const":"success","description":"Always \"success\" when the order was created."}},"required":["order_id","status"],"title":"Shipping Request Result","description":"Result of booking a rate (`POST /ship/request`).","examples":[{"order_id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23","status":"success"}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","ITEM_ALREADY_CLAIMED","INVALID_QUOTE","QUOTE_UNBOUND","QUOTE_EXPIRED","QUOTE_EMPTY","INVALID_RATE","SHIP_ITEM_NOT_FOUND","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","ITEM_ALREADY_CLAIMED":"Item already claimed","INVALID_QUOTE":"quote_id is required","QUOTE_UNBOUND":"Quote is missing its bound items or address; request a new estimate","QUOTE_EXPIRED":"Quote has expired; request a new estimate","QUOTE_EMPTY":"Quote binds no items; request a new estimate","INVALID_RATE":"Invalid pricing estimate","SHIP_ITEM_NOT_FOUND":"Item not found","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"ITEM_ALREADY_CLAIMED":{"value":{"error":"Item already claimed","code":"ITEM_ALREADY_CLAIMED"}},"INVALID_QUOTE":{"summary":"Missing/malformed quote reference","value":{"error":"quote_id is required","code":"INVALID_QUOTE"}},"QUOTE_UNBOUND":{"value":{"error":"Quote is missing its bound items or address; request a new estimate","code":"QUOTE_UNBOUND"}},"QUOTE_EXPIRED":{"summary":"Quote-lifecycle rejection","value":{"error":"Quote has expired; request a new estimate","code":"QUOTE_EXPIRED"}},"QUOTE_EMPTY":{"value":{"error":"Quote binds no items; request a new estimate","code":"QUOTE_EMPTY"}},"INVALID_RATE":{"value":{"error":"Invalid pricing estimate","code":"INVALID_RATE"}},"SHIP_ITEM_NOT_FOUND":{"summary":"Shipping item not found or not owned","value":{"error":"Item not found","code":"SHIP_ITEM_NOT_FOUND"}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"402":{"description":"Payment required: the platform's prepaid balance is too low.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INSUFFICIENT_BALANCE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INSUFFICIENT_BALANCE":"Insufficient prepaid balance"}}}},"examples":{"INSUFFICIENT_BALANCE":{"value":{"error":"Insufficient prepaid balance","code":"INSUFFICIENT_BALANCE"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","QUOTE_NOT_FOUND","PARTNER_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","QUOTE_NOT_FOUND":"Session not found","PARTNER_NOT_FOUND":"Partner not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"QUOTE_NOT_FOUND":{"value":{"error":"Session not found","code":"QUOTE_NOT_FOUND"}},"PARTNER_NOT_FOUND":{"value":{"error":"Partner not found","code":"PARTNER_NOT_FOUND"}}}}}},"409":{"description":"Conflict: an in-flight request already used this Idempotency-Key, or the quote/items were already claimed by a concurrent request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["DUPLICATE_REQUEST","CONCURRENT_SHIP_CONFLICT","DUPLICATE_LEDGER_ENTRY","CONFLICT"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"DUPLICATE_REQUEST":"Duplicate request: this Idempotency-Key is already in flight","CONCURRENT_SHIP_CONFLICT":"One or more items are already being shipped; request a new quote","DUPLICATE_LEDGER_ENTRY":"duplicate ledger reference","CONFLICT":"Generic conflict fallback."}}}},"examples":{"DUPLICATE_REQUEST":{"value":{"error":"Duplicate request: this Idempotency-Key is already in flight","code":"DUPLICATE_REQUEST"}},"CONCURRENT_SHIP_CONFLICT":{"summary":"Items already being shipped by a concurrent request","value":{"error":"One or more items are already being shipped; request a new quote","code":"CONCURRENT_SHIP_CONFLICT"}},"DUPLICATE_LEDGER_ENTRY":{"value":{"error":"duplicate ledger reference","code":"DUPLICATE_LEDGER_ENTRY"}},"CONFLICT":{"summary":"Quote already consumed by a concurrent request","value":{"error":"This claim has already been submitted; request a new estimate","code":"CONFLICT"}}}}}},"422":{"description":"Unprocessable: the request was well-formed but could not be fulfilled, or the Idempotency-Key was reused with a different body.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["IDEMPOTENCY_KEY_REUSED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"IDEMPOTENCY_KEY_REUSED":"Idempotency-Key already used with different request parameters"}}}},"examples":{"IDEMPOTENCY_KEY_REUSED":{"summary":"Idempotency-Key replayed with a different body","value":{"error":"Idempotency-Key already used with different request parameters","code":"IDEMPOTENCY_KEY_REUSED"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional idempotency key for this write, equivalent to the body's `idempotency_key` (which takes precedence if both are sent). Retrying with the same key and body replays the original response. See the overview's Idempotency section."}]}},"/ship/order/{order_id}":{"get":{"operationId":"getShipOrder","summary":"Get a shipping order","description":"Returns the current state of one shipping order. `order_id` is the UUID `POST /ship/request` returned, which is the same value as the `session_id` of the quote that was booked.\n\nThe optional `user_id` narrows the read to the end-user the order was booked for. Supply it when\nyou are serving a request on that user's behalf so an order that does not belong to that user\nreturns `ORDER_NOT_FOUND`. Omit it and any order under your partner resolves.\n\nDrive fulfillment UI from `status` and the timestamps. `tracking_number` fills in once the vault vendor cuts a label, and is the same value the `shipping.*` webhooks report for the order. `tracking_url` is always null on this flow, because partner shipments are fulfilled by the vault vendor without a carrier label bought through this API, so there is no carrier tracking page to link to. (The `tracking_url` on a `shipping.*` webhook payload is a different thing: a link to the order on your partner dashboard, for your operators.)\n\n`cancelled` and `failed` (with `error_message`) are uncommon, and typically mean an invalid address, a carrier exception, or customs. They are resolved through customer support; there is no API that credits the partner ledger or reverses the pack purchase. The matching `shipping.cancelled` / `shipping.failed` webhooks are notifications only.","tags":["Shipping"],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"Shipping order id (the `order_id` UUID from `/ship/request`).","examples":["0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23"]}},{"name":"user_id","in":"query","schema":{"type":"string","minLength":1,"description":"Optional: your partner-defined user id the order was booked for. When sent, an order booked for a different user reads as not found."},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"order_id":{"type":"string","description":"Shipping order id."},"status":{"enum":["processing","transit","tracking_available","delivered","cancelled","failed"],"type":"string","description":"Current fulfillment status. `cancelled` and `failed` are uncommon (invalid address, carrier exception, customs) and are resolved through customer support; they do not credit the partner ledger."},"carrier":{"type":"string","description":"Booked carrier."},"service":{"type":"string","description":"Booked service level."},"tracking_number":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Shipment tracking number once the label is cut, else null."},"tracking_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Always null; there is no carrier integration on this flow, so there is no tracking URL to give."},"amount":{"type":"number","description":"Charged shipping cost."},"currency":{"type":"string","description":"Currency of `amount` (e.g. USD)."},"destination":{"$ref":"#/components/schemas/ShippingDetails"},"items":{"type":"array","items":{"type":"object","properties":{"item_id":{"type":"string","description":"Shipped item id."},"name":{"type":"string","description":"Item display name."},"image":{"type":"string","description":"Item front image URL (empty when unresolved)."},"back_image":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Item back image URL, or null."}},"required":["item_id","name","image","back_image"]},"description":"Items included in this shipment."},"created_at":{"type":"string","description":"When the order was created (ISO 8601).","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},"shipped_at":{"anyOf":[{"type":"string","description":"An ISO 8601 instant string with a required UTC offset (e.g. 2023-01-15T13:45:30Z)","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},{"type":"null"}],"description":"When the order shipped (ISO 8601), or null."},"delivered_at":{"anyOf":[{"type":"string","description":"An ISO 8601 instant string with a required UTC offset (e.g. 2023-01-15T13:45:30Z)","format":"date-time","pattern":"^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)(\\[.+\\])?$"},{"type":"null"}],"description":"When the order was delivered (ISO 8601), or null."},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Failure reason when `status` is \"failed\", else null. Support-handled; not an automatic refund."}},"required":["order_id","status","carrier","service","tracking_number","tracking_url","amount","currency","destination","items","created_at","shipped_at","delivered_at","error_message"],"title":"Shipping Order","description":"A shipping order and its fulfillment state (`GET /ship/order/{order_id}`).","examples":[{"order_id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23","status":"tracking_available","carrier":"UPS","service":"Ground","tracking_number":"1Z999AA10123456784","tracking_url":null,"amount":20,"currency":"USD","destination":{"name":"Jamie Collector","line_1":"500 Terry A Francois Blvd","line_2":"Suite 300","city":"San Francisco","region":"CA","postal_code":"94158","country":"US"},"items":[{"item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null}],"created_at":"2026-07-14T14:20:00.000Z","shipped_at":"2026-07-14T18:45:00.000Z","delivered_at":null,"error_message":null}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"404":{"description":"Not found: the addressed resource does not exist.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","ORDER_NOT_FOUND"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"NOT_FOUND":"Generic not-found fallback.","ORDER_NOT_FOUND":"Order not found"}}}},"examples":{"NOT_FOUND":{"summary":"Unknown resource","value":{"error":"Not found","code":"NOT_FOUND"}},"ORDER_NOT_FOUND":{"value":{"error":"Order not found","code":"ORDER_NOT_FOUND"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}},"/ship/orders":{"get":{"operationId":"listShipOrders","summary":"List a user's shipping orders","description":"Returns every shipping order booked for the end-user named by `user_id`, newest first, in the same per-order shape `GET /ship/order/{order_id}` returns. Orders are scoped to your partner as well as to that user, so this never surfaces another partner's shipments.\n\nThere is no cursor: the response is a bare array capped at **1000** orders, the user's whole shipping history within that limit. Quote sessions that were never booked do not appear; an order exists here only once `POST /ship/request` has succeeded.","tags":["Shipping"],"parameters":[{"name":"user_id","in":"query","required":true,"schema":{"type":"string","minLength":1,"description":"Your partner-defined user id whose orders to list."},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"orders":{"type":"array","items":{"$ref":"#/components/schemas/ShipOrder"},"description":"The user's shipping orders, newest first."}},"required":["orders"],"title":"Shipping Orders","description":"A user's shipping orders (`GET /ship/orders`).","examples":[{"orders":[{"order_id":"0194f0a5-3f60-7d4b-ae5f-8c1b7a4d9f23","status":"tracking_available","carrier":"UPS","service":"Ground","tracking_number":"1Z999AA10123456784","tracking_url":null,"amount":20,"currency":"USD","destination":{"name":"Jamie Collector","line_1":"500 Terry A Francois Blvd","line_2":"Suite 300","city":"San Francisco","region":"CA","postal_code":"94158","country":"US"},"items":[{"item_id":"6miVCvzYMeZbfxYA1g6aAs74wWpAfwq8dswiCrS1aVLK","name":"1999 Pokemon Base Set Pokemon Center #85 PSA 10","image":"https://img.phygitals.com/cdn-cgi/image/width=1024,quality=85,format=auto,fit=scale-down/https%3A%2F%2Fimg.phygitals.com%2F6Ubfh7Nt7T9LvYqj1hQ1iHwSAVK5GvJUW2BLRieJC8bq-cropped","back_image":null}],"created_at":"2026-07-14T14:20:00.000Z","shipped_at":"2026-07-14T18:45:00.000Z","delivered_at":null,"error_message":null}]}]}}}},"400":{"description":"Bad request: input validation failed, or the operation was rejected.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["VALIDATION_ERROR","BAD_REQUEST"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"VALIDATION_ERROR":"Invalid request","BAD_REQUEST":"Generic rejection that maps to no specific code (fallback)."}},"details":{"description":"Extra failure detail. For `VALIDATION_ERROR`, the list of field-level issues; for `INVALID_ADDRESS`, a human-readable reason string.","oneOf":[{"type":"array","items":{"type":"object","required":["field","message"],"additionalProperties":false,"properties":{"field":{"type":"string","description":"The offending request field (dotted path)."},"message":{"type":"string","description":"What was wrong with the field."}}}},{"type":"string"}]},"suggested":{"type":"object","description":"A corrected address suggestion, when the address validator can offer one (`INVALID_ADDRESS` only)."}}},"examples":{"VALIDATION_ERROR":{"summary":"Input validation failed","value":{"error":"Amount must be a whole number","code":"VALIDATION_ERROR","details":[{"field":"amount","message":"Amount must be a whole number"}]}},"BAD_REQUEST":{"summary":"Generic rejection (fallback)","value":{"error":"Bad request","code":"BAD_REQUEST"}}}}}},"401":{"description":"Unauthenticated: no valid credential accompanied the request.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"UNAUTHORIZED":"Invalid API key or unauthorized user"}}}},"examples":{"UNAUTHORIZED":{"summary":"Missing/invalid API key or bearer token","value":{"error":"Invalid API key or unauthorized user","code":"UNAUTHORIZED"}}}}}},"403":{"description":"Forbidden: the credential is not permitted to use this route.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["SANDBOX_KEY_ON_LIVE_ROUTE"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"SANDBOX_KEY_ON_LIVE_ROUTE":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1."}}}},"examples":{"SANDBOX_KEY_ON_LIVE_ROUTE":{"summary":"Sandbox key used on the live API","value":{"error":"This credential is sandbox-scoped and cannot be used on the live partner API (/api/partner/v1). Use a live API key, or call /api/partner/sandbox/v1.","code":"SANDBOX_KEY_ON_LIVE_ROUTE"}}}}}},"500":{"description":"Internal server error: a server-side operation failed unexpectedly.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"additionalProperties":false,"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","enum":["INTERNAL_SERVER_ERROR"],"description":"Machine-readable error code. Branch on this, not the message text. Each value's default `error` message is listed below (some raisers substitute a more specific message).","x-enum-descriptions":{"INTERNAL_SERVER_ERROR":"Unexpected server error (fallback)."}}}},"examples":{"INTERNAL_SERVER_ERROR":{"summary":"Unexpected server error (fallback)","value":{"error":"Internal server error","code":"INTERNAL_SERVER_ERROR"}}}}}}}}}}}