v1.0.0
OpenAPI 3.1.1

Phygitals Public API

The Phygitals Public API gives account holders programmatic access with user-scoped API keys: browse packs and marketplace filter facets, buy packs with crypto, and sell claw items back to the pool. All requests target https://api.phygitals.com.

Documented request and response shapes match what the API serves.

Authentication

API keys are user-scoped: create one in the Phygitals web app under Settings → API keys (enable API access first; a connected Solana wallet is required). Keys start with phy_ and are shown in full exactly once at creation. A lost key cannot be recovered, so revoke it and create a new one. Treat keys like passwords: keep them in an environment variable or secrets manager, never hardcoded or shipped client-side.

Authenticated endpoints accept the key in either header:

  • X-API-Key: phy_your_secret_key
  • Authorization: Bearer phy_your_secret_key

Keys carry scopes: vm.buy.crypto (pack purchase endpoints) and marketplace.take-claw-bid (claw buyback endpoints); new keys receive both by default. A request with a missing, invalid, or revoked key returns 401, as does one whose key lacks the endpoint's scope. Authenticated endpoints are rate-limited; exceeding a limit returns 429.

The catalog reads in this reference (the pack listing, the chase-card previews, and the marketplace filter facets) require no credential. The pack-buy settle and its status poll require a key holding the vm.buy.crypto scope; the buyback quote and the two claw-buyback endpoints require a key holding marketplace.take-claw-bid. On buyback init, pass owner_wallet when opening a sellback for a holder who will co-sign.

Errors

An error response carries a conventional HTTP status and a JSON envelope: code (a stable machine-readable string, e.g. INPUT_VALIDATION_FAILED), status, message, defined (whether the code is one the endpoint documents), and an optional data object with structured detail (for INPUT_VALIDATION_FAILED, the { formErrors, fieldErrors } from the failed request). For backward compatibility the body also includes an error field equal to message.

Each endpoint documents the specific codes it can return as a code enum (with a per-code message table and a copy-pasteable example per code), grouped by HTTP status, so you see only the errors that endpoint actually raises rather than a catalog-wide list.

Server:https://api.phygitals.com/api

Production

Client Libraries

Claw machines

Public pack-catalog reads: the available-pack listing and chase-card previews for a pack (“what could I pull?”).

List available packs

Lists every purchasable pack in the storefront catalog: pricing, expected value (EV), rarity distribution, and reward metadata for each. Public catalog data: no credential required. Use platform to select a storefront (defaults to the first-party mainnet packs), includeRepacks=true to include partner repack listings, and excludeVariants=true to drop a base pack's sibling variant editions and list only base packs. Returns 200 with an empty array when no packs match the filters.

