GroupMe MLB Scores bot Requested

MLB Scores GroupMe Bot (Google Apps Script)

A Google Apps Script that listens for commands in your GroupMe group and posts today’s MLB scores back to the chat. Pulls live data from the free MLB StatsAPI (statsapi.mlb.com) — no API key needed.

Triggers on any of these commands (case-insensitive, with or without / or ! prefix):

  • scores / score
  • mlb
  • games / game

Examples that all work: scores, /scores, !mlb, mlb, Games.


What you need before starting

  1. A Google account (for Apps Script)
  2. A GroupMe account, and the group you want the bot in
  3. Your GroupMe Bot ID (the script is preconfigured with a1ce15473da547ab1940778f53double-check this is your full Bot ID, since standard GroupMe bot IDs are 32 hex characters and yours appears shorter; if so, replace it in Code.gs)

Step 1 — Register the GroupMe bot

If you already created the bot and have the Bot ID, skip to Step 2.

  1. Go to GroupMe Developers
  2. Sign in with your GroupMe account
  3. Click Create Bot
  4. Pick the group you want the bot in
  5. Give it a name (e.g. MLB Scores)
  6. Leave the Callback URL blank for now — we’ll come back and fill it in once the Apps Script is deployed
  7. Click Submit
  8. Copy the Bot ID that GroupMe shows you — you’ll paste it into the script

Step 2 — Create the Apps Script project

  1. Go to https://script.google.com
  2. Click New project
  3. Rename the project (top-left, where it says “Untitled project”) to something like MLB GroupMe Bot
  4. Delete the default function myFunction() {} placeholder in Code.gs
  5. Copy this code below
Code.gs
const GROUPME_BOT_ID = 'a1ce15473da547ab1940778f53';
const GROUPME_POST_URL = 'https://api.groupme.com/v3/bots/post';
const MLB_SCHEDULE_URL = 'https://statsapi.mlb.com/api/v1/schedule';
const COMMAND_PATTERN = /^(\/|!)?\s*(scores?|mlb|games?)\b/i;

function doPost(e) {
  try {
    const body = JSON.parse(e.postData.contents);

    if (body.sender_type !== 'user') {
      return ContentService.createTextOutput('');
    }

    const text = (body.text || '').trim();
    if (!COMMAND_PATTERN.test(text)) {
      return ContentService.createTextOutput('');
    }

    const message = buildScoresMessage();
    postToGroupMe(message);
  } catch (err) {
    console.error('doPost error: ' + err);
  }
  return ContentService.createTextOutput('');
}

function buildScoresMessage() {
  const tz = Session.getScriptTimeZone();
  const date = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd');
  const url = `${MLB_SCHEDULE_URL}?sportId=1&date=${date}&hydrate=linescore,team`;

  const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  if (response.getResponseCode() !== 200) {
    return 'MLB scores unavailable right now.';
  }

  const data = JSON.parse(response.getContentText());
  const games = (data.dates && data.dates[0] && data.dates[0].games) || [];

  if (games.length === 0) {
    return `No MLB games scheduled for ${date}.`;
  }

  const lines = games.map(formatGame);
  return `MLB — ${date}\n` + lines.join('\n');
}

function formatGame(g) {
  const away = g.teams.away;
  const home = g.teams.home;
  const status = g.status.detailedState;
  const awayName = (away.team && (away.team.abbreviation || away.team.teamName || away.team.name)) || 'Away';
  const homeName = (home.team && (home.team.abbreviation || home.team.teamName || home.team.name)) || 'Home';

  const preGameStates = ['Scheduled', 'Pre-Game', 'Warmup', 'Delayed Start'];
  if (preGameStates.indexOf(status) !== -1) {
    const tz = Session.getScriptTimeZone();
    const start = Utilities.formatDate(new Date(g.gameDate), tz, 'h:mm a');
    return `${awayName} @ ${homeName} — ${start}`;
  }

  const a = (away.score != null) ? away.score : 0;
  const h = (home.score != null) ? home.score : 0;
  let tag = status;
  if (status === 'Final') tag = 'F';
  else if (status === 'In Progress' && g.linescore && g.linescore.currentInningOrdinal) {
    const half = g.linescore.inningHalf === 'Top' ? '▲' : '▼';
    tag = `${half} ${g.linescore.currentInningOrdinal}`;
  }
  return `${awayName} ${a} @ ${homeName} ${h} (${tag})`;
}

function postToGroupMe(text) {
  const chunks = chunkText(text, 950);
  for (let i = 0; i < chunks.length; i++) {
    UrlFetchApp.fetch(GROUPME_POST_URL, {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify({ bot_id: GROUPME_BOT_ID, text: chunks[i] }),
      muteHttpExceptions: true
    });
  }
}

