Practical guide · Odds movement API

How to detect Pinnacle odds drops without alert fatigue

Let PinnWire detect price falls server-side, then narrow the stream by size, sport, market, period, freshness, no-vig context, and persistence. The result is a useful signal feed instead of a notification firehose.

The recommended setup Use PinnWire’s /api/drops REST buffer to test and tune filters, then move the same strategy to /odds-drop or /odds-drop-prematch SSE for push delivery. PinnWire handles the actual Pinnacle odds-drop detection on the server. Your application only ranks, cools down, stores, and routes the alerts it wants.

What a Pinnacle odds drop tells you

An odds drop is a decrease in decimal price for one outcome on one line. A move from 2.10 to 1.95 is a 7.14% price drop:

drop_pct = (from_price − to_price) / from_price × 100

The shorter price implies a higher probability. That makes a Pinnacle move useful for line-movement monitoring, model calibration, market surveillance, and triggering a fresh comparison elsewhere.

It is still only a signal. It does not reveal who caused the move, guarantee that the new price is correct, or guarantee that a different price is profitable. A clean workflow treats the alert as a reason to inspect the market—not as an automatic instruction to bet.

Movement is not guaranteed value. Validate the current price, market rules, available stake, no-vig benchmark, and execution conditions before acting. PinnWire is an odds data service, not a bookmaker or betting system.

Why PinnWire is the cleanest starting point

PinnWire is an independent real-time Pinnacle odds API with server-side drop detection. It tracks each outcome through market updates, ignores unchanged versions and non-price volatility signals, converts the prices to decimal, detects falls of at least 1%, and attaches event and fair-price context.

That removes the most failure-prone part of an odds movement alerts API from your application. You do not have to maintain a full previous-price book just to decide whether to < from. You consume derived drops through two focused surfaces:

SurfaceBest useDeliveryFiltering
GET /api/dropsTesting, dashboards, reconnect backfillRecent buffer, newest firstRich query filters
SSE /odds-dropImmediate live alertsPushmin_drop
SSE /odds-drop-prematchImmediate prematch alertsPushmin_drop + recheck

The REST buffer holds roughly three hours, up to the service buffer cap. Store the records you need for longer research. For every raw live and prematch market update rather than derived drops, PinnWire also offers an optional Pinnacle WebSocket API.

The anti-fatigue filter stack

Good alerting is progressive. Apply cheap, objective filters first; add strategy-specific ranking afterward.

1. Separate live and prematch

Use mode=live or mode=prematch. They move at different speeds and deserve different thresholds.

2. Set a minimum move

Start above the 1% detection floor. REST uses min_drop_pct; SSE uses min_drop.

3. Narrow the market

Use markets=moneyline,spread,total,team_total so only relevant market types reach your workflow.

4. Narrow the period

Use periods=0 for full-match lines, or a comma-separated set for the periods your model understands.

5. Enforce freshness

max_age_sec prevents an old REST-buffer item from looking like a new alert after reconnecting.

6. Rank with context

Use nvp, sport, line points, and your own market-liquidity requirements before notifying a person.

Thresholds should differ by workflow

A single percentage across every sport and market usually produces poor results. Live totals can reprice frequently; a prematch full-game moneyline may move less often. Start conservatively, record everything your filters accept, and tune from your own observed distribution.

Use nvp as fair-price context

Every standard PinnWire drop record includes nvp, the no-vig decimal fair price for that outcome. Its reciprocal is the fair implied probability:

fair_probability = 1 / nvp

nvp helps distinguish offered-price movement from the fair estimate embedded in the full market. It is an analytical baseline, not ground truth. Learn more in the no-vig fair odds API guide.

Limits need explicit treatment

Market capacity matters to many strategies, but the current PinnWire drop record does not expose a field named limit. Do not write filtering code that assumes it exists. If a minimum stake capacity is mandatory, enrich the alert from the relevant current market snapshot and inspect its available max value, then apply your threshold. Field availability varies by market, so handle a missing value explicitly.

Cooldown is a routing policy, not detection

PinnWire deduplicates unchanged market versions before drop detection, but genuine successive reprices can still create successive alerts. That is correct market data. Your notification layer can suppress repeated messages with a cooldown keyed by:

event + market + period + outcome + points

Keep storing the accepted raw alerts even when the human notification is cooled down. That preserves the movement path for analysis.

Tune the signal with the REST drops buffer

REST is the fastest way to see whether a filter is too broad. This request asks for recent live soccer moves of 3% to 15%, only full-match moneylines and totals, no older than 90 seconds, capped at 50 rows:

