On the wire · 14:06:49 UTC Live 65 · Prematch 8,088 · Markets 32,770 Vol. I · No. 1 · Monday 9 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Guide · Google Sheets · Apps Script

Pull Pinnacle odds into Google Sheets

Updated · PinnFeed · 5 min read

This guide writes Pinnacle soccer moneylines into a Google Sheet: one row per match, with decimal prices, no-vig fair prices and the market margin. A time trigger refreshes the tab every 15 minutes. The script uses the PinnFeed REST API, so it runs on the Free plan (€0, 30-second delay, 1,000 requests/day).

Step 1 — Get a Free key#

  1. Create an account at pinnfeed.io and open the dashboard.
  2. Create a key and copy it. Keys look like pf_live_….
  3. On Free, pick up to two sports in the dashboard. This script defaults to soccer; change SPORT if you chose something else.

You do not need Builder for Sheets. Builder (€99/month, the WebSocket plan) is for real-time push clients such as the Python and Node guide.

Step 2 — Create the spreadsheet#

  1. Create a new Google Sheet.
  2. Rename the first tab to Odds (exact name; the script looks it up).
  3. Open Extensions → Apps Script. Delete the stub myFunction and leave the project empty for the next step.

Step 3 — Paste the script and set properties#

Paste this into Code.gs:

// Google Apps Script: Extensions → Apps Script, paste this file.
// Script properties (Project Settings → Script properties):
//   PINNFEED_BASE = https://pinnfeed.io
//   PINNFEED_KEY  = pf_live_...
// Add a sheet tab named "Odds", run refreshOdds once, then add a 15-minute trigger.

const PROPS = PropertiesService.getScriptProperties();
const BASE = PROPS.getProperty('PINNFEED_BASE') || 'https://pinnfeed.io';
const KEY = PROPS.getProperty('PINNFEED_KEY');
const SPORT = 'soccer'; // change to basketball, tennis, … (must be in your plan)

function refreshOdds() {
  if (!KEY) {
    console.error('Set script property PINNFEED_KEY first');
    return;
  }
  const url = BASE + '/v1/odds'
    + '?sport=' + encodeURIComponent(SPORT)
    + '&market=moneyline&main=true&live=false&limit=500';
  const res = UrlFetchApp.fetch(url, {
    headers: { 'x-api-key': KEY },
    muteHttpExceptions: true,
  });
  const code = res.getResponseCode();
  if (code !== 200) {
    console.warn('PinnFeed HTTP ' + code + ': ' + res.getContentText().slice(0, 300));
    return; // leave the tab alone on a bad minute
  }
  const data = JSON.parse(res.getContentText());
  const rows = [];
  for (const ev of data.events || []) {
    const ml = (ev.markets || []).find((m) => m.type === 'moneyline' && m.main);
    if (!ml) continue;
    const byName = {};
    for (const o of ml.outcomes || []) byName[o.name] = o;
    const home = byName.home || {};
    const draw = byName.draw || {};
    const away = byName.away || {};
    rows.push([
      ev.startTime ? new Date(ev.startTime) : '',
      (ev.league && ev.league.name) || '',
      ev.home || '',
      ev.away || '',
      home.decimal || '',
      draw.decimal || '',
      away.decimal || '',
      home.fair || '',
      draw.fair || '',
      away.fair || '',
      ml.margin != null ? ml.margin : '',
      ev.id,
    ]);
  }
  const sheet = SpreadsheetApp.getActive().getSheetByName('Odds');
  if (!sheet) {
    console.error('Create a tab named Odds first');
    return;
  }
  sheet.clearContents();
  sheet.appendRow([
    'Starts', 'League', 'Home', 'Away',
    '1', 'X', '2',
    'Fair 1', 'Fair X', 'Fair 2',
    'Margin', 'Event id',
  ]);
  if (rows.length) {
    sheet.getRange(2, 1, rows.length, 12).setValues(rows);
  }
  console.log('Wrote ' + rows.length + ' moneyline rows for ' + SPORT);
}

Then open the project gear (Project Settings) and under Script properties add:

PropertyValue
PINNFEED_KEYyour key (pf_live_…)
PINNFEED_BASEhttps://pinnfeed.io (optional; that is the default)

Never paste the key into the spreadsheet cells or share the Apps Script project publicly.

Step 4 — Run it once and authorize#

  1. Select refreshOdds in the function dropdown and click Run.
  2. Google asks for permission to connect to an external service and edit the sheet. Approve it for your account.
  3. Reload the spreadsheet. The Odds tab should show columns Starts, League, Home, Away, 1, X, 2, the three fair prices, Margin and Event id.

A failed call (bad key, rate limit, sport not in plan) logs a warning and leaves the previous rows alone, so a bad minute never blanks the board.

Step 5 — Add a 15-minute trigger#

  1. In Apps Script, open Triggers (clock icon) → Add Trigger.
  2. Function: refreshOdds. Event source: Time-driven. Type: Minutes timer. Interval: Every 15 minutes.
  3. Save.

That is 96 calls a day for one sport, well inside the Free plan's 1,000 requests/day and 1 req/s. Two sports every 30 minutes is also 96. See plans and rate limits.

TriggerCalls / dayFits Free?
Every 15 min, 1 sport96yes
Every 30 min, 2 sports96yes
Every 15 min, 2 sports192yes
Every 1 min, 1 sport1,440no — needs Hobby or above (no daily quota)

Apps Script has its own quotas on top; the shortest time trigger is one minute.

Step 6 — Read fair prices and margins#

Each moneyline row carries PinnFeed's no-vig fields from the schema:

  • Fair 1 / Fair X / Fair 2 — the fair decimal price on each outcome (margin removed, multiplicative method).
  • Margin — the market's margin (overround = sum of implied probabilities − 1).

Use those columns when you compare a soft-book price in another tab: expected value is soft × (1 / fair) − 1, the same math as the value betting guide. For live steam moves, Sheets is the wrong tool; use the WebSocket on Builder and the steam detection guide.

Questions people ask#

Which PinnFeed plan do I need?#

Free is enough for a delayed board refreshed every few minutes. Hobby (€29/month) removes the daily quota and the 30-second delay but is still REST-only. Real-time push needs Builder (€99/month).

Why does the script use x-api-key instead of a query string?#

That is the preferred auth header in the docs. Apps Script's UrlFetchApp can set headers, so there is no reason to put the key in the URL (where it can leak into logs).

Can I chart price history?#

Yes. Keep the Odds tab as a live board (cleared and rewritten each run). Add a second tab that only appends rows after each refresh, then insert a line chart. Do not clear the history tab.

Is this educational content for adults?#

Yes. Odds data and spreadsheet tooling are educational and for adults (18+). Nothing here is a tip, a sure bet or a promise of profit.

The WebSocket plan: Builder, €99/mo.

Real-time Pinnacle odds pushed over one WebSocket, all 13 sports, every market, no-vig fair odds and closing lines. 3 days free, cancel in one click.

Form K-1 · Free API key

Get your free API key.

No password. We email you a link; click it and your key is on screen.

One free key per email. Link valid for 20 minutes.