Phygitals Partner API
The Phygitals Partner API lets you embed Phygitals' pack, buyback, and physical-shipping flows into your own platform: browse and buy packs, look up the items inside them, sell items back to the pool, and quote, request, and track physical shipments. Everything is REST over HTTPS with JSON request and response bodies. The operations are grouped in the sidebar — Storefront (read + buy) and Events (outbound webhooks).
Platform
The infrastructure for digital collectibles. Sell digital packs backed by real, graded physical
cards. An end user buys a pack, watches it reveal instantly, holds the card in an insured vault, and
then ships it or sells it back whenever they want — all through this one API. These are the same
rails the main Phygitals storefront runs on: $350M+ GMV, 2.8M+ transactions, 100K+ users,
and a pack-configured buyback (often ~85–90% of FMV — see each pack's buyback_percent).
The four-step flow
-
Buy a pack — debited from your prepaid partner ledger (not end-user card/crypto on this
API). Each pack is backed by real physical cards. Start with
POST /vm/buy/init; look it up withPOST /vm/buy/status(or thepurchase.settledwebhook). -
Instant reveal — the picked cards are known the moment the pack opens.
buy/initreturns them synchronously (there is nopendingstate to poll for). -
Securely vaulted — cards sit in top-tier insured US facilities; the user owns the digital
representation until they act on it. Holdings are surfaced by
GET /inventory/{user_id}. -
Ship or sell — redeem for worldwide physical shipping (
POST /ship/quote→POST /ship/request→GET /ship/order/{order_id}), or sell back at the pack'sbuyback_percentof FMV viaPOST /vm/buyback.
Vault, pricing & logistics
Every digital collectible is 1:1 backed by a real, graded card in an insured US vault. Phygitals operates the full physical stack end-to-end — you don't store or fulfill anything yourself.
| Vault partners | Alt (primary), PSA, Fanatics |
| Pricing | Alt's live FMV feed powers buyback_price on every item and every sellback |
| Insurance | Full coverage across all stored items, climate-controlled facilities |
| Fulfillment | Worldwide shipping, tracking and insurance included on every redemption |
Phygitals funds the buybacks. Sellback liquidity is underwritten by Phygitals, not the partner.
When a user calls POST /vm/buyback, the credit hits your partner ledger — you don't post capital,
run a secondary market, or carry sellback risk.
White-label. Phygitals is both a direct-to-consumer platform and infrastructure for partner brands. A launch can be API-only (you keep your own frontend and integrate these endpoints) or a fully managed build (Phygitals designs the branded storefront and backend on the same rails). Every partner gets a dedicated point of contact. Partnership inquiries: hello@phygitals.com.
Concepts
-
Pack — a themed, purchasable bundle (the id the
claw_idsfilter references). Buying one draws items from it provably-fair and returns them. - Item — an individual card / NFT pulled from a pack. You can look one up, ship it, or sell it back to the pool.
-
Chase card — one of a pack's headline high-value cards, its "top hits": the marquee,
long-odds pulls a pack can yield (a numbered rookie autograph, a low-population parallel, and the
like).
GET /vm/chase/{slug}returns them, so you can render a "what could I pull?" preview before a buyer purchases. They're the best-case outcomes in the pack's pool, not a guarantee that any given buy lands one. (GET /vm/availablealways returnschase: []— use the chase endpoint for top hits.) -
Buyback — selling an item back to the pool at the pack's
buyback_percentof FMV for instant liquidity, viaPOST /vm/buyback. Phygitals funds it; the partner posts no capital. - Vault — the insured US facility (Alt / PSA / Fanatics) holding the physical card behind each item until the user ships or sells it.
- Partner ledger — prepaid balance Phygitals debits on buy/ship and credits on buyback. Live partner settlement is ledger-based, not an on-chain payment from the end user.
-
Seed commitment — a server seed we bind ourselves to before a draw, via
POST /vm/buy/commit, which returns its hash and a single-usecommitment_idto spend on the buy. It turns a purchase's fairness proof from a record of the draw into a pre-commitment — see Provable fairness.
Getting an API key
Access is provisioned per partner: you need a Partner account on Phygitals before you can call the API. Once your organization is set up, an owner of your partner mints keys from the partner dashboard (the "Docs & setup" panel):
- Sandbox keys (
pk_sandbox_…) — a partner owner can create these self-serve. Start here. -
Live keys (
pk_live_…) — issued by Phygitals only. A partner owner cannot self-issue one; contact your Phygitals partner manager (or support) to enable production access.
The secret is shown exactly once, at creation — store it somewhere safe. If you lose it, revoke the key and mint a new one. Don't have a partner account yet? Talk to your Phygitals contact — or email hello@phygitals.com — to get onboarded.
Authentication
Every request must be authenticated. Two credentials are accepted, and either one satisfies a request:
-
X-API-Key: <your-key>— your partner API key. This is the primary credential for server-to-server integrations, and the only way to reach live data. -
Authorization: Bearer <token>— a signed-in partner-dashboard session token for a user belonging to your partner. Session auth is sandbox-only; live access requires apk_live_…API key.
curl https://api.phygitals.com/api/v1/vm/available \
-H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
Base URLs & environments
Production is https://api.phygitals.com/api/v1 — that's the base URL to build against, and
the one every example here uses. The raw OpenAPI document behind this reference is served on it
too, at https://api.phygitals.com/api/v1/spec.json.
The same operations are mounted on four prefixes; pick the one for the environment you want.
| Environment | Base URL | Settlement |
|---|---|---|
| Production (live) | https://api.phygitals.com/api/v1 |
Prepaid partner ledger + real vaulted inventory |
| Production (long form) | https://api.phygitals.com/api/partner/v1 |
Same as production |
| Sandbox | https://api.phygitals.com/api/partner/sandbox/v1 |
Fully simulated (no ledger / vault / chain) |
| Legacy | https://api.phygitals.com/_/api/v1 |
Follows the key's mode |
Sandbox runs the full API against a simulated backend — buys, buybacks, and settlement are emulated and nothing touches the vault or chain — so you can build and test end-to-end for free. The sandbox base URL forces sandbox behavior for any credential, so even a live key is safely downgraded there.
The production base URL requires a live key: a sandbox-scoped credential (a pk_sandbox_…
key, or a dashboard session) is rejected with 403 rather than being accepted against live data.
https://api.phygitals.com/api/partner/v1 is a long-form alias of
https://api.phygitals.com/api/v1, kept for integrations that already call it; prefer the short
form. The legacy https://api.phygitals.com/_/api/v1 prefix also keeps working and follows the
key's own mode, but new integrations should use https://api.phygitals.com/api/v1 (or
https://api.phygitals.com/api/partner/sandbox/v1 to start in sandbox).
Conventions
- Field names are snake_case in every request and response body.
-
Money types vary by field: pack listing prices / admin pack rows use decimal strings
(e.g.
"25"); itemfmv/buyback_price, buy/buybackamount, and ship rate costs are JSON numbers. Never invent floats where the schema uses a string. Timestamps are ISO-8601 / RFC-3339 UTC strings (e.g."2026-01-15T09:30:00Z"). -
Idempotency is supported on every write bar one (
POST /vm/buy/commit) — see the dedicated section below.
Idempotency
Every mutating request (buy, buyback, create-pack, …) can carry an idempotency key so a dropped connection can't double-execute. Send one on every write.
-
Two equivalent channels. Supply the key as an
Idempotency-Key: <token>HTTP header or as anidempotency_keyfield in the JSON body — they are interchangeable. If you send both, the body field wins and the header is ignored. -
Replay. Retrying with the same key and the same request body replays the original response
verbatim, carrying an
idempotency-replayed: trueresponse header, instead of executing again. Stored responses stay replayable for 24 hours. - Reuse conflict. Reusing a key with a different request body is rejected
422 IDEMPOTENCY_KEY_REUSED— a sign a key was recycled by mistake. -
In-flight duplicate. The same key retried before the first call has returned is rejected
409 DUPLICATE_REQUEST; wait for the original to finish (or poll the status endpoint) rather than retrying immediately. -
Commit-reveal buys retry identically. Passing a
commitment_iddoes not change any of the above: a retry under a key that already bought is still409 DUPLICATE_REQUEST, even though the commitment it names was spent by that same purchase. You never have to special-case the commit-reveal path in your retry logic.409 BUY_COMMITMENT_ALREADY_USEDmeans something different — that commitment was already consumed by another attempt, yours under another key or whoever else held the id — and is covered under Provable fairness. What DOES change with acommitment_id: retrying a genuinely failed buy needs a fresh one. -
Failed attempts don't stick. A key whose only prior attempt failed is not blocked: the retry
executes fresh, since nothing was charged. A
commitment_idis the one thing a failed attempt does consume — retry with a fresh one (see Provable fairness). -
Looking up a past attempt.
POST /vm/buy/statusandPOST /vm/buyback/statusaccept the sameidempotency_keyyou sent on the original write to resolve its durable outcome. There the key is a lookup key, not a dedup token, so passing it never trips the reuse conflict above. -
One exempt write:
POST /vm/buy/commit. It ignores the key entirely — none of the above applies, and a retry after a dropped connection really does mint a second commitment (harmless: an unspent commitment reserves nothing and costs nothing; just use thecommitment_idfrom the reply you did receive). It is exempt so that ONE key can cover a whole purchase — the commit and thePOST /vm/buy/initthat spends it — instead of the buy being rejected422 IDEMPOTENCY_KEY_REUSEDfor carrying the same key as the commit. Send your purchase key on the buy; the commit doesn't need one.
Provable fairness
Every live pull is drawn from two seeds — a server seed we hold and a client seed you can supply — and both are published once the buy has settled, so you or your end-user can re-derive the outcome instead of taking our word for it. The endpoints are grouped under the Fairness tag.
There are two modes, and they differ only in when we bind ourselves to the server seed:
-
Record of the draw (the default) — omit
commitment_idand the server seed is minted at draw time. The proof shows exactly what ran and is internally consistent, but the seed and its hash were recorded together, so it cannot show we were committed to that seed beforehand. -
Commit-reveal — call
POST /vm/buy/commitfirst and we hand yousha256(server_seed)before any draw runs, revealing the seed only once the buy it backs has settled. That the revealed seed hashes to a value you were already holding is what rules out a seed chosen to suit the outcome.
Send your own client_seed on the buy in either mode. Without one the platform picks both halves
of the draw input, and a commitment then proves only that the hash predated the draw — not that the
input was outside our control.
Commit → buy → verify
1. Commit. POST /vm/buy/commit takes no request body and returns commitment_id and
server_seed_hash. Show that hash to your end-user, or record it, now — its existing before the
buy is the whole point. Commitments are single-use, never expire with a live key, and ignore Idempotency-Key (see
Idempotency), so one key can cover both this call and the buy that spends it.
2. Buy. POST /vm/buy/init with that commitment_id and your own client_seed. The draw runs on
the seed you already hold the hash of. The buy mints its own session_id and returns it — the
commitment id is never reused as a session id.
One commitment, one attempt. The buy consumes the commitment_id before it draws, so it is spent
whether or not it settles: if the buy fails — the pack sold out, your balance was short — that
commitment is gone and a retry gets 409 BUY_COMMITMENT_ALREADY_USED. Mint a fresh one and buy
again; POST /vm/buy/commit is free and takes no body, so a replacement costs you nothing. Guards
that don't depend on the seed run first, so ordinary integration mistakes — an unknown or disabled
pack, an amount over max_per_mint — reject before the commitment is touched and cost you
nothing.
3. Verify. GET /vm/sessions/{session_id}/fairness-proof reveals server_seed. Three checks make
it a commit-reveal rather than a record after the fact:
-
sha256(server_seed)reproduces the proof'sserver_seed_hash— and theserver_seed_hashstep 1 handed you. Comparing against your own stored copy is what the whole flow rests on; comparing only within the response proves nothing beyond internal consistency. -
committed_atmatches whenPOST /vm/buy/commitanswered you. It is when we recorded the seed, which on a committed buy is that call — but every draw records one, so the field's presence alone proves nothing. Your own stored hash is the evidence; this is a cross-check against it, not a substitute. client_seedis the value you sent (when you sent none, it is thesession_idwe minted).
# 1. commit — returns { commitment_id, server_seed_hash }
curl -X POST https://api.phygitals.com/api/v1/vm/buy/commit \
-H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
# 2. buy with it — returns { session_id, nfts }
curl -X POST https://api.phygitals.com/api/v1/vm/buy/init \
-H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"id":"<pack_id>","amount":1,"user_id":"<your_user>",
"commitment_id":"<commitment_id>","client_seed":"<your_seed>"}'
# 3. reveal — sha256(server_seed) must equal the hash from step 1
curl https://api.phygitals.com/api/v1/vm/sessions/<session_id>/fairness-proof \
-H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
Then re-derive the picks themselves. Each selections entry records the values both steps of a draw
generated — tierRandom for the rarity tier, then rowSeed and itemRandom for the item within it.
GET /vm/sessions/{session_id}/fairness-proof documents the exact derivation, including the one step
that is not independently reproducible (the item-within-tier pick, which would require the pool of
eligible items as it stood at draw time). GET /vm/fairness-proofs pages through every proof you have
recorded, newest first, if you would rather reconcile in bulk than per session.
Commitment errors
-
404 BUY_COMMITMENT_NOT_FOUND— unknown or invalidcommitment_id. Treat each id as a secret and mint a fresh one if you no longer have a usable copy. -
409 BUY_COMMITMENT_ALREADY_USED— that commitment has already been consumed by aPOST /vm/buy/init, and one commitment backs one attempt. Mint a fresh one withPOST /vm/buy/commit; retrying this one cannot succeed. The consuming attempt need not have succeeded — see "One commitment, one attempt" above. Note this is not what a plain retry of your own settled buy returns: a repeat under anIdempotency-Keythat already bought is409 DUPLICATE_REQUESTexactly as it would be without a commitment (see Idempotency).
Sandbox simulates draws rather than running the fairness protocol, so it records no proof — see Sandbox.
Sandbox
The sandbox runs the full API against a simulation, so you can integrate and test the whole buy →
inventory → sellback → shipping flow without consuming inventory, moving money, or shipping real
cards. Point at /api/partner/sandbox/v1 (or use a pk_sandbox_… key). It mirrors the live
surface — same routes, same request/response shapes, same error codes — but a few behaviors
differ, and you should know them before you rely on them:
- State is ephemeral. Sandbox sessions, simulated inventory, buybacks, and shipping orders do not persist across restarts or deploys. Treat every sandbox run as fresh.
-
No real fulfillment. Nothing touches the vault (Alt / PSA / Fanatics), the chain, a payment
processor, or a shipping carrier — buys, buybacks, settlement, and shipping rates are all
simulated. (Destination address validation is the one exception:
POST /ship/quoteruns the same real address check that live does.) - Not a concurrency test bed. Sandbox does not faithfully reproduce live contention when two buyers race for the same card — use live keys when you need to validate that behavior.
-
Idempotency replay works; some live-only duplicate guards do not. Retrying with the same
Idempotency-Keyand body still replays the first response. Hard rejection of some concurrent same-key duplicates is live-only — test that case with a live key. -
Seed commitments are one-shot in both modes. A
commitment_idis consumed by the firstPOST /vm/buy/initthat names it — whether or not that buy succeeds — in sandbox exactly as in live, so a retry after a simulated stock-out gets409 BUY_COMMITMENT_ALREADY_USEDin both. -
Sandbox commitments can expire; live ones do not. An outstanding sandbox
commitment_idmay later return404 BUY_COMMITMENT_NOT_FOUND— mint a fresh one rather than treating it as a bug. Live commitments stay spendable until used. -
No provably-fair draw. Sandbox pulls are simulated rather than drawn against the fairness
protocol: a
client_seedyou send toPOST /vm/buy/initis accepted and then ignored, no fairness proof is recorded, and the fairness endpoints return nothing for a sandbox session.POST /vm/buy/commitstill works, so you can wire the commit → buy call sequence end-to-end, but there is no seed to reveal at the end of it. Verify the proof recipe itself against a live key — see Provable fairness. -
Sellback pricing is frozen at buy time — in both modes. A sandbox
POST /vm/buybackpays thebuyback_pricefixed on the item when it was bought, and live does the same: it pays the FMV and the pack's buyback percent as they stood at purchase, not today's. What differs is only that sandbox values come out of the simulation, so don't assert on exact sandbox payouts. -
Shipping orders don't progress. A sandbox
POST /ship/requestcreates an order and leaves it there —shipped_atanddelivered_atstay null for the life of the process, since no carrier actually moves the card. (tracking_number/tracking_urlare not a sandbox difference: they are never populated in either mode — seeGET /ship/order/{order_id}.) -
Scoped by
user_idonly. Sandbox state is isolated by theuser_idyou submit; a live key is additionally partner-scoped. Webhooks fire in both modes — register a sandbox endpoint to receive sandbox events (a sandbox event never reaches a live URL), so you can test your receiver for free.
Errors
Errors use one consistent JSON shape and a conventional HTTP status:
{ "error": "Claw machine is out of stock", "code": "OUT_OF_STOCK" }
error is a human-readable message; code is a stable, machine-readable token — branch on
code, not the message text. Input-validation failures use code: "VALIDATION_ERROR" and add
a details array listing every field problem at once:
{
"error": "Amount must be a whole number",
"code": "VALIDATION_ERROR",
"details": [{ "field": "amount", "message": "Amount must be a whole number" }]
}
Statuses follow the usual conventions: 400 bad request, 401 unauthenticated, 402
insufficient prepaid balance, 403 wrong environment for the credential, 404 not found, 409
idempotency conflict, 422 unprocessable. Each operation below documents the exact codes it can
return.
Webhooks
Rather than poll, subscribe to outbound events — shipping lifecycle, purchase settlement/failure,
and buyback settlement — by registering a URL in your partner dashboard. Phygitals then POSTs
each event to your endpoint. Deliveries are HMAC-signed (X-Phygitals-Signature) and sent
at-least-once with retries, so verify the signature and dedupe on the envelope's id (stable across
redeliveries — not data.idempotency_key, which is null when the originating request sent no key).
Register or update your endpoint under your partner dashboard → Webhooks
(/partners/<your-slug>/webhooks, owner/editor only). The full payloads, and the registration
walkthrough, are in the Webhooks section below.
Next steps
- Mint a sandbox key from your partner dashboard.
- Call
GET /api/partner/sandbox/v1/vm/availableto list packs. - Walk a full buy → item → shipment flow in sandbox.
- Register a webhook and confirm you receive events.
- Ask Phygitals to enable a live key when you're ready for production.
For long-form integration guides, see the linked Partner integration guides.