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#
| Path | What it answers |
|---|---|
GET /listings | Every 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 /changes | What 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.
| Parameter | Type | Meaning |
|---|---|---|
limit | integer | Listings per page, 1 to 500. Default 100. |
after | listing id | The next_after of the previous page. Leave it out for the first page. |
chain | string | Only this chain: solana, ethereum, polygon, base, abstract, flow. |
issuer | string | Only this issuer's collectibles: courtyard, collector-crypt, phygitals, beezie, rip-fun, rip-cars, dyli. |
ref | string | Your 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_price | number | Lowest price, in USD. |
max_price | number | Highest price, in USD. |
{
"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.
| Parameter | Type | Meaning |
|---|---|---|
since | cursor | The next_cursor of the previous answer. Leave it out to get a starting cursor. |
limit | integer | Changes per page, 1 to 500. Default 100. |
chain | string | Only this chain: solana, ethereum, polygon, base, abstract, flow. |
issuer | string | Only this issuer's collectibles: courtyard, collector-crypt, phygitals, beezie, rip-fun, rip-cars, dyli. |
ref | string | Your 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
| Reason | Meaning |
|---|---|
sold | Somebody bought it. |
cancelled | The seller took it down. |
expired | It reached expires_at. |
failed | A 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:
- Call
/changeswithoutsinceand keep itsnext_cursor. - Walk
/listingsto the end. That is the book. - Poll
/changes?since=<cursor>: apply each upsert and removal, keep the newnext_cursor, and fetch again at once whilehas_moreis 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);
}/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#
| Field | Type | Meaning |
|---|---|---|
id | string | The listing's id. Stable for its whole life; a relist is a new id. |
url | string | Where to send a buyer. Opens the card on Vollector with the purchase ready. Link to this, not to a page you build. |
price.amount | string | The price as a decimal string, at least two decimals ("1250.00"). A string so no float rounds it. |
price.currency | string | Always USDC today. |
item.chain | string | The chain the collectible lives on. |
item.contract | string | Its contract (EVM) or collection (Solana) address. EVM addresses may come in either case; compare them lowercased. |
item.token_id | string | Its token id on EVM; its asset (mint) address on Solana. |
item.standard | string | erc721, erc1155 or solana. |
item.quantity | integer | Units for sale in this listing, 1 for a single card. |
item.name | string | null | The collectible's name as the issuer gives it. |
item.image | string | null | An image URL. Hotlinking is fine. |
item.issuer | object | null | { slug, name } of the platform that tokenized it; the slug is what the issuer filter takes. |
item.grader | string | null | PSA, BGS, CGC, SGC and so on, when graded. |
item.grade | string | null | The grade, as a string ("10", "9.5"). |
item.tier | string | null | The 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.cert | string | null | The grading certificate number, when known. |
seller.wallet | string | The wallet the listing is signed by. |
listed_at | timestamp | When it was listed. |
updated_at | timestamp | When something on this object last changed. |
expires_at | timestamp | null | When 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).
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.amountprice.currency, unrounded. - Hide a listing once its
expires_athas 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.
Mark
SVG, takes the color of the text around it: next to a price, in a button.
- 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.
| Answer | CDN cache (s) | Served stale while refreshing (s) |
|---|---|---|
/listings | 60 | 300 |
/listings/{id} | 15 | 60 |
/changes | 15 | 30 |
A 400 (bad parameters) | 300 | 0 |
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.