Integrate GamePigeon iMessage games into your app in a few lines of TypeScript.
Read the board as plain data. Send moves as a URL. Works over Linq & Photon — no AI, no SQLite, no Mac.
![]() 8-Ball — sent via API, rendered natively by GamePigeon |
![]() Cup Pong — the recipient's own GamePigeon draws the board |
liveLayoutInfo).
GamePigeon is a black box: games ride iMessage as encrypted imessage_app balloons.
OpenPigeon reverse-engineers that wire format so your bot can understand a game
and respond to it — turning an opaque balloon into a plain object and back:
import * as op from "openpigeon";
const m = op.read(url); // decode any GamePigeon URL
m.game; // "connect4"
m.turn; // 2 (whose turn is next)
m.state; // { grid: [...], move: {...} } ← the board, as data
const reply = m.reply({ botId: "MY-ID", state: m.state }); // your move → a URL
op.invite("sea_battle"); // start a fresh game → { url, id }No cipher, no percent-encoding, no MSMessage plumbing — the SDK handles all of it.
npm install openpigeon| Linq (webhook) | Photon (stream) |
|---|---|
import express from "express";
import * as op from "openpigeon";
const linq = new op.Linq({
token: process.env.LINQ_TOKEN!,
fromNumber: "+1...",
});
app.post("/webhook", async (req, res) => {
res.sendStatus(200);
const inb = op.fromWebhook(req.body);
if (!inb || inb.move.isInvite) return;
const m = inb.move;
// your move logic here
const url = m.reply({ botId: BOT, state: m.state });
await linq.send(url, {
to: inb.replyTo!, chatId: inb.chatId ?? undefined,
});
}); |
import { customizedMiniApp } from "spectrum-ts/providers/imessage";
import * as op from "openpigeon";
for await (const [space, message] of app.messages) {
const inb = op.fromPhotonMessage(message);
if (!inb || inb.move.isInvite) continue;
const m = inb.move;
// your move logic here
const url = m.reply({
botId: BOT, state: m.state, https: true, // https carrier
});
await space.send(
customizedMiniApp(op.Photon.balloon(url)),
);
} |
Full examples: examples/linq-express-bot.ts · examples/photon-bot.ts · examples/quickstart.ts
import { render } from "openpigeon/games/connect4";
console.log(render(op.read(url).state));
// . . O X X X O
// . . X O . . .
// . . . . . . .All 11 decode to a documented state and round-trip byte-exact against real captured messages.
| game | game= token(s) |
state in | what you can read |
|---|---|---|---|
| 🎱 pool (8-Ball) | pool · pool2 · pool3 |
replay | shot (aim/power/spin) + every ball {x,y,rot,alpha,num,v…,pocketed} |
| 🎯 darts | darts |
replay | countdown scores + throws {x,y,points,segment,mult,ring} |
| ⛳ mini_golf | golf |
replay | per-hole stroke log {power,angle} (+ opponent's) |
| 🔴 connect4 | connect |
replay | 7×6 board grid + {col,row,player} + render() |
| 🏹 archery | archery |
replay | round state + throws {player,x,y,z} |
| 🏀 basketball | basketball |
replay ×4 | shots {t,arc,spin,made} across rounds |
| 🥤 cup_pong | beer |
replay | cups standing per side + throw records |
| 🚢 sea_battle | sea |
envelope+replay | size, ships {pos,hits,rot}, shot grid, last shot |
| 🔤 word_hunt | hunt |
envelope | letter grid, per-player score + found words |
| 🔀 anagrams | anagrams |
envelope | letter pool, per-player score + found words |
| 🧩 wordbites | wordbites |
envelope | tile pieces, per-player score + found words |
Per-game state shapes → docs/GAMES.md · adding a game is one file → src/games/pool.ts.
| provider | send | receive | notes |
|---|---|---|---|
| Linq | ✅ balloon + text | ✅ message.received webhook |
pure-TS end to end; native render (liveLayoutInfo) |
| Photon | ✅ via spectrum-ts |
✅ gRPC message stream | one process, no bridge; moves ride an https carrier |
- No AI / no move selection. It reads and writes state; you decide moves. Not a game engine, runs no physics.
- Doesn't play games for you.
read→replyis a codec, not an opponent. - No real-time "Play Now" online mode. That's a live PlayerIO socket, not an iMessage payload — only turn-based games ride the pipe.
- No SQLite /
chat.dbscraping. By design — you receive from your provider's webhook/stream; no Mac required. - Decode-correct, not byte-identical to a captured message — a rebuilt URL always decrypts to identical bytes (what the recipient parses), but the app's outer escaping is content-dependent and not reproduced verbatim.
- A few field semantics are inferred (flagged in each module): cup_pong trajectory samples kept as opaque tokens; darts bull (25/50) is a best guess; some state tuples are positional. The encodings are exact and reversible.
- Photon sending happens in Node, through
spectrum-ts(a gRPC SDK). OpenPigeon gives you thecustomizedMiniApp(...)args, not the socket. - You bring the account + host (a Linq/Photon account; a public webhook endpoint for Linq). Add Linq HMAC signature verification before exposing a webhook publicly.
- Not affiliated with GamePigeon. Independent, interoperable reimplementation from static analysis; ships no GamePigeon code or assets.
Full reverse-engineered protocol — balloon identity, the length-seeded anagram cipher, the state envelope, the turn rule — in docs/PROTOCOL.md.
One file in src/games/. Copy pool.ts: export
parse(replay, env) → state, build(state, env) → replay, and register({...}).
npm test # codecs (byte-exact vs real captures) + receive paths
node examples/quickstart.ts
npm run build # tsc → dist/Node ≥ 22 (runs .ts directly via type-stripping; builds with tsc 5.7+).
MIT — © 2026 time-attack. See LICENSE.
You're free to use, modify, and redistribute OpenPigeon in your own projects, including commercially — but you must keep the copyright and license notice (i.e., credit the author). The MIT license requires the notice above to be included in all copies or substantial portions of the software.
"GamePigeon" is a trademark of its owner; this project is not affiliated with or endorsed by them.

