iMessage Bot Development
Run your own instance of the GAIA iMessage bot. The bot receives messages through Photon Spectrum, which bridges iMessage to a signed webhook, so you need a publicly reachable URL.Looking to use GAIA on iMessage as a user? See Using GAIA on iMessage.
Prerequisites
- Node.js 18+ and pnpm
- A Photon Spectrum project with iMessage enabled
- GAIA API running (see Self-Hosting Guide)
- A publicly reachable URL for the webhook (or a tunnel like ngrok during development)
Step 1: Set up Photon Spectrum
- Create a Spectrum project and enable the iMessage platform
- Copy your project ID and project secret — together they are the Basic-auth credentials for Spectrum’s management API
-
Register a webhook pointing at your bot’s
/webhookendpoint (the bot listens on port3204by default; override withBOT_SERVER_PORT): -
Copy the webhook secret: Spectrum signs every request, and the SDK rejects requests whose
X-Spectrum-Signaturedoes not verify
Step 2: Configure environment variables
Create a.env file in apps/bots/imessage/ (or use the shared apps/bots/.env):
GAIA_BOT_API_KEY must match the BOT_API_KEY configured in your GAIA API. Generate BOT_LOG_HASH_SECRET with openssl rand -hex 32.
Step 3: Start the bot
Recipients must be registered first
Spectrum’s shared line will not deliver to a number the project has not registered. GAIA’s connect flow does this for you —POST /platform-links/imessage/connect registers the number and returns the pool line assigned to it, which is what the user texts.
Registration is idempotent and expects E.164 format. Each registered user gets their own assignedPhoneNumber, so there is no single number to hand out.
Platform behavior
A few iMessage-specific constraints, handled automatically by the adapter:- Phone-number handles only: the shared pool routes messages sent from a phone number. A user sending from their Apple ID email gets Spectrum’s canned bounce until they switch Settings → Messages → Send & Receive and start a new conversation
- Direct messages only: group spaces are ignored
- No streaming or edits: responses are sent as one complete message once generation finishes
- Plain text: iMessage has no markdown rendering, so responses are sent unformatted
- Outbound sends go through the space:
im.space.create(handle).send(...), there is no REST send endpoint - Media: images and documents are uploaded; voice notes are not supported, because the webhook payload carries no attachment id for them
- Pro-gated: linking is restricted to Pro accounts and the plan is re-checked on every message, so a downgrade is refused rather than silently served
Docker note
The bot image ships onlydist/ plus the three gRPC peer packages the Photon SDK resolves at runtime (nice-grpc, nice-grpc-common, @grpc/grpc-js). tsup bundles their code, but the SDK calls import.meta.resolve() on them before creating its client, so they must also exist on disk — the shared apps/bots/Dockerfile stages them for this bot and fails the build if any is missing.