Query Parameters
  • platform
    Type: string

    Platform filter (e.g. "mainnet" for first-party Phygitals packs). Defaults to "mainnet".

  • includeRepacks

    Include partner repack listings. Accepts a real boolean from an RPC-style caller, or the literal string "true" from a query string; any other string is treated as false.

    • Type: boolean

      Include partner repack listings. Accepts a real boolean from an RPC-style caller, or the literal string "true" from a query string; any other string is treated as false.

  • excludeVariants

    Exclude child variant packs (rows with a non-null variant_of, a base pack's sibling spice-level editions), returning only base packs. Accepts a real boolean from an RPC-style caller, or the literal string "true" from a query string; any other value is treated as false. Omitted returns every pack, variants included.

    • Type: boolean

      Exclude child variant packs (rows with a non-null variant_of, a base pack's sibling spice-level editions), returning only base packs. Accepts a real boolean from an RPC-style caller, or the literal string "true" from a query string; any other value is treated as false. Omitted returns every pack, variants included.

Responses
  • application/json
  • application/json
Request Example for get/vm/available
curl https://api.phygitals.com/api/vm/available
[
  {
    "id": "AaB9kQ7pXt2vNf4dLmH8sYcR3wZ1eJ6uK5gP0oT2iVn",
    "slug": "topps-chrome-2024",
    "platform": "mainnet",
    "type": "CORE",
    "name": "Topps Chrome 2024",
    "enable": true,
    "description": "Chrome refractors, rookie autos, and numbered parallels.",
    "mint_price": "9.99",
    "max_per_mint": 10,
    "in_stock": true,
    "num_pulls_7d": 128,
    "chase": [],
    "rarity_distribution": [
      {
        "id": 0,
        "name": "Common",
        "color": "#9ca3af",
        "lower": 0,
        "upper": 10,
        "weight": 70
      },
      {
        "id": 1,
        "name": "Rare",
        "color": "#3b82f6",
        "lower": 10,
        "upper": 100,
        "weight": 25
      },
      {
        "id": 2,
        "name": "Chase",
        "color": "#f59e0b",
        "lower": 100,
        "upper": 1000,
        "weight": 5
      }
    ],
    "ev": 8.42,
    "ev_updated_at": "2026-01-02T03:04:05.678Z",
    "last_pull": null,
    "category": "cards",
    "categories": [
      "cards",
      "sports"
    ],
    "min_ev": 1.5,
    "max_ev": 250,
    "buyback_percent": 0.85,
    "repack": false,
    "claw_image_url": "https://cdn.phygitals.com/packs/topps-chrome-2024.png",
    "creator_profile": {
      "id": "did:privy:phygitals",
      "username": "phygitals",
      "profile_picture": null
    },
    "rewards_amounts": [],
    "sellback_rewards_amounts": [],
    "rewards_mint_addresses": [],
    "rewards_symbols": [],
    "rewards_decimals": [],
    "pulls_per_voucher": 0,
    "variant_of": null
  }
]

List a pack's chase cards

High-value chase cards for the pack identified by slug: the top cards by fair-market value in the pack's own rarity tier, so a buyer can preview what they could pull before purchasing. Public catalog data: no credential required. An unknown, mint-less, or tier-less slug returns 200 with an empty array rather than a 404, so an empty response means the pack has no chase cards to preview.

Path Parameters
  • slug
    Type: string
    required

    The pack's URL slug: the identifier used across the pack catalog (e.g. topps-chrome-2024).

Responses
  • application/json
  • application/json
Request Example for get/vm/chase/{slug}
curl https://api.phygitals.com/api/vm/chase/topps-chrome-2024
[
  {
    "id": "8674311907",
    "name": "Juan Soto RC Auto /25",
    "image": "https://cdn.phygitals.com/cards/soto-rc-auto.png",
    "fmv": 250
  }
]

Pack buys

Pack (claw) buy flow: settle a purchase with a signed on-chain payment, then poll the session for fulfillment.

Settle a pack buy

Completes a pack purchase and returns the drawn items. The server verifies the payment, runs the provably-fair draw, delivers the cards on-chain, and answers with the pulled items, so a 200 here means the buy is settled, not merely accepted.

Scope required: vm.buy.crypto. Send a phy_ user API key as X-API-Key or Authorization: Bearer phy_…. Authenticated traffic is rate-limited (429 when exceeded).

Before you call

  1. An API key with the vm.buy.crypto scope.
  2. A linked external Solana wallet holding enough USDC or USDT.
  3. The pack's details from GET /api/vm/available: you need id (send it as claw_id), mint_price to compute the payment amount, and max_per_mint to cap amount. A pack whose in_stock is false is sold out.

Payment methods

The body is one of four shapes, picked by payment_method and, for a crypto buy, by chain. Fields belonging to another shape are ignored.

payment_method Pays with Also required
crypto on solana (the default) A transfer the buyer signed, in txs currency; legs when splitting across two stablecoins
crypto on an EVM chain An ERC-20 permit_data the buyer signed Nothing else
credits 100% Pack Credits idempotency_key; not available with an API key (403)
mixed Pack Credits plus an on-chain remainder idempotency_key, credit_portion_bps, txs, legs; the same

A voucher_id replaces the payment entirely: the on-chain payment is omitted and the buy is one pull.

Producing txs (Solana)

API-key callers build and sign the payment client-side (there is no phy_-callable builder that returns a ready transaction). Submit the signed bytes in txs as a base64 string or a byte array.

  1. Resolve the current signer pubkeys from POST /api/orpc/config/signers/pubkeys: you need vmBuyback (owner of the payment-receiver ATA), and solanaFeePayer if you want Phygitals to pay the network fee.
  2. Payment amount = amount × mint_price × 1_000_000 in the chosen stablecoin's base units (6 decimals; round to an integer, so 2 pulls at $25.00 is 50_000_000). Transfer that from the buyer ATA to the vmBuyback ATA for that mint.
  3. Compile a versioned (v0) transaction, then have the buyer wallet sign. Optional compute-budget and memo instructions may wrap the transfer; a mismatched amount, currency, receiver, fee payer, or stale blockhash fails verification.

Who pays the network fee

The fee payer is payerKey / account index 0, and you choose between two wallets:

Fee payer at index 0 Effect
solanaFeePayer Phygitals pays the fee and co-signs index 0. This is the default.
buyer_wallet The buyer pays their own fee. Sign index 0 with the buyer's key, or the transaction is rejected.

Any other wallet at index 0 is rejected: Phygitals will not co-sign for a fee payer it does not hold, and the buyer's signature does not cover a third party's slot. Build the rest of the transaction identically either way: verification derives what it expects from the fee payer you chose, so the two shapes are equally valid but not interchangeable within one request.

When using legs, those legs must describe the same transfers as the signed transactions.

The server rebuilds the expected payment from buyer_wallet, claw_id, amount, currency, legs, and the voucher flag, and rejects a mismatch with 400 ("Transaction fingerprint mismatch"). Nothing is charged. Rebuild with a fresh blockhash and re-sign when that happens.

After a purchase

The response carries the settled result directly. If you need to re-read it, or a client timeout left you unsure, poll POST /api/vm/buy/status with the returned result.session_id, waiting briefly between attempts. Record a pull only when that poll returns { result }; see that endpoint's docs for the in-flight status values (there is no "pending" value).

Do NOT retry this endpoint to recover: a retry re-sends a transaction whose blockhash is stale and no longer matches the request, and on credits / mixed the idempotency_key is what makes a replay safe.

Errors

Status Example Cause
400 { "error": "Transaction fingerprint mismatch: …" } The signed transaction is not the payment the server expected
400 { "error": "Unsupported currency: …" } No currency was sent
400 { "error": "…" } Pack out of stock, voucher not claimable, insufficient credits, or another business-rule rejection
401 { "message": "Invalid or missing credentials" } Missing, invalid, revoked, or out-of-scope key
403 { "message": "Pack Credits purchases require a user session, not an API key" } credits or mixed attempted with an API key
422 { "message": "amount must be a positive integer" } The body failed validation; the message names the field
429 { "message": "Rate limit exceeded" } Rate limit exceeded for this key
502 { "message": "Pack credits purchase failed" } A server-side fault on a Pack Credits path
Body
required
application/json
    • Settle a pack buy with a signed Solana payment transaction.

      • amount
        Type: integer
        greater than:  
        0
        min:  
        -9007199254740991
        max:  
        1000
        required

        How many pulls to settle. Must not exceed the pack's max_per_mint.

      • chain
        const:  
        solana
        required
      • claw_id
        Type: string
        min length:  
        1
        required

        Pack identifier, as returned by GET /api/vm/available.

      • payment_method
        const:  
        crypto
        required
      • buyer_wallet
        Type: string Pattern: ^[1-9A-HJ-NP-Za-km-z]{32,44}$nullable

        API-key callers only: settle on behalf of this wallet's owner rather than the key's owner. Must be the wallet that signed the payment transaction. Vouchers still follow the authenticated caller.

        A base58-encoded Solana address (a 32-byte account address; may be a keypair pubkey or a PDA).

      • currency
        Type: string enum

        Payment token: usdc or usdt.

        values
        • usdc
        • usdt
      • extraData
        Type: object nullable

        Caller-supplied metadata stored with the purchase session.

      • legs
        Type: array object[] | null · Payment Leg[] …2nullable

        Split tender: one or two stablecoin legs that together fund the payment. Must describe the same transfers as the signed transactions in txs.

        One stablecoin transfer funding part of the on-chain remainder.

      • txs
        Type: array | null · Signed Transaction[] …4nullable

        The buyer-signed payment transaction(s). Fee payer (account index 0) is either solanaFeePayer, which Phygitals co-signs, or the buyer's own wallet, which the buyer must sign. Absent for a voucher redemption, which pays nothing on chain.

      • voucher_id
        Type: string nullable

        A PURCHASE voucher's id or public code to spend on this pull. On Solana the payment transfer is replaced with a noop instruction.

Responses
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
Request Example for post/vm/buy/crypto
curl https://api.phygitals.com/api/vm/buy/crypto \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: YOUR_SECRET_TOKEN' \
  --data '{
  "payment_method": "crypto",
  "claw_id": "BSG6DyEihFFtfvxtL9mKYsvTwiZXB1rq5gARMTJC2xAM",
  "amount": 1,
  "voucher_id": "",
  "extraData": {
    "additionalProperty": ""
  },
  "chain": "solana",
  "currency": "usdc",
  "txs": [
    ""
  ],
  "legs": [
    {
      "currency": "usdc",
      "units": 4500000
    }
  ],
  "buyer_wallet": "BSG6DyEihFFtfvxtL9mKYsvTwiZXB1rq5gARMTJC2xAM"
}'
{
  "result": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "user_id": "did:privy:abc123",
    "public_id": "_a1b2c3d4",
    "total": 25,
    "tx_hash": "5Kn8...bundle_signature",
    "nfts": [
      {
        "id": "9XnY6bTkPu3mRc8vJfLdQa2ZwEoN7sH1yG4iVxB5tCrM",
        "buyback_price": 21.25,
        "type": "ebay"
      }
    ]
  }
}

