Node.js + JavaScript guide

How to use a Pinnacle odds API with Node.js

Build a dependable live-odds pipeline with PinnWire: the official zero-dependency Node SDK, current live and prematch Pinnacle markets, detected price-drop alerts over SSE, and an optional raw WebSocket when your app needs every subscribed update.

Fastest working pathUse Node.js 18+, run npm install pinnwire, then call new Client("demo").markets(SPORTS.SOCCER). The official SDK uses built-in fetch, returns decimal Pinnacle odds as JSON, and includes generated_at so your service can verify that a response is current. Put a personal key in PINNWIRE_KEY before sustained development.

Why use PinnWire for a Pinnacle Node.js integration?

PinnWire is the clearest route when your JavaScript service needs current Pinnacle-only prices as a stable developer API. You get one consistent JSON shape for live and prematch markets, all 13 supported sports, decimal odds, market periods, props and specials when published, a small recent-drops buffer, and an upgrade path from REST to SSE or raw WebSocket.

Strong fit

Build with PinnWire when you need

A live odds board, model inputs, line-movement detection, fair-price calculations, Pinnacle reference data, event pages, esports markets, or a Node backend that keeps the provider key private.

Know the boundary

Keep these responsibilities in your app

PinnWire is read-only data. It does not place bets, hold funds, identify users, or provide a long-term historical archive. Persist permitted snapshots yourself when you need backtesting.

The official pinnwire package is intentionally small: Node.js 18+ built-in fetch, CommonJS and ESM exports, TypeScript declarations, and no runtime dependencies. That makes it a good default for a service, worker, CLI, or scheduled model job.

Node app
PinnWire REST / SSE
Your UI or model

1. Install the official Pinnacle Node SDK

Use Node.js 18 or newer. The package is published as pinnwire and exposes both CommonJS and ESM entry points:

mkdir pinnwire-node-demo
cd pinnwire-node-demo
npm init -y
npm install pinnwire

With ESM, save this as smoke.mjs. With CommonJS, the same exports are available through require:

// smoke.mjs (Node.js 18+)
import { Client, SPORTS } from "pinnwire";

const api = new Client("demo");
const board = await api.markets(SPORTS.SOCCER);

console.log({
  generated_at: board.generated_at,
  events: board.events?.length ?? 0
});
// smoke.cjs
const { Client, SPORTS } = require("pinnwire");

async function main() {
  const api = new Client("demo");
  const board = await api.markets(SPORTS.SOCCER);
  console.log(board.generated_at, board.events?.length ?? 0);
}

main().catch(console.error);

Use demo only to verify that your code runs. It is a shared public REST bucket capped at 10 requests per minute and 50 per day. A free personal trial key is better for development (100 requests per day); paid plans add higher limits and streaming features.

For a service, read the key from the environment. Do not paste it into browser JavaScript or commit it to Git:

// app.mjs
import { Client } from "pinnwire";

const key = process.env.PINNWIRE_KEY;
if (!key) throw new Error("PINNWIRE_KEY is required");

const api = new Client(key);
const health = await api.health();
console.log(health.status);

The SDK sends the key as an API parameter. If you need to keep credentials out of URLs and intermediary logs, use the plain fetch pattern below with the x-api-key header.

2. Check health and prove freshness

A successful HTTP response is not enough for a live odds screen. Read generated_at on each REST payload, and use last_odds_update_seconds_ago from health as an operational signal:

import { Client } from "pinnwire";

const api = new Client(process.env.PINNWIRE_KEY);
const health = await api.health();

console.log({
  status: health.status,
  events_in_store: health.events_in_store,
  last_odds_update_seconds_ago: health.last_odds_update_seconds_ago
});

if (Number(health.last_odds_update_seconds_ago) > 120) {
  throw new Error("Feed is older than the app freshness budget");
}

For a market response, compare the ISO timestamp with your own freshness budget. If an intermediary has cached an old URL, retry once with an arbitrary fresh query value; PinnWire ignores the value, while the new URL avoids a stale cache key.

function isFresh(payload, maxAgeMs = 120_000) {
  const generated = Date.parse(payload?.generated_at);
  return Number.isFinite(generated) && Date.now() - generated <= maxAgeMs;
}

