Docs / Developers

Listings API

Everything for sale on Vollector as JSON: a snapshot, a changes feed and buy links, free and keyless.

Prefer plain text, or handing this to an assistant? Read it as markdown.

On this page What it is

What it is#

Everything collectors list for sale on Vollector, for any app, aggregator or bot that wants to show it. Read-only, free, no key, and open to any origin (CORS *), so it works from a server or straight from a browser.

It is link-out: each listing carries a url that opens the purchase on Vollector, where the buyer checks out. You never hold an order, a signature or anyone's funds, and there is nothing to integrate beyond reading JSON and linking.

Endpoints#

PathWhat it answers
GET /listingsEvery live listing, a page at a time.
GET /listings/{id}One listing: 200 while for sale, 410 with its status after, 404 if unknown.
GET /changesWhat changed since a cursor: upserts and removals, oldest first.

Paths are under the base URL. Everything is GET and JSON.

Quick start#

# Everything for sale, first page
curl "https://api.vollector.id/v1/listings?ref=yourapp"

# Courtyard cards under $500, 500 per page
curl "https://api.vollector.id/v1/listings?issuer=courtyard&max_price=500&limit=500&ref=yourapp"

# Everything for sale on Solana
curl "https://api.vollector.id/v1/listings?chain=solana&ref=yourapp"

# One listing
curl "https://api.vollector.id/v1/listings/7b0c1e52-3f4a-4c8e-9d21-5a6f0b8e2c13"

# A starting cursor for the changes feed, then what changed since
curl "https://api.vollector.id/v1/changes"
curl "https://api.vollector.id/v1/changes?since=MjAyNi0xMC0wMlQxODowNDo0NS4wMDBafGZmZmZmZmZmLWZmZmYtZmZmZi1mZmZmLWZmZmZmZmZmZmZmZg"

GET /listings#

Every live listing, in id order. Walk it with after, the next_after of the page before, until has_more is false. The order is stable, so a walk never skips or repeats a listing because another was added meanwhile.

ParameterTypeMeaning
limitintegerListings per page, 1 to 500. Default 100.
afterlisting idThe next_after of the previous page. Leave it out for the first page.
chainstringOnly this chain: solana, ethereum, polygon, base, abstract, flow.
issuerstringOnly this issuer's collectibles: courtyard, collector-crypt, phygitals, beezie, rip-fun, rip-cars, dyli.
refstringYour app's name, 1 to 32 letters, digits, - or _. It rides on every listing's url, so we can see the buyers you send. A Connect API partner uses its partner id.
min_pricenumberLowest price, in USD.
max_pricenumberHighest price, in USD.
Unknown parameters are refused with a 400, and so is one given twice. That is deliberate: every answer is cached by its URL, and a stray parameter would be a fresh, uncached question.
{
  "listings": [
    {
      "id": "7b0c1e52-3f4a-4c8e-9d21-5a6f0b8e2c13",
      "url": "https://vollector.id/buy/7b0c1e52-3f4a-4c8e-9d21-5a6f0b8e2c13?ref=yourapp",
      "price": {
        "amount": "1250.00",
        "currency": "USDC"
      },
      "item": {
        "chain": "polygon",
        "contract": "0x251BE3A17Af4892035C37ebf5890F4a4D889dcAD",
        "token_id": "48211393570123994812385730016622931807",
        "standard": "erc721",
        "quantity": 1,
        "name": "2023 Pokemon Scarlet & Violet 151 Charizard ex #199 PSA 10",
        "image": "https://images.example.com/charizard-151-psa10.png",
        "issuer": {
          "slug": "courtyard",
          "name": "Courtyard"
        },
        "grader": "PSA",
        "grade": "10",
        "tier": null,
        "cert": "84512039"
      },
      "seller": {
        "wallet": "0x9f3a2b8c41d5e6f70812a3b4c5d6e7f8091a2b3c"
      },
      "listed_at": "2026-10-02T18:00:00.124518+00:00",
      "updated_at": "2026-10-02T18:00:00.124518+00:00",
      "expires_at": "2026-11-01T18:00:00+00:00"
    }
  ],
  "next_after": "7b0c1e52-3f4a-4c8e-9d21-5a6f0b8e2c13",
  "has_more": true
}

GET /changes#

What changed after a cursor, oldest first, up to 20 to 35 seconds ago. An upsert carries the listing as it is now (it is new, or its price or expiry moved); a remove says why it left. Follow next_cursor while has_more is true; once it is false you are up to date, and polling again in 30 to 60 seconds is plenty.

Without since the answer is empty, and its next_cursor is where to start: 15 minutes back, so a snapshot read right after it misses nothing. Pass the same filters you used for the snapshot.