Poll pack-buy fulfillment

Polls the status of a purchase session created by POST /api/vm/buy/crypto. Authenticate with a phy_ user API key holding the vm.buy.crypto scope (X-API-Key or Authorization: Bearer phy_…).

Outcomes

Outcome Response Action
Fulfilled { "result": { session_id, user_id, buyer_wallet, public_id, nfts, tx_hash? } } Stop; record the pull once
Failed HTTP 400 "Transaction failed" Stop; do not record
Still in flight { "status": … } Keep polling, do not record yet

There is no wire value "pending". Treating anything other than a result payload as complete will double-count when fulfillment lands.

status values (in-flight arm)

Value Meaning Action
null / absent Session open; nothing settled yet Keep polling
processing Purchase still in progress Keep polling
debited Credits applied; delivery not yet confirmed Keep polling
review Parked for reconciliation (may have broadcast; not auto-refunded) Keep polling
success Terminal; delivered Normally returned as { "result": … } instead
failed Terminal; did not complete Normally returned as HTTP 400 instead

Any unrecognized status should be treated as still in flight.

result.buyer_wallet

On a fulfilled response, buyer_wallet is the wallet that paid and received the pull. When POST /api/vm/buy/crypto was called with a buyer_wallet override, this field echoes that override; otherwise it is the caller's linked wallet.