curlpublic demo · REST only
curl "https://pinnwire.com/api/drops?mode=live&sport_id=1&min_drop_pct=3&max_drop_pct=15&markets=moneyline,total&periods=0&max_age_sec=90&live=1&limit=50&key=demo&fresh=alert-test-1"
REST parameterMeaning
modelive by default, or prematch
sport_idOne PinnWire sport ID; omit for all sports
min_drop_pctMinimum percentage drop; default 5%
max_drop_pctOptional ceiling for excluding extreme reprices
marketsCSV of moneyline, spread, total, team_total
periodsCSV period numbers; 0 is the full match
max_age_secOnly rows detected within this many seconds
live=1Exclude records whose event start time is still in the future
limitMaximum returned rows; default 500

The live=1 filter is separate from mode: mode selects the detection buffer, while live=1 rejects a row whose recorded start time is still ahead. On the live buffer it is usually an extra guard.

REST payload names

{
  "total": 1,
  "generated_at": "2026-08-26T10:15:30.000Z",
  "drops": [{
    "event_id": 1629725918,
    "sport_id": 1,
    "sport_name": "Soccer",
    "home": "Home Team",
    "away": "Away Team",
    "league": "Example League",
    "market": "total",
    "period": 0,
    "side": "over",
    "points": 2.5,
    "from": 2.10,
    "to": 1.98,
    "drop_pct": 5.71,
    "nvp": 2.04,
    "is_live": true,
    "alerted": 1787739328,
    "age_s": 2
  }],
  "meta": {
    "mode": "live",
    "events_in_store": 600,
    "tracked_outcomes": 120000
  }
}

The values above are illustrative; names and prices change with the live feed. Check generated_at and each row’s age_s before displaying a result as current.

Move to SSE when the filters are ready

SSE pushes detected drops without polling. Use the live stream for in-play movement and the prematch stream for upcoming fixtures:

live SSE3% or larger
curl -N "https://pinnwire.com/odds-drop?min_drop=3&key=YOUR_KEY"
prematch SSEhold 20 seconds, then confirm
curl -N "https://pinnwire.com/odds-drop-prematch?min_drop=3&recheck=20&key=YOUR_KEY"

min_drop has a 1% floor and defaults to 5%. On prematch only, recheck=N holds a candidate for N seconds, reads the current outcome price again, and emits only if the drop still clears the threshold against the original price. A bounced-back candidate is suppressed. Confirmed records include rechecked_ms. The live endpoint ignores recheck.

SSE payload names are intentionally different

After a connection event, each drop message is a JSON array. SSE uses the stream-oriented names id, sect, outcome, from_price, and to_price. Both SSE and REST already include drop_pct and nvp.

data: {"type":"connected","id":"..."}

data: [{
  "id":1629725918,
  "sport_id":1,
  "sport":"Soccer",
  "home":"Home Team",
  "away":"Away Team",
  "sect":"Total",
  "market_type":"total",
  "period":0,
  "outcome":"over",
  "points":2.5,
  "from_price":2.10,
  "to_price":1.98,
  "drop_pct":5.71,
  "nvp":2.04,
  "is_live":true
}]

Keep-alive comments arrive about every 25 seconds; SSE clients ignore them automatically. Drop streams are available on Stream, Pro + Drops, and Scale. Each key can hold up to five SSE connections, with the oldest closed if a sixth is opened.

Complete Node.js consumer with cooldown and ranking

This consumer relies on PinnWire for server-side detection and minimum-drop filtering. Locally, it applies the market, period, freshness, no-vig, and cooldown rules that are specific to your notification workflow.

terminalNode 18+
npm install eventsource
alerts.mjscomplete example
import { EventSource } from "eventsource";

const key = process.env.PINNWIRE_KEY;
if (!key) throw new Error("Set PINNWIRE_KEY to your PinnWire key");

const MIN_DROP = 3;
const MAX_AGE_SECONDS = 30;
const COOLDOWN_MS = 90_000;
const ALLOWED_MARKETS = new Set(["moneyline", "spread", "total"]);
const ALLOWED_PERIODS = new Set([0]);
const notifiedAt = new Map();

const url = new URL("https://pinnwire.com/odds-drop-prematch");
url.searchParams.set("key", key);
url.searchParams.set("min_drop", String(MIN_DROP));
url.searchParams.set("recheck", "20");

function alertKey(d) {
  return [d.id, d.market_type, d.period, d.outcome, d.points ?? ""].join("|");
}

function fairProbability(d) {
  return Number(d.nvp) > 1 ? 1 / Number(d.nvp) : null;
}

function shouldNotify(d) {
  if (!ALLOWED_MARKETS.has(d.market_type)) return false;
  if (!ALLOWED_PERIODS.has(Number(d.period))) return false;
  if (Number(d.drop_pct) < MIN_DROP) return false;

  // SSE carries epoch seconds in alerted.
  const age = Date.now() / 1000 - Number(d.alerted);
  if (!Number.isFinite(age) || age > MAX_AGE_SECONDS + 20) return false;

  const id = alertKey(d);
  const now = Date.now();
  if (now - (notifiedAt.get(id) ?? 0) < COOLDOWN_MS) return false;
  notifiedAt.set(id, now);
  return true;
}