ParameterTypeMeaning
sincecursorThe next_cursor of the previous answer. Leave it out to get a starting cursor.
limitintegerChanges per page, 1 to 500. Default 100.
chainstringOnly this chain: solana, ethereum, polygon, base, abstract, flow.
issuerstringOnly this issuer's collectibles: courtyard, collector-crypt, phygitals, beezie, rip-fun, rip-cars, dyli.
refstringYour app's name, 1 to 32 letters, digits, - or _. It rides on every listing's url, so we can see the buyers you send. A Connect API partner uses its partner id.

Why a removal happens

ReasonMeaning
soldSomebody bought it.
cancelledThe seller took it down.
expiredIt reached expires_at.
failedA purchase reverted on-chain, which usually means the seller's approval is gone; it stays off until they relist.
{
  "changes": [
    {
      "type": "upsert",
      "id": "7b0c1e52-3f4a-4c8e-9d21-5a6f0b8e2c13",
      "updated_at": "2026-10-02T18:00:00.124518+00:00",
      "listing": {
        "id": "7b0c1e52-3f4a-4c8e-9d21-5a6f0b8e2c13",
        "url": "https://vollector.id/buy/7b0c1e52-3f4a-4c8e-9d21-5a6f0b8e2c13?ref=yourapp",
        "price": {
          "amount": "1250.00",
          "currency": "USDC"
        },
        "item": {
          "chain": "polygon",
          "contract": "0x251BE3A17Af4892035C37ebf5890F4a4D889dcAD",
          "token_id": "48211393570123994812385730016622931807",
          "standard": "erc721",
          "quantity": 1,
          "name": "2023 Pokemon Scarlet & Violet 151 Charizard ex #199 PSA 10",
          "image": "https://images.example.com/charizard-151-psa10.png",
          "issuer": {
            "slug": "courtyard",
            "name": "Courtyard"
          },
          "grader": "PSA",
          "grade": "10",
          "tier": null,
          "cert": "84512039"
        },
        "seller": {
          "wallet": "0x9f3a2b8c41d5e6f70812a3b4c5d6e7f8091a2b3c"
        },
        "listed_at": "2026-10-02T18:00:00.124518+00:00",
        "updated_at": "2026-10-02T18:00:00.124518+00:00",
        "expires_at": "2026-11-01T18:00:00+00:00"
      }
    },
    {
      "type": "remove",
      "id": "c41d9a07-6e2b-4f13-8a55-0b7e9c3d1f28",
      "updated_at": "2026-10-02T18:03:12.551902+00:00",
      "reason": "sold"
    }
  ],
  "next_cursor": "MjAyNi0xMC0wMlQxODowNDo0NS4wMDBafGZmZmZmZmZmLWZmZmYtZmZmZi1mZmZmLWZmZmZmZmZmZmZmZg",
  "has_more": false
}

Keeping a full copy#

Three steps, and the copy stays exact for as long as you keep polling:

  1. Call /changes without since and keep its next_cursor.
  2. Walk /listings to the end. That is the book.
  3. Poll /changes?since=<cursor>: apply each upsert and removal, keep the new next_cursor, and fetch again at once while has_more is true.

Replays are harmless: an upsert always carries the current listing, and removing something you do not have is a no-op. Store the cursor and a restart resumes where it stopped; cursors do not expire.

const API = "https://api.vollector.id/v1";
const REF = "yourapp";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function get(path) {
  for (;;) {
    const res = await fetch(API + path);
    if (res.ok) return res.json();
    if (res.status !== 503) throw new Error(res.status + " " + (await res.text()));
    await sleep(5000); // our side hiccuped: retry shortly
  }
}

// 1. A starting cursor, taken BEFORE the snapshot so nothing slips between them.
let cursor = (await get("/changes?ref=" + REF)).next_cursor;

// 2. The snapshot: every live listing.
const book = new Map();
let after = null;
do {
  const page = await get("/listings?limit=500&ref=" + REF + (after ? "&after=" + after : ""));
  for (const l of page.listings) book.set(l.id, l);
  after = page.has_more ? page.next_after : null;
} while (after);

// 3. Keep up, for as long as you run.
for (;;) {
  const page = await get("/changes?limit=500&ref=" + REF + "&since=" + cursor);
  for (const c of page.changes) {
    if (c.type === "upsert") book.set(c.id, c.listing);
    else book.delete(c.id);
  }
  cursor = page.next_cursor; // store it: a restart resumes from here
  if (!page.has_more) await sleep(30_000);
}
Only showing a few listings, say on a card page? Skip the feed and ask /listings/{id}, or filter /listings by issuer or chain.

GET /listings/{id}#

One listing, by the id the other endpoints gave you.