Polling

Retry with a short delay between attempts until result arrives or a 400 is returned. Safe completion rule: record only when result is present.

Body
required
application/json
  • session_id
    Type: string Format: uuid
    required

    Session ID from the buy/crypto response (result.session_id).

Responses
  • application/json
  • application/json
  • application/json
  • application/json
Request Example for post/vm/buy/status
curl https://api.phygitals.com/api/vm/buy/status \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: YOUR_SECRET_TOKEN' \
  --data '{
  "session_id": "550e8400-e29b-41d4-a716-446655440000"
}'
{
  "result": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "user_id": "did:privy:abc123",
    "buyer_wallet": "7Z3TvJrbjTFn9Vd8SzHfPu9pQpEdxKzsEwUqcXNoWaSF",
    "public_id": "_a1b2c3d4",
    "tx_hash": "5Kn8...bundle_signature",
    "nfts": [
      {
        "id": "9XnY...mint_address",
        "type": "enft",
        "buyback_price": 21.25,
        "mint_address": "9XnY...mint_address",
        "content": {
          "metadata": {
            "name": "2024 Juan Soto RC Auto /25",
            "image": "https://cdn.phygitals.com/cards/example.png"
          },
          "links": {
            "image": "https://cdn.phygitals.com/cards/example.png"
          }
        }
      }
    ]
  }
}

