prizepicksapi

Line movement

Track PrizePicks line movement from the change feed.

The change feed records new, changed, unavailable, and restored offers. A line-movement alert needs only part of that stream, and it needs a freshness check before it notifies anyone.

What counts as movement

Event typeMovement signal?
offer.newNo previous line. This is the first time this service recorded the offer.
offer.changedOnly when previous_line is not null and differs from line.
offer.unavailableThe offer left the observed board. In a partial collection this may be a gap, not a removal.
offer.restoredThe offer was observed again after being unavailable.

A runnable Python consumer

This script stores its cursor and processed event IDs in SQLite, prints moves of at least one point, and stops without advancing when the source is not fresh or partial. Run it from cron or a scheduler; each run reads only events newer than the last committed cursor.

import json, os, sqlite3, time
from urllib.parse import urlencode
from urllib.request import Request, urlopen

KEY = os.environ["PRIZEPICKS_API_KEY"]
db = sqlite3.connect("movement.db")
db.execute("CREATE TABLE IF NOT EXISTS state (k TEXT PRIMARY KEY, v TEXT)")
db.execute("CREATE TABLE IF NOT EXISTS seen (event_id TEXT PRIMARY KEY)")

def get_changes(params):
    req = Request("https://prizepicksapi.com/api/v1/changes?" + urlencode(params),
                  headers={"Authorization": "Bearer " + KEY})
    with urlopen(req, timeout=20) as res:
        return json.load(res)

def poll(league="NBA", threshold=1.0):
    row = db.execute("SELECT v FROM state WHERE k = 'cursor'").fetchone()
    cursor = row[0] if row else None
    while True:
        params = {"league": league, "limit": 200}
        if cursor:
            params["after"] = cursor
        page = get_changes(params)
        status = page["meta"]["source_status"]
        if status not in ("fresh", "partial"):
            print("source is", status, "- alerts paused, cursor unchanged")
            return
        for event in page["data"]:
            if db.execute("SELECT 1 FROM seen WHERE event_id = ?", (event["id"],)).fetchone():
                continue
            prev = event["previous_line"]
            if event["type"] == "offer.changed" and prev is not None and abs(event["line"] - prev) >= threshold:
                print(event["player_name"], event["stat"], prev, "->", event["line"], "at", event["observed_at"])
            db.execute("INSERT INTO seen VALUES (?)", (event["id"],))
        if page["data"]:
            cursor = page["data"][-1]["cursor"]
            db.execute("INSERT OR REPLACE INTO state VALUES ('cursor', ?)", (cursor,))
        db.commit()  # alert side effects and cursor commit together
        if not page["meta"]["next_cursor"]:
            return
        time.sleep(6.1)

poll()

Replace print with your notifier, but keep the order: perform the side effect, mark the event seen, and commit the cursor in the same transaction. If the process crashes before commit, the next run replays those events, and the seen table prevents duplicate alerts.

Pick a threshold per market

A half-point move means something different in a points market than in a low-count market. Key thresholds on stat (and, if you track it from projections, market period) rather than using one global number. Change events carry stat but not period; look up the projection by projection_id when period matters.

Budget the polling interval

One poll with no new events is still one request. Polling every five minutes is 288 requests per day, which exceeds a Free monthly allowance within a few days; hourly is 24 per day. Choose the interval from your plan and from how quickly an alert is actually useful. Webhooks remove empty polls on the Production plan.

Limits to state in your alert

  • The time shown is when this service observed the change, not an exact upstream change time.
  • A move can be missed if it happened and reverted between two collections.
  • The line is the base line. Promotional or user-specific offers are not modeled.
  • An alert is informational data, not a recommendation.

Frequently asked questions

Does every offer.changed event mean the line moved?

No. A change can involve selections, status details, player, or event details. Compare previous_line with line to detect numeric movement.

Should I poll or use webhooks?

Polling /changes works on every plan and is easy to replay. Webhooks push the same events and require the Production plan.

References

Current API documentation ↗Machine-readable OpenAPI contract ↗SQLite: INSERT OR REPLACE ↗

PrizePicks API is an independent data service. It is not affiliated with or endorsed by PrizePicks. Observations, completeness, and freshness are separate claims; consult source status before relying on a stored line.