Watchlist

Get Watchlist Price Updates (Cursor Delta Poll)

Changed on

Price changes for your watched cards since your cursor.

How it works

  1. 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.
  2. Store next_cursor from each response and pass it to the next call.
  3. 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
post/v1/cards/watchlist-updates

Request

  • Base URL
    https://api.cardhedger.com
  • Auth
    API key in header X-API-Key

Request body

cursorstring nullable

Opaque cursor from a previous response. Omit for the initial bootstrap walk (delta from the beginning of time).

limitinteger
from_nowboolean

Start at the head of the feed instead of walking from the beginning: returns no events and a next_cursor to poll from, so you only receive changes made after this call. Only meaningful on a call without a cursor; ignored when a cursor is sent. Cannot be combined with from_time.

from_timestring date-time nullable

Start the feed at this time (ISO 8601; no zone means UTC) instead of walking from the beginning. A time in the future starts at the head. Only meaningful on a call without a cursor; ignored when a cursor is sent. Cannot be combined with from_now. (A since field, as price-updates takes, has no effect here.)

include_fmvboolean

Attach CardHedge Fair Market Value to each changed event as fmv. Adds latency (about 0.3s for a quiet poll, about 1s for a full page of recent changes, up to ~4s when catching up on older ones), so limit is capped at 200 when this is set; keep looping while has_more is true. On a watch-all ('*') watchlist it is available only once you are caught up: your cursor must be from the last 60 minutes (422 otherwise; start with from_now or catch up without it).

Response

Price updates for watched cards since the cursor

countinteger required
next_cursorstring required
has_moreboolean required

Changes

    • info

      added the new optional request property

    • info

      added the new optional request property

    • info

      the request body application/json default value changed from {"limit":500,"include_fmv":false} to {"limit":500,"from_now":false,"include_fmv":false}

    • info

      added the new optional request property

    • info

      the request body application/json default value changed from {"limit":500} to {"limit":500,"include_fmv":false}

    • info

      added the optional property // to the response with the status