Marketplace

Public catalog reads (the metadata filter facets that populate the trait pickers on the listing search), plus claw buyback: sell a pulled card back into the pool at its standing offer, in two steps (build the sellback transaction for optional owner_wallet or the caller's wallets, then settle).

Get marketplace filters

Returns the allowed metadata facet values for the Phygitals marketplace catalog: a map of trait name → allowed-value options, restricted to the public whitelist (Type, Rarity, Set, Grader, Grade, Set Release Date, Language). Use it to populate filter UI. Public catalog data: no credential required.

Responses
  • application/json
  • application/json
Request Example for get/marketplace/filters
curl https://api.phygitals.com/api/marketplace/filters
{
  "filters": {
    "metadata": {
      "Grader": [
        {
          "value": "PSA",
          "count": 1842
        },
        {
          "value": "BGS",
          "count": 311
        }
      ],
      "Grade": [
        {
          "value": "10",
          "count": 906
        },
        {
          "value": "9.5",
          "count": 274
        }
      ],
      "Language": [
        {
          "value": "English",
          "count": 2103
        },
        {
          "value": "Japanese",
          "count": 588
        }
      ]
    },
    "priceRanges": []
  }
}

Start a claw buyback (build the sellback transaction)

Prices a standing buyback offer on a claw-pulled card and returns the transaction(s) that swap it for USDC, plus the session id they are pinned to. Pass optional owner_wallet to open a sellback for that holder to sign; omit it to use the caller's own wallets. Exactly one offer must match, or the request is rejected. Optional client_id is stored on the session and echoed unchanged by take-claw-bid-finish so you can correlate the two steps. Nothing settles here: sign the transactions with the seller's wallet and post them to take-claw-bid-finish. API-key callers need the marketplace.take-claw-bid scope; exceeding the rate limit returns 429.

Who pays the network fee

fee_payer picks the wallet in the transaction's fee-payer slot (account index 0):

fee_payer Effect
platform (default) Phygitals pays the fee and the payout-ATA rent, and signs that slot during finish.
user The seller wallet pays both. Their signature covers the fee-payer slot as well as the transfer they authorize.

The choice is fixed when the session is opened, because the fee payer also funds the seller's USDC account when they do not have one yet, which makes it part of the transaction take-claw-bid-finish matches the session against. Ask for user at init or not at all; you cannot switch at finish, and a transaction built the other way is rejected there as a fingerprint mismatch.

Body·TakeClawBidInitInput
required
application/json
  • mint_address
    Type: string
    min length:  
    1
    required

    Mint address of the card being sold back. Must resolve to EXACTLY ONE standing buyback offer for owner_wallet (when set) or one of the caller's active wallets, or the request is rejected.

  • client_id
    Type: string
    min length:  
    1
    max length:  
    128

    Optional opaque id of your choosing. Stored with the sellback session and returned unchanged on take-claw-bid-finish (including an idempotent replay), so you can correlate finish with the call that opened the session.

  • fee_payer
    Type: string enum

    Who pays the network fee and any payout-ATA rent on the returned transactions. platform (the default) puts the Phygitals fee payer in that slot and signs it at finish; user puts the seller wallet there instead, so the seller's own signature covers the fee.

    values
    • platform
    • user
  • owner_wallet
    Type: string Pattern: ^[1-9A-HJ-NP-Za-km-z]{32,44}$

    Holder wallet that owns the card and will co-sign the returned transactions. When set, the offer is priced for this wallet so an integrator key can open a sellback their end user then signs. Omit to use the caller's own wallets.

Responses
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
Request Example for post/marketplace/transaction/take-claw-bid-init
curl https://api.phygitals.com/api/marketplace/transaction/take-claw-bid-init \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: YOUR_SECRET_TOKEN' \
  --data '{
  "mint_address": "8sN9dCLMzmvJvDCK1YbQGoDPbjLLxWLCVDDcU8Xt4ZAp"
}'
{
  "session_id": "019627f4-1f2c-7b3e-9a11-4c5d6e7f8091",
  "txV0s": [
    "gAEAAQ...base64-versioned-transaction"
  ]
}

