Get Watchlist Price Updates (Cursor Delta Poll)
Changed onPrice changes for your watched cards since your cursor.
How it works
- First call: omit cursor. You'll page through the current state of every watched item, oldest price-change first (bootstrap walk). To start somewhere else, send "from_now": true (start at the head: no events, just a cursor to poll from) or "from_time": "<ISO 8601 time>" (start at that time). These only matter on a call without a cursor; once you send a cursor they are ignored.
- Store next_cursor from each response and pass it to the next call.
- While has_more is true, loop immediately. Once it is false you are caught up — keep polling with the latest cursor (once a minute is plenty).
Watch-all (*) keys: the walk from the beginning covers the whole catalog (millions of rows, thousands of pages). Unless you want that full snapshot, start with from_now (or from_time) and poll from there.
Your first call: choose where to start
Your first call has no cursor yet, so you tell the feed where to start. After that, every call is just {"cursor": "<next_cursor from the last response>"}.
A. Your watchlist's current state (the default; send {}):
curl -X POST -H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{}' \
"https://api.cardhedger.com/v1/cards/watchlist-updates"
You get the current price of everything you watch, oldest change first, in pages: keep calling with the returned next_cursor while has_more is true.
B. Only changes from now on (from_now; recommended for watch-all keys):
curl -X POST -H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"from_now": true}' \
"https://api.cardhedger.com/v1/cards/watchlist-updates"
# -> nothing to read yet, just a cursor to poll from:
# {"updates": [], "count": 0, "next_cursor": "eyJ2IjoxLCJjIjpbIjIwMjYtMTAtMTdUMTk6NTA6MDArMDA6MDAiLDBdLCJkIjpbIjIwMjYtMTAtMTdUMTk6NTA6MDArMDA6MDAiLDBdfQ==", "has_more": false}
# Your first real poll sends that cursor back, verbatim:
curl -X POST -H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"cursor": "eyJ2IjoxLCJjIjpbIjIwMjYtMTAtMTdUMTk6NTA6MDArMDA6MDAiLDBdLCJkIjpbIjIwMjYtMTAtMTdUMTk6NTA6MDArMDA6MDAiLDBdfQ=="}' \
"https://api.cardhedger.com/v1/cards/watchlist-updates"
# -> {"updates": [{"event": "changed", "card_id": "1586812246197x228181943611293700",
# "grade_label": "PSA 10", "latest_price": 1250.0, ...}], "count": 1,
# "next_cursor": "eyJ2IjoxLCJjIjpbIjIwMjYtMTAtMTdUMTk6NTY6NDEuMjIwOTEzKzAwOjAwIiw4MjEzXSwiZCI6WyIyMDI2LTEwLTE3VDE5OjUwOjAwKzAwOjAwIiwwXX0=", "has_more": false}
C. Changes since a time (from_time): send -d '{"from_time": "2026-10-17T00:00:00Z"}' to get everything that changed since then, in pages, like A.
from_now and from_time only apply to a call without a cursor. Once you send a cursor they are ignored, so one request builder can serve every call.
Update fields
- event — "changed" = new current price for this card+grade. "removed" = the price was retracted and nothing replaced it — drop your cached value (price fields are null).
- card_id — CardHedge card ID.
- provider / grade_label — grading company and grade (e.g. PSA / "PSA 10").
- latest_price — current price for this card+grade.
- median_price_90d / sample_count_90d — 90-day median and sale count, when available.
- last_updated — time of the underlying sale.
- changed_at — when this state change entered the feed (drives cursor ordering).
- fmv — Fair Market Value for this card+grade; only present when you send include_fmv: true (see below), otherwise null.
Fair Market Value (include_fmv)
Send "include_fmv": true to get CardHedge's smoothed valuation with each changed event, so you don't need a second card-fmv-batch call. fmv carries price, price_low, price_high, confidence_grade (A–D) and method; see card-fmv for what they mean.
- It costs latency. FMV is computed per card+grade by a separate service: expect roughly +0.3s on a quiet poll, about 1s on a full page of recent changes, and up to ~4s when you are catching up on older ones. Leave it off if you don't need it.
- Pages are capped at 200 events when it's on (a larger limit is lowered to that). Nothing is lost: keep looping while has_more is true.
- fmv is null when the FMV service is unavailable (or on removed events). The poll still succeeds and the cursor still advances, so a missed value is not redelivered; fetch it with card-fmv-batch if you need it. method: "no_data" means the service answered but has no value for this card+grade.
- Watch-all (*) watchlists: only once you are caught up. Your cursor must be from the last 60 minutes, otherwise the request returns 422. Start with from_now (or catch up without include_fmv), then add it. Valuing the old rows of a full walk would be thousands of slow lookups.
curl -X POST -H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"include_fmv": true}' \
"https://api.cardhedger.com/v1/cards/watchlist-updates"
# Same polling loop as below, with {"include_fmv": True} added to the body.
for u in data["updates"]:
if u["event"] == "changed":
fmv = u["fmv"] # None if the FMV service was unavailable
value = fmv["price"] if fmv else None
Semantics
- Events are coalesced state changes: "the current price for this card+grade changed", not per-sale records. If a card trades three times between polls you see the latest state once.
- Apply events in order; the same card+grade may legitimately appear more than once in a walk (old state then newer state). Last state wins.
- Cursors never expire. A client offline for weeks just catches up.
- Updates appear here ~2–3 minutes after a sale syncs: a short safety horizon guarantees no update can ever be skipped, trading a little latency for a lossless feed.
- On errors, retry with the same cursor — no updates are lost. Cursors are opaque; only ever pass back a cursor this endpoint returned.
Politeness
Poll on a timer started at process launch (e.g. every 60s), not on the wall-clock minute — synchronized :00 polling creates load spikes.
Example: walk your watchlist, then poll with the cursor
# 1) First call: no cursor. Returns current state of your watchlist
# plus a next_cursor. Repeat with the cursor while has_more is true.
curl -X POST -H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{}' \
"https://api.cardhedger.com/v1/cards/watchlist-updates"
# -> {"updates": [ ... ], "count": 24,
# "next_cursor": "eyJ2IjoxLCJjIjpbIjIwMjYtMDctMDNUMTg6MDE6MTIuNDEwMjMzKzAwOjAwIiw0MjFdLCJkIjpbbnVsbCxudWxsXX0=",
# "has_more": false}
# 2) Every later poll: send back the next_cursor you received, verbatim.
curl -X POST -H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"cursor": "eyJ2IjoxLCJjIjpbIjIwMjYtMDctMDNUMTg6MDE6MTIuNDEwMjMzKzAwOjAwIiw0MjFdLCJkIjpbbnVsbCxudWxsXX0="}' \
"https://api.cardhedger.com/v1/cards/watchlist-updates"
# -> only what changed since that cursor; {"updates": [], "count": 0, ...}
# when nothing did. Store next_cursor each time - it never expires.
Example polling loop
import requests, time
API_KEY = "your-api-key"
URL = "https://api.cardhedger.com/v1/cards/watchlist-updates"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}
# Where the FIRST call starts (see "Your first call"):
FIRST_CALL = {} # walk your watchlist's current state, or
# FIRST_CALL = {"from_now": True} # only changes from now on (watch-all keys)
cursor = None
while True:
body = {"cursor": cursor} if cursor else FIRST_CALL
data = requests.post(URL, headers=HEADERS, json=body).json()
for u in data["updates"]:
if u["event"] == "changed":
print(f'{u["card_id"]} {u["grade_label"]}: ${u["latest_price"]}')
else: # "removed"
print(f'{u["card_id"]} {u["grade_label"]}: price retracted')
cursor = data["next_cursor"]
if not data["has_more"]:
time.sleep(60) # caught up - relax to steady polling
Request
- Base URLhttps://api.cardhedger.com
- AuthAPI key in header X-API-Key
Request body
Response
Price updates for watched cards since the cursor
Changes
- info
added the new optional request property
- info
added the new optional request property
- info
the request body
application/jsondefault value changed from{"limit":500,"include_fmv":false}to{"limit":500,"from_now":false,"include_fmv":false}
- info
- info
added the new optional request property
- info
the request body
application/jsondefault value changed from{"limit":500}to{"limit":500,"include_fmv":false} - info
added the optional property
//to the response with the status
- info