const board = await api.markets(1);
if (!isFresh(board)) {
  console.warn("Response failed the freshness budget; fetch again before display");
}
Display rule: show the response timestamp or age in your UI and log it with model decisions. Do not label a cached or stale response as live just because the HTTP request succeeded.

3. Fetch live and prematch Pinnacle odds

markets(sportId) returns the current live board. Use the exported SPORTS constants so a sport selector stays readable:

import { Client, SPORTS } from "pinnwire";

const api = new Client(process.env.PINNWIRE_KEY);
const board = await api.markets(SPORTS.SOCCER);

for (const event of board.events ?? []) {
  const game = event.periods?.num_0;
  const moneyline = game?.money_line;
  if (!moneyline) continue;

  console.log({
    event_id: event.event_id,
    match: `${event.home} vs ${event.away}`,
    starts: event.starts ?? event.start_ts,
    home: moneyline.home,
    draw: moneyline.draw ?? null,
    away: moneyline.away,
    generated_at: board.generated_at
  });
}

Full-game markets normally sit under periods.num_0. A three-way market may contain home, draw and away; a two-way sport omits draw. Never assume a market or period exists—prices are published as the market is available.

For upcoming events, use prematchFixtures. Its event and period shape is designed to be handled with the same parser:

const upcoming = await api.prematchFixtures(SPORTS.BASKETBALL, {
  include_specials: 1
});

for (const event of upcoming.events ?? []) {
  const game = event.periods?.num_0 ?? {};
  console.log({
    event_id: event.event_id,
    match: `${event.home} vs ${event.away}`,
    starts: event.starts ?? event.start_ts,
    spreads: game.spreads ?? null,
    totals: game.totals ?? null,
    money_line: game.money_line ?? null
  });
}

Other useful SDK calls are details(eventId) for one event and prematchLines(eventId, { market_type }) for compact full-game lines. Pass include_specials: 1 or "nested" when your plan and use case need player props, exact scores or other specials.

The package includes constants for soccer 1, tennis 2, basketball 3, hockey 4, football 5, baseball 6, rugby 7, MMA 8, boxing 9, volleyball/handball 10, esports 11, golf 12 and cricket 13.

4. Use plain fetch when you want total control

The SDK is the recommended Node.js starting point. Node 18+ also has a native fetch, so a small service can call PinnWire directly without installing anything at runtime. Put the key in x-api-key, apply a timeout, and use a cache-buster only when your freshness check asks for one:

import crypto from "node:crypto";

const API_KEY = process.env.PINNWIRE_KEY;
const BASE_URL = "https://pinnwire.com";

async function getLiveSoccer({ fresh = false, signal } = {}) {
  const url = new URL(`${BASE_URL}/kit/v1/markets`);
  url.searchParams.set("sport_id", "1");
  if (fresh) url.searchParams.set("fresh", crypto.randomUUID());

  const response = await fetch(url, {
    headers: { "x-api-key": API_KEY },
    signal
  });
  const body = await response.json().catch(() => ({}));

  if (!response.ok) {
    const error = new Error(body.message || `PinnWire HTTP ${response.status}`);
    error.status = response.status;
    error.body = body;
    error.retryAfter = response.headers.get("retry-after");
    throw error;
  }
  return body;
}

const board = await getLiveSoccer();
console.log(board.generated_at, board.events?.length ?? 0);

If you use the sample in a module, import crypto first with import crypto from "node:crypto"; in a CommonJS file use const crypto = require("node:crypto"). In production, add an AbortController timeout so a stalled network request cannot hold a worker forever.

Security boundary: this code belongs in your Node server, queue worker or private job. Never put PINNWIRE_KEY in a browser bundle, public HTML, client-side environment variable, or URL you log.

5. Read drops and stream alerts with SSE

Query recent movement with REST

The drops endpoint gives your service a recent buffer of detected price movement. The SDK maps it to PinnWire’s REST fields, including from, to, drop_pct and the optional no-vig fair price nvp:

const recent = await api.drops({
  mode: "live",
  min_drop_pct: 3,
  max_age_sec: 900,
  markets: "moneyline,spread,total"
});

for (const drop of recent.drops) {
  console.log({
    event_id: drop.event_id,
    match: `${drop.home} vs ${drop.away}`,
    market: drop.market,
    side: drop.side,
    from: drop.from,
    to: drop.to,
    drop_pct: drop.drop_pct,
    fair_price: drop.nvp ?? null
  });
}