Finish a claw buyback (settle and pay out)

Settles the sellback opened by take-claw-bid-init and pays the seller. Solana callers post the session id plus the signed transactions; EVM callers (chain: animechain | arbitrum) post the card and a signed ERC-721 permit instead. Post back exactly the transactions init returned, signed by the seller's wallet. Phygitals adds the payout authority's signature, and the fee payer's when the session was opened with the default fee_payer: "platform". A fee_payer: "user" session gets no platform signature in that slot at all, so the seller's own must cover it, and an unsigned fee-payer slot is refused here rather than dropped on-chain. A 200 with tx_status: "success" includes tx_hash (the Solana payout signature(s), or the EVM settlement hash). When init was called with a client_id, that value is echoed on every finish response for the session — including an idempotent replay. Safe to retry: a replayed call answers 409 while the sellback is in flight and 200 with the recorded status (and tx_hash when the prior finish succeeded) once it is terminal. API-key callers need the marketplace.take-claw-bid scope; exceeding the rate limit returns 429.

Body·TakeClawBidFinishInput
required
application/json
  • chain
    Type: string

    Settlement chain. animechain / arbitrum select the EVM path (mint_address + nft_permit); anything else, including omitting it, selects the Solana path (session_id + txs).

  • mint_address
    Type: string

    EVM path: the card being sold back. Required there, ignored on Solana.

  • nft_permit
    Type: object

    EVM path: the seller's signed ERC-721 permit authorizing the swap. Required there.

  • session_id
    Type: string

    Solana path: the session id take-claw-bid-init returned. Required there, ignored on EVM.

  • skip_rewards
    Type: boolean

    Integrations should omit this field. Omitted or true accrues no rewards; do not rely on false.

  • txs
    Type: array · Signed Transaction[] 1…4

    Solana path: the init transactions, now co-signed by the seller's wallet. Must be the same transactions returned by init (non-empty, at most 4).

Responses
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
Request Example for post/marketplace/transaction/take-claw-bid-finish
curl https://api.phygitals.com/api/marketplace/transaction/take-claw-bid-finish \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: YOUR_SECRET_TOKEN' \
  --data '{
  "session_id": "019627f4-1f2c-7b3e-9a11-4c5d6e7f8091",
  "txs": [
    [
      128,
      1,
      0,
      1
    ]
  ],
  "skip_rewards": true
}'
{
  "tx_status": "success",
  "tx_hash": "5Kn8TvJrbjTFn9Vd8SzHfPu9pQpEdxKzsEwUqcXNoWaSF",
  "referral_credit": {
    "referrer_username": "collector",
    "amount_usd": 0.42
  },
  "client_id": "order-42"
}

Vending machine

Buyback quote by card. GET /vm/offers/{mint_address_or_slug} returns the standing claw offer (same marketplace.take-claw-bid scope as the sellback).

Vending machine Operations

Get the buyback offer for one NFT

Returns the standing claw buyback bid for one card (by mint address or slug), or an empty offers array when none is eligible. API-key callers need the marketplace.take-claw-bid scope (same as the sellback); exceeding the rate limit returns 429.

Path Parameters
  • mint_address_or_slug
    Type: string
    min length:  
    1
    required

    The NFT's on-chain address or its /card/<slug> segment.

Responses
  • application/json
  • application/json
  • application/json
  • application/json
Request Example for get/vm/offers/{mint_address_or_slug}
curl 'https://api.phygitals.com/api/vm/offers/{mint_address_or_slug}' \
  --header 'X-API-Key: YOUR_SECRET_TOKEN'
{
  "offers": [
    {
      "bid_state": "string",
      "owner": "string",
      "closer": "string",
      "seller": "string",
      "target_id": "string",
      "state": "string",
      "amount": 1,
      "expires_at": "string",
      "created_at": "2026-08-24T21:24:20.244Z",
      "updated_at": "2026-08-24T21:24:20.244Z"
    }
  ]
}