Making a GroupMe bot that can mention everyone

GroupMe had no @everyone, so I built one. It started in Java, shipped on Express and Heroku, and now runs on Hono, Cloudflare Workers and D1.

A few years back, a big part of my church’s community chose GroupMe as our de facto messaging app for all things church-related. In hindsight, that was a horrible idea given GroupMe’s lack of functionality. WhatsApp or Telegram probably would’ve been better, but by then we’d reached a point where we stuck with it.

There was one missing feature that especially bugged me: @Everyone. Having to mention every user in a group manually was very frustrating. Why?! It was even more painful in large groups with a few spammy members, because most members would end up muting the group and couldn’t be reached without @all or some other way to notify them.

I wondered whether GroupMe was at all extensible through plugins or something similar, which led me down the path of GroupMe bots. I found a few that were okay, but most cost money and didn’t do exactly what I wanted. After all, how hard could it be to throw together a bot that does one thing, and one thing only?

Project Requirements

These are the requirements I set for myself:

  1. As an admin of a GroupMe group, I should be able to @all in the chat.
  2. Ordinary users shouldn’t be able to use that feature, to avoid notification exhaustion and spam.

The GroupMe bot ecosystem

A bot in GroupMe is not a user. You register it to a single group and get back a bot_id, which lets you post messages into that group. That’s all it can do:

POST https://api.groupme.com/v3/bots/post
{ "bot_id": "...", "text": "hello" }

You also give the bot a callback_url at registration time. From then on, GroupMe POSTs every message in that group to your endpoint. That means you need a server that’s always listening, and it receives every message in the group, not just the ones meant for the bot.

How it started

As a college student in 2016, I was most comfortable with Java, so I built the first version in Java on Heroku. The first commit, in April 2017, has Bot.java, Parser.java and Server.java, with the compiled .class files committed right next to them. That version never went anywhere.

Two years later I started over in Node. That second pass was a single Express file with one route, /post, and I used it for two completely different jobs:

  1. The OAuth redirect that comes back with a user’s access token
  2. The message callback that GroupMe fires on every message

Mixing them was a mistake, and I left myself a TODO about it at the bottom of the file. The version that shipped followed it:

Separate the whole Web part from the callback url (Callback URL is only for messages nothing else)

GroupMe sends every message in the group to the callback URL, so that endpoint should only handle messages. Anything stateful (e.g. logging a user in or registering a group) belongs somewhere else.

Reading the roster

To mention everyone, you need the list of members, and a bot_id has no way to get it. Reading the member roster is a different call, and it needs a user access token:

GET https://api.groupme.com/v3/groups/:group_id?token=:access_token

So the bot can’t be self-contained. Somebody has to log in with GroupMe OAuth, and we hold onto that token to look up members later. That means an OAuth flow, a place to store { groupId, botId, accessToken } per group, and, since we’re storing credentials that act on someone’s behalf, a terms page and a privacy policy.

Mentions

The mention itself is an attachment on the outgoing message:

{
  bot_id: bot.botId,
  text: 'Mentioning Everyone',
  attachments: [{ type: 'mentions', user_ids: members }]
}

members is every user_id from the group lookup. The text is what people see, and the attachment is what sends everyone a notification.

A GroupMe chat where an admin sends "@all" and the @Everyone bot replies "Mentioning Everyone" a minute later

The trigger is an exact match on the message text:

isMentioningEveryone (msg) {
  const cleanStr = msg.trim().toLowerCase()
  return cleanStr === '@all'
}

GroupMe sends the bot’s own reply back to the callback too. Because the reply says “Mentioning Everyone” rather than @all, it doesn’t match and the bot doesn’t trigger itself, but that was luck: a reply of @all would have kept triggering the bot until GroupMe rate-limited it. The original never checked sender_type on the incoming payload to ignore messages from bots. The current version does.

Who can use it

Once mentions worked, the next question was who should be allowed to send them.

A 200-person church group where anyone can ping everyone sounds like a headache. So the check happens server-side, against the roster fetched on that request, on every message:

const messagingUser = groupDetails.response.members
  .find(member => member.user_id === userId)

if (messagingUser &&
    (messagingUser.roles.includes('admin') ||
     messagingUser.roles.includes('owner'))) {
  // ...send the mention
}

This check is enforced on the callback, not just in the UI. The onboarding page only lists groups where you’re an admin or owner, but that can change later, since you can be promoted or demoted. We re-read the roster on every message rather than caching it, so a role change on GroupMe’s side applies to the very next @all.

What it ended up being

By the end it was a small web app that installs the bot into any group. You log in with GroupMe OAuth and get a list of the groups you own or admin, with a terms checkbox. Installing calls POST /v3/bots with your group_id and my callback_url, then saves the resulting bot_id. From then on, one callback endpoint looks up the members, checks the sender’s role and sends the mention. Uninstalling calls POST /v3/bots/destroy and deletes the record.

The first version that shipped used Express and MongoDB on Heroku, with a three-page Bootstrap 3 and jQuery frontend served as static files.

Where it lives now

When Heroku ended free dynos in November 2022, the original deployment went with it. In 2024 I cleaned up the codebase and added tests and CI, and in 2026 I ported it to Hono on Cloudflare Workers, with D1 in place of MongoDB. It lives at ateveryonebot.abanobmikaeel.workers.dev and works the same way it always did: an owner or admin types @all, and everyone in the group gets pinged.

The port also fixed a few gaps in the original. GroupMe doesn’t sign bot callbacks, so the bot now confirms each message through GroupMe’s API before acting on it, ignores anything not sent by a user, and records message IDs so a replayed callback can’t fire twice.

Looking back

As one of my first toy projects, it was great to build something from scratch, put it out into the world, and see people in my church actually use it.

Building this back in 2016 with Java, or even in 2018 with Node.js, felt like pure euphoria once it was done. With LLMs and Claude Code in 2026, it’s much simpler and quicker to put something like this together within an hour. Perhaps part of that is also the 10 years of building experience I’ve gained in between.

← All writing