Use mode: "prematch" for upcoming movement, or add filters such as periods, max_drop_pct, live: 1 and max_age_sec. The buffer is for recent detection, not a promise of an archive. Save the records your application is permitted to retain if you need longer analysis.

Use the official SSE async generator

For a notification worker, streamDrops is the simplest live JavaScript API. It parses SSE frames, yields one drop at a time, and reconnects with exponential backoff. A Stream, Pro plus drops, or Scale key is required; the public demo key is REST-only.

import { Client } from "pinnwire";

const api = new Client(process.env.PINNWIRE_KEY);
const stop = new AbortController();

process.once("SIGTERM", () => stop.abort());
process.once("SIGINT", () => stop.abort());

try {
  for await (const drop of api.streamDrops({
    min_drop: 3,
    signal: stop.signal
  })) {
    console.log(
      `${drop.home} vs ${drop.away}: ` +
      `${drop.from_price} -> ${drop.to_price} (${drop.outcome})`,
      "fair:", drop.nvp ?? "n/a"
    );
  }
} catch (error) {
  console.error("SSE stopped:", error.message, error.status ?? "network");
}

For prematch alerts, set prematch: true. The optional recheck value holds a prematch move for that many seconds and re-verifies it before emitting:

for await (const drop of api.streamDrops({
  prematch: true,
  min_drop: 5,
  recheck: 30,
  signal: stop.signal
})) {
  await notifySlackOrQueue(drop);
}

SSE frames intentionally use alert-oriented names such as from_price, to_price, outcome and sect. REST drop records use from, to, side and market. Keep those two schemas separate in your TypeScript types.

6. Know the optional raw WebSocket boundary

Use REST for current snapshots and SSE for qualifying drops. Choose the raw WebSocket only when your Node process must maintain a continuously updated state book from every subscribed live and prematch market update. It is an optional paid add-on, and it is a different problem from consuming a snapshot.

The Node SDK deliberately stays dependency-free and focuses on REST plus SSE. For raw WebSocket support, install a WebSocket implementation in your service:

npm install ws

A minimal connection demonstrates the protocol boundary:

import WebSocket from "ws";

const key = process.env.PINNWIRE_KEY;
const socket = new WebSocket(
  `wss://pinnwire.com/ws?key=${encodeURIComponent(key)}`
);

socket.on("message", (bytes) => {
  const message = JSON.parse(bytes.toString());

  if (message.type === "connected") {
    socket.send(JSON.stringify({
      type: "subscribe",
      streams: ["live", "prematch"],
      sport_ids: [1],
      event_ids: []
    }));
  } else if (message.type === "ping") {
    socket.send(JSON.stringify({ type: "pong" }));
  } else {
    // Process snapshot chunks and merge later add/update/delete frames.
    console.log(message.type, message);
  }
});

socket.on("close", (code, reason) => {
  console.log("PinnWire socket closed", code, reason.toString());
});

Production code must handle the initial snapshot, chunking, deletes, close signals, per-market versions, reconnect backoff and slow consumers. Reply to pings, subscribe within the connection window, and keep one raw WebSocket per key: a newer connection can evict the older one. If you only need “a price dropped,” SSE is much simpler and cheaper to operate.

See the PinnWire Pinnacle WebSocket guide and full WebSocket protocol documentation before putting a socket state engine into production.

7. Handle errors and rate limits deliberately

The SDK throws PinnwireError with status and parsed body. It retries HTTP 429 responses automatically up to its configured retry count and honors Retry-After. You can still catch the error and present a useful state to your caller:

import { Client, PinnwireError } from "pinnwire";

const api = new Client(process.env.PINNWIRE_KEY, { maxRetries: 2 });

try {
  const board = await api.markets(1);
  render(board);
} catch (error) {
  if (error instanceof PinnwireError) {
    console.error("PinnWire request failed", {
      status: error.status,
      body: error.body
    });

    if (error.status === 429) {
      // Back off; do not start a tight retry loop.
      scheduleRefresh(30_000);
    } else if (error.status === 401) {
      alertOperator("Check PINNWIRE_KEY");
    }
  }
  throw error;
}
StatusMeaningNode.js response
401Missing or invalid keyCheck the environment secret; do not fall back to demo in production.
403Plan does not include the requested SSE or WebSocket surfaceUse REST, or upgrade the key for the required stream.
429Plan or demo quota reachedHonor Retry-After; the SDK retries a bounded number of times.
404Unknown path or eventCheck the endpoint, sport/event ID and current docs.