{
  "listing": {
    "id": "7b0c1e52-3f4a-4c8e-9d21-5a6f0b8e2c13",
    "url": "https://vollector.id/buy/7b0c1e52-3f4a-4c8e-9d21-5a6f0b8e2c13?ref=yourapp",
    "price": {
      "amount": "1250.00",
      "currency": "USDC"
    },
    "item": {
      "chain": "polygon",
      "contract": "0x251BE3A17Af4892035C37ebf5890F4a4D889dcAD",
      "token_id": "48211393570123994812385730016622931807",
      "standard": "erc721",
      "quantity": 1,
      "name": "2023 Pokemon Scarlet & Violet 151 Charizard ex #199 PSA 10",
      "image": "https://images.example.com/charizard-151-psa10.png",
      "issuer": {
        "slug": "courtyard",
        "name": "Courtyard"
      },
      "grader": "PSA",
      "grade": "10",
      "tier": null,
      "cert": "84512039"
    },
    "seller": {
      "wallet": "0x9f3a2b8c41d5e6f70812a3b4c5d6e7f8091a2b3c"
    },
    "listed_at": "2026-10-02T18:00:00.124518+00:00",
    "updated_at": "2026-10-02T18:00:00.124518+00:00",
    "expires_at": "2026-11-01T18:00:00+00:00"
  }
}

Once it is gone the answer is a 410 with its status, one of the removal reasons above: {"id": "...", "status": "sold"}. An id that never was a listing is a 404.

The listing object#

FieldTypeMeaning
idstringThe listing's id. Stable for its whole life; a relist is a new id.
urlstringWhere to send a buyer. Opens the card on Vollector with the purchase ready. Link to this, not to a page you build.
price.amountstringThe price as a decimal string, at least two decimals ("1250.00"). A string so no float rounds it.
price.currencystringAlways USDC today.
item.chainstringThe chain the collectible lives on.
item.contractstringIts contract (EVM) or collection (Solana) address. EVM addresses may come in either case; compare them lowercased.
item.token_idstringIts token id on EVM; its asset (mint) address on Solana.
item.standardstringerc721, erc1155 or solana.
item.quantityintegerUnits for sale in this listing, 1 for a single card.
item.namestring | nullThe collectible's name as the issuer gives it.
item.imagestring | nullAn image URL. Hotlinking is fine.
item.issuerobject | null{ slug, name } of the platform that tokenized it; the slug is what the issuer filter takes.
item.graderstring | nullPSA, BGS, CGC, SGC and so on, when graded.
item.gradestring | nullThe grade, as a string ("10", "9.5").
item.tierstring | nullThe tier above a plain 10, when the card has it: black_label (Beckett, all four subgrades 10) or pristine (CGC). Its grade reads 10.
item.certstring | nullThe grading certificate number, when known.
seller.walletstringThe wallet the listing is signed by.
listed_attimestampWhen it was listed.
updated_attimestampWhen something on this object last changed.
expires_attimestamp | nullWhen the listing lapses on its own. Hide it from then on, even before the feed says so.

Issuers today: courtyard (Courtyard), collector-crypt (Collector Crypt), phygitals (Phygitals), beezie (Beezie), rip-fun (RIP.FUN), rip-cars (Rip Cars), dyli (DYLI).

There is no fair-market value in this API. The price is the seller's.

Linking and attribution#

Send buyers to url as it is. It goes through https://vollector.id/buy/{id}, which opens the card's current page with the purchase ready. Once the listing is gone it opens the card alone, so a stale link never dead-ends.

Add ref=<your app> to your calls and every url carries it, so we can credit the buyers you send. Without it we go by the site the click came from, which apps and links with rel="noreferrer" do not reveal. If you also integrate Connect API, use your partner id as ref: one identifier across both.

  • Show the price as price.amount price.currency, unrounded.
  • Hide a listing once its expires_at has passed, even before the feed sends its removal.
  • Images may be hotlinked.

Logo and icon#

To show where a listing is sold: the mark next to the price or the buy button, with "Vollector" or "Buy on Vollector", or the icon wherever you list your sources.

Vollector mark

Mark

SVG, takes the color of the text around it: next to a price, in a button.

Vollector icon, dark

Icon, dark

White mark on black, square: avatars, source lists, app grids on dark interfaces.

Vollector icon, light

Icon, light

Black mark on white, square: the same, on light interfaces.

  • Use the files as they are: not stretched, recolored or redrawn.
  • Next to a listing, the mark with "Vollector" or "Buy on Vollector" says where it is sold.

Caching and limits#

Every answer is the same for every caller and is cached at our CDN, so it is fast for you and close to free for us. There is no key and no hard rate limit. Be reasonable (a poll every 30 seconds is plenty) and it stays that way.

AnswerCDN cache (s)Served stale while refreshing (s)
/listings60300
/listings/{id}1560
/changes1530
A 400 (bad parameters)3000

Errors are JSON, {"error": "..."}. A 400 names the bad parameter. A 503 means our side hiccuped: retry after the Retry-After seconds.

Versioning#

This is v1. Fields may be added to any object; nothing is removed or changes meaning within v1. Anything breaking ships as v2, with v1 kept running. Every answer carries X-Api-Version.