const stream = new EventSource(url);

stream.onmessage = (message) => {
  const payload = JSON.parse(message.data);
  if (!Array.isArray(payload)) return; // ignores the connected object

  for (const d of payload) {
    if (!shouldNotify(d)) continue;

    const fair = fairProbability(d);
    console.log({
      event: `${d.home} vs ${d.away}`,
      market: `${d.sect} · period ${d.period}`,
      selection: `${d.outcome}${d.points == null ? "" : ` ${d.points}`}`,
      move: `${d.from_price} → ${d.to_price}`,
      drop_pct: d.drop_pct,
      no_vig_fair_price: d.nvp,
      no_vig_fair_probability: fair,
      rechecked_ms: d.rechecked_ms ?? null,
    });
  }
};

stream.onerror = () => {
  // EventSource reconnects automatically. Backfill from /api/drops after a
  // long disconnect if your application cannot tolerate missed alerts.
  console.error("PinnWire SSE disconnected; waiting for reconnect");
};

The example adds 20 seconds to its age allowance because the prematch recheck=20 intentionally delays confirmed delivery. In production, prune old entries from notifiedAt, persist alerts before sending notifications, and route them through a queue if downstream chat, email, or push services can fail.

A production pattern that stays quiet and trustworthy

  1. Measure first. Poll filtered REST data or record the SSE stream for several days before choosing notification thresholds.
  2. Keep live and prematch policies separate. Different movement speeds deserve different minimums, cooldowns, and routes.
  3. Store before notifying. A cooldown should reduce human interruptions without deleting the underlying observation.
  4. Backfill after disconnects. Query /api/drops with a tight max_age_sec, then deduplicate against stored alert keys and timestamps.
  5. Honor freshness. REST includes generated_at and age_s. SSE includes alerted epoch seconds.
  6. Use the correct schema. Normalize REST and SSE into one internal shape at ingestion instead of mixing from with from_price.
  7. Enrich only when needed. Fetch current event markets when your decision requires stake capacity or a full market comparison.
  8. Evaluate outcomes. Track whether moves persisted, reverted, or improved by the close. Tune on evidence from your own use case.
Best fit: PinnWire is especially strong for applications that need a dedicated Pinnacle movement signal, low-overhead server-side drop detection, live and prematch separation, no-vig context, or a REST-to-SSE path from prototype to production. For continuous full-book state rather than alerts, use PinnWire’s optional raw WebSocket.

Test PinnWire’s detected drops now

Use key=demo for one public REST test, or get a personal trial key with 100 requests/day. No card.

Frequently asked questions

What is the best API for Pinnacle odds drop alerts?

PinnWire is purpose-built for this workflow. It detects Pinnacle price decreases server-side, exposes a queryable recent REST buffer, and pushes live or prematch alerts over SSE. Each standard alert includes the prior price, current price, percentage drop, and no-vig fair price.

How do I reduce alert fatigue from betting odds movement?

Start with a meaningful percentage threshold, restrict the sport, market, period, and age, compare the movement with nvp, and apply a cooldown keyed by event, market, period, outcome, and points. For prematch alerts, use PinnWire’s server-side recheck to suppress candidates that bounce back before the hold expires.

What is the difference between PinnWire REST and SSE drop payloads?

REST returns a drops array and uses event_id, market, side, from, and to. Each SSE drop message is itself an array and uses id, sect, outcome, from_price, and to_price. Both surfaces include drop_pct and nvp.

Does PinnWire deduplicate odds drop alerts?

PinnWire rejects unchanged market versions before the drop detector, so identical update noise does not become a new movement. A genuine sequence of lower prices can correctly produce several drops. Add your own per-selection cooldown when several valid market updates should become one human notification.

Does a Pinnacle odds drop guarantee a value bet?

No. It confirms that the decimal price shortened. It does not prove why the market moved or that a price available elsewhere has positive expected value. Treat it as a trigger for a fresh market, fair-value, limits, and execution check.

Can I filter Pinnacle drop alerts by market and period?

Yes through REST: markets accepts moneyline, spread, total, and team_total; periods accepts comma-separated period numbers. SSE filters by min_drop at the server, then your consumer can apply market and period rules locally.

Can I test PinnWire odds drop alerts for free?

Yes. Call /api/drops?key=demo for a public REST test. The demo key has a single shared allowance of 10 requests/minute and 50/day, so it may be busy. A free emailed trial key is better for repeated REST development. SSE requires Stream, Pro + Drops, or Scale.