Skip to main content

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

  1. Create a Spectrum project and enable the iMessage platform
  2. Copy your project ID and project secret — together they are the Basic-auth credentials for Spectrum’s management API
  3. Register a webhook pointing at your bot’s /webhook endpoint (the bot listens on port 3204 by default; override with BOT_SERVER_PORT):
  4. Copy the webhook secret: Spectrum signs every request, and the SDK rejects requests whose X-Spectrum-Signature does not verify
A project fans every message.received event out to all registered webhooks. Delete stale entries (an old tunnel URL, for example) or every inbound message will also fire at a dead endpoint, and repeated failures can get a webhook disabled.

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 only dist/ 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.

Troubleshooting