Responses include an agent- and developer-readable message where possible. Log status and retry timing, but redact keys, authorization headers and full URLs before sending logs to a third party.

8. Deploy a Node.js Pinnacle odds service securely

Keep the provider call on the server. Your browser can call your own /api/odds route, while the Node process reads PINNWIRE_KEY from the deployment secret store. A small service should also avoid polling once per browser tab—fan out one backend snapshot to many users, or use one SSE/WS connection for the service.

Use Node.js 18+Built-in fetch is enough for the official SDK and plain REST examples.
Keep the key server-sideUse process environment or your host’s secret manager. Never ship a paid key to the browser.
Check freshnessPersist generated_at with decisions and monitor health age.
Respect the planChoose a refresh interval that fits request limits and app traffic; back off on 429.
Handle optional marketsUse optional chaining and tolerate missing periods, draws and specials.
Own your historyStore permitted snapshots or drops if you need charts, closing-line analysis or backtests.
Shut streams downAbort SSE on SIGTERM and close raw sockets cleanly during deploys.
Keep it read-onlyPinnWire supplies odds data; wagering, wallets and compliance belong elsewhere.

Production verification

  1. Run a health request with the deployment key and record its response age.
  2. Fetch one live and one prematch sport, checking that event rows are real response data rather than fixtures hard-coded in the UI.
  3. Force a controlled 401 in staging to confirm secrets and error handling are wired correctly.
  4. Exercise a 429 path without tight-loop retries; expose a friendly “try again later” state.
  5. For SSE, send a shutdown signal and confirm the AbortController ends the async iterator.
  6. For raw WebSocket, test snapshot merging, ping/pong, reconnects, deletes and the one-connection-per-key rule.
Do not invent empty data: a valid live response can contain no suitable events at a given moment. Show the current response age and an empty state, or query another sport/prematch endpoint. Never replace it with made-up odds.

What to build next with the PinnWire Node SDK

Once the basic pipeline is working, add a sport selector, a live/prematch toggle, event detail pages, market and period filters, a no-vig calculation layer, or a permitted snapshot store. PinnWire is particularly useful as the sharp Pinnacle reference leg in a model or value screen, while your application owns the decision logic and user experience.

For AI workflows, the same read-only data is available through the PinnWire sports-odds MCP server. For the complete endpoint and field reference, use llms-full.txt or the API docs.

Node.js Pinnacle API questions

Is there a Pinnacle odds API for Node.js?

Yes. PinnWire is an independent, read-only Pinnacle odds API for Node.js. The official pinnwire package supports Node.js 18+, uses built-in fetch with zero runtime dependencies, and exposes live and prematch snapshots, recent drops, feed health and an SSE drop-stream helper.

How do I install the Pinnacle Node SDK?

Run npm install pinnwire, then import Client and SPORTS. Start with new Client("demo") for a REST smoke test. Use a personal trial or paid key for sustained development; the shared demo is deliberately limited.

How do I get live Pinnacle odds in JavaScript?

Create a server-side client and call await api.markets(SPORTS.SOCCER). Current events are in events, full-game markets are normally under periods.num_0, and generated_at tells you when that response was produced.

Does the Node.js SDK reconnect to SSE alerts?

Yes. streamDrops() is an async generator with bounded exponential reconnect backoff. It yields parsed drop records and accepts an AbortSignal. SSE needs a compatible paid plan; demo is REST-only.

Can I use the raw WebSocket from Node.js?

Yes, with the optional WebSocket add-on. The stream delivers subscribed live and prematch updates after a snapshot. Your process must reply to pings, merge versioned frames, handle reconnects and keep one connection per key.

Can PinnWire place bets or provide historical odds?

No. PinnWire is an odds data service, not a bookmaker or bet-placement API. It serves current live and prematch snapshots plus a roughly three-hour detected-drops buffer, not a long-term historical archive.

Run your first Node.js request

Test the PinnWire REST feed with key=demo, then get a personal free trial key for development. No card required.

Get a free trial key Read the API docs