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/scoremlbgames/game
Examples that all work: scores, /scores, !mlb, mlb, Games.
What you need before starting
- A Google account (for Apps Script)
- A GroupMe account, and the group you want the bot in
- Your GroupMe Bot ID (the script is preconfigured with
a1ce15473da547ab1940778f53— double-check this is your full Bot ID, since standard GroupMe bot IDs are 32 hex characters and yours appears shorter; if so, replace it inCode.gs)
Step 1 — Register the GroupMe bot
If you already created the bot and have the Bot ID, skip to Step 2.
- Go to GroupMe Developers
- Sign in with your GroupMe account
- Click Create Bot
- Pick the group you want the bot in
- Give it a name (e.g.
MLB Scores) - Leave the Callback URL blank for now — we’ll come back and fill it in once the Apps Script is deployed
- Click Submit
- Copy the Bot ID that GroupMe shows you — you’ll paste it into the script
Step 2 — Create the Apps Script project
- Go to https://script.google.com
- Click New project
- Rename the project (top-left, where it says “Untitled project”) to something like
MLB GroupMe Bot - Delete the default
function myFunction() {}placeholder inCode.gs - 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());
}
- At the top of the file, confirm
GROUPME_BOT_IDmatches the Bot ID GroupMe gave you. If it doesn’t match, replace it. - Click the Save icon (or
Ctrl+S)
Step 3 — Test the script before deploying
Quick sanity check that the script can reach both APIs:
- In the Apps Script editor, in the function dropdown at the top, select
testPost - Click Run
- 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 Advanced → Go to [your project name] (unsafe) → Allow
- 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.
- In the Apps Script editor, click Deploy (top-right) → New deployment
- Click the gear icon next to “Select type” → choose Web app
- Fill in:
- Description:
MLB scores bot v1(anything you want) - Execute as:
Me (your-email@gmail.com) - Who has access:
Anyone← must be “Anyone”, otherwise GroupMe can’t reach it
- Description:
- Click Deploy
- Authorize again if prompted (same flow as Step 3)
- Copy the Web app URL that ends in
/exec— you’ll need this in the next step
Anytime you change
Code.gslater, 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
- Go back to GroupMe Developers
- Click your bot’s name to edit it
- Paste the Apps Script Web app URL (from Step 4) into the Callback URL field
- 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_PATTERNat the top ofCode.gs. It’s a regex — current pattern matchesscores,score,mlb,games,gamewith 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(), filtergamesbefore 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
doPostentry 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, notAnyone 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_IDinCode.gsmatches 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
testPostonce 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