function chunkText(text, max) {
  if (text.length <= max) return [text];
  const out = [];
  let cur = '';
  const lines = text.split('\n');
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const candidate = cur ? cur + '\n' + line : line;
    if (candidate.length > max) {
      if (cur) out.push(cur);
      cur = line;
    } else {
      cur = candidate;
    }
  }
  if (cur) out.push(cur);
  return out;
}

function testPost() {
  postToGroupMe(buildScoresMessage());
}
  1. At the top of the file, confirm GROUPME_BOT_ID matches the Bot ID GroupMe gave you. If it doesn’t match, replace it.
  2. Click the Save icon (or Ctrl+S)

Step 3 — Test the script before deploying

Quick sanity check that the script can reach both APIs:

  1. In the Apps Script editor, in the function dropdown at the top, select testPost
  2. Click Run
  3. The first time you run it, Google will prompt you to authorize the script:
    • Click Review permissions
    • Pick your Google account
    • You’ll see a “Google hasn’t verified this app” warning — this is normal for a personal Apps Script. Click AdvancedGo to [your project name] (unsafe)Allow
  4. Check your GroupMe group — you should see the bot post today’s scores (or a “No MLB games scheduled” message if it’s the offseason / off day)

If the post lands in GroupMe, you’re good. If not, check the Execution log at the bottom of the Apps Script editor for errors.


Step 4 — Deploy as a Web App

This gives the script a public URL that GroupMe can call when someone posts a message.

  1. In the Apps Script editor, click Deploy (top-right) → New deployment
  2. Click the gear icon next to “Select type” → choose Web app
  3. Fill in:
    • Description: MLB scores bot v1 (anything you want)
    • Execute as: Me (your-email@gmail.com)
    • Who has access: Anyonemust be “Anyone”, otherwise GroupMe can’t reach it
  4. Click Deploy
  5. Authorize again if prompted (same flow as Step 3)
  6. Copy the Web app URL that ends in /exec — you’ll need this in the next step

Anytime you change Code.gs later, you must redeploy: Deploy → Manage deployments → pencil icon → Version: New version → Deploy. Re-using “Manage deployments” keeps the same URL so you don’t have to update the GroupMe bot.


Step 5 — Connect the bot’s callback URL

  1. Go back to GroupMe Developers
  2. Click your bot’s name to edit it
  3. Paste the Apps Script Web app URL (from Step 4) into the Callback URL field
  4. Click Submit

GroupMe will now POST every group message to your Apps Script. The script ignores everything that isn’t a recognized command.


Step 6 — Try it in your group

In your GroupMe group, type:

scores

The bot should reply within a few seconds with today’s scores formatted like:

MLB — 2026-04-24
NYY 5 @ BOS 3 (F)
LAD 2 @ SF 4 (▼ 7th)
HOU @ SEA — 9:40 PM

Customization tips

  • Change the trigger words: edit COMMAND_PATTERN at the top of Code.gs. It’s a regex — current pattern matches scores, score, mlb, games, game with optional / or ! prefix.
  • Change time zone for game-time display: in the Apps Script editor, click the gear icon (Project Settings) → set the Time zone to your local zone. The script reads it via Session.getScriptTimeZone().
  • Show only specific teams: inside buildScoresMessage(), filter games before mapping — e.g. games.filter(g => ['NYY','BOS'].includes(g.teams.home.team.abbreviation) || ['NYY','BOS'].includes(g.teams.away.team.abbreviation))
  • Add more commands later (standings, schedule, individual team): add another endpoint from the StatsAPI docs (https://statsapi.mlb.com/docs/) and route via a second regex match in doPost.

Troubleshooting

Bot doesn’t reply in the group

  • Confirm the callback URL in GroupMe Developers ends in /exec (not /dev)
  • Open Apps Script → Executions (left sidebar, clock icon) — you should see a doPost entry every time someone posts in the group. If not, GroupMe isn’t reaching the script.
  • Check that Who has access on the deployment is Anyone, not Anyone with Google account

testPost works but commands in the group don’t

  • The deployment URL and the bot’s callback URL don’t match — copy it again
  • Check the message text actually matches COMMAND_PATTERN

Posts as the wrong bot / nothing posts

  • Confirm GROUPME_BOT_ID in Code.gs matches exactly what GroupMe Developers shows for your bot
  • Check Apps Script Executions logs — look for HTTP errors from api.groupme.com

“Authorization required” loop

  • After editing the script, you sometimes need to re-run testPost once from the editor to re-trigger the consent dialog before redeploying

Script stops responding after a long quiet period

  • Apps Script web apps are stateless and serverless — they don’t sleep. If it stops, check Executions logs for quota errors (UrlFetchApp has a daily quota; consumer Google accounts get 20,000 calls/day, well above what a chat bot needs)

Credits

Claude Code Note: I didn’t have time to review this yet, was designed on request

it works thank you

1 Like

Is it live scores?

@Jay

you have to text it scores then yes

1 Like