> Discover all available pages from the documentation index: https://mastra.ai/llms.txt # iMessage iMessage channels let a Mastra agent receive direct messages and group messages from iMessage. Two vendor-maintained adapters connect Mastra to iMessage: [Photon](https://app.photon.codes) and [Linq](https://linqapp.com). Mastra handles the agent wiring and the webhook route; the adapter docs cover number provisioning, credentials, and webhook registration. ## Choose an adapter Both adapters follow the same Mastra wiring, so pick the provider first: - **Photon**: Hosted or self-hosted iMessage service. Supports webhooks and a [gateway listener](#gateway-listener) that streams messages over an open connection. - **Linq**: Hosted iMessage, RCS, and SMS API. Webhook-driven, with tapback reactions and media mapped in both directions. ## Install the adapter **Photon**: Install the Photon iMessage adapter: **npm**: ```bash npm install @photon-ai/chat-adapter-imessage ``` **pnpm**: ```bash pnpm add @photon-ai/chat-adapter-imessage ``` **Yarn**: ```bash yarn add @photon-ai/chat-adapter-imessage ``` **Bun**: ```bash bun add @photon-ai/chat-adapter-imessage ``` **Linq**: ```bash npm install @photon-ai/chat-adapter-imessage ``` **Tab 3**: ```bash pnpm add @photon-ai/chat-adapter-imessage ``` **Tab 4**: ```bash yarn add @photon-ai/chat-adapter-imessage ``` **Tab 5**: ```bash bun add @photon-ai/chat-adapter-imessage ``` **Tab 6**: Install the Linq Chat SDK adapter: **npm**: ```bash npm install @linqapp/chat-sdk-adapter ``` **pnpm**: ```bash pnpm add @linqapp/chat-sdk-adapter ``` **Yarn**: ```bash yarn add @linqapp/chat-sdk-adapter ``` **Bun**: ```bash bun add @linqapp/chat-sdk-adapter ``` **Tab 7**: ```bash npm install @linqapp/chat-sdk-adapter ``` **Tab 8**: ```bash pnpm add @linqapp/chat-sdk-adapter ``` **Tab 9**: ```bash yarn add @linqapp/chat-sdk-adapter ``` **Tab 10**: ```bash bun add @linqapp/chat-sdk-adapter ``` ## Agent configuration Add the adapter factory to the agent's `channels.adapters` object: **Photon**: ```typescript import { Agent } from '@mastra/core/agent' import { createiMessageAdapter } from '@photon-ai/chat-adapter-imessage' export const imessageAgent = new Agent({ id: 'imessage-agent', name: 'iMessage Agent', instructions: 'Answer questions and help with tasks over iMessage.', model: 'openai/gpt-5.6-sol', channels: { adapters: { imessage: { adapter: createiMessageAdapter(), toolDisplay: 'text', }, }, threadContext: { maxMessages: 0 }, }, }) ``` `threadContext: { maxMessages: 0 }` skips the platform history lookup Mastra runs when an agent is first mentioned in a group chat, which the Photon adapter can't perform. **Linq**: ```typescript import { Agent } from '@mastra/core/agent' import { createLinqAdapter } from '@linqapp/chat-sdk-adapter' export const imessageAgent = new Agent({ id: 'imessage-agent', name: 'iMessage Agent', instructions: 'Answer questions and help with tasks over iMessage.', model: 'openai/gpt-5.6-sol', channels: { adapters: { imessage: { adapter: createLinqAdapter({ apiKey: process.env.LINQ_API_KEY!, signingSecret: process.env.LINQ_WEBHOOK_SECRET!, }), toolDisplay: 'text', }, }, }, }) ``` `createLinqAdapter()` takes the credentials directly: `apiKey` is your Linq API key and `signingSecret` comes from the [webhook subscription](#webhook-url). The Linq adapter can fetch thread history, so the default `threadContext` behavior works. Register the agent on the Mastra instance: ```typescript import { Mastra } from '@mastra/core' import { imessageAgent } from './agents/imessage-agent' export const mastra = new Mastra({ agents: { imessageAgent }, }) ``` Use `imessage` as the adapter key. Mastra derives the webhook path and the `platform` value on `requestContext` from this key. `toolDisplay: 'text'` describes tool calls in the message, since iMessage has no interactive cards for Approve and Deny actions. Both adapters need it: Photon has no card rendering, and Linq flattens cards to plain text where buttons show their labels but can't trigger actions. > **Warning:** Text rendering can't submit approval decisions. A tool with `requireApproval: true` stays suspended until a UI or API action, such as Studio, submits an explicit approval or decline, so avoid approval-gated tools on iMessage agents unless another surface handles the decision. See [Tool approval](https://mastra.ai/docs/channels). ## Adapter setup **Photon**: Follow the [Photon iMessage adapter docs](https://github.com/photon-hq/vercel-chat-adapter-imessage) for iMessage-specific setup, including number provisioning, hosted and self-hosted modes, and webhook registration. The adapter picks its mode from the environment variables you set. For the hosted service, create a project at [app.photon.codes](https://app.photon.codes) and use the project credentials: ```bash IMESSAGE_PROJECT_ID=your-project-id IMESSAGE_PROJECT_SECRET=your-project-secret IMESSAGE_WEBHOOK_SECRET=your-webhook-signing-secret ``` For a self-hosted server, point the adapter at its gRPC address, written as `host:port`. The adapter strips any URL scheme and appends `:443` to a bare host: ```bash IMESSAGE_SERVER_URL=imessage.example.com:443 IMESSAGE_API_KEY=your-server-token IMESSAGE_PHONE=+15551234567 ``` `IMESSAGE_PHONE` is optional and routes messages when a self-hosted server has several numbers. You can also pass these values to `createiMessageAdapter()` directly, including a `credentials` function that resolves the project ID and secret at first use from a secret store. **Linq**: Follow the [Linq API docs](https://docs.linqapp.com/) for Linq-specific setup, including phone number provisioning and API keys. Create a Linq account, copy your API key, and set the credentials the agent configuration reads: ```bash LINQ_API_KEY=your-linq-api-key LINQ_WEBHOOK_SECRET=your-webhook-signing-secret ``` `LINQ_WEBHOOK_SECRET` is the signing secret returned when you create a webhook subscription in the next section. The adapter also accepts a `baseURL` option to target a different Linq API base URL, such as a sandbox. ## Webhook URL Mastra generates the iMessage webhook route from the agent ID and adapter key: ```text /api/agents/imessage-agent/channels/imessage/webhook ``` Use your public Mastra server URL as the base URL: ```text https://your-app.example.com/api/agents/imessage-agent/channels/imessage/webhook ``` **Photon**: Register this URL in the [Photon dashboard](https://app.photon.codes), then set the signing secret it returns as `IMESSAGE_WEBHOOK_SECRET`. The secret is shown once at registration. The adapter verifies the signature on every delivery and rejects requests that don't match. Webhooks are available in hosted mode only. **Linq**: Create a [webhook subscription](https://docs.linqapp.com/guides/webhooks/subscriptions/) with this URL as the target and subscribe to at least these events: - `message.received` - `reaction.added` - `reaction.removed` Set the `signing_secret` the subscription returns as `LINQ_WEBHOOK_SECRET`. The secret is shown once at creation and can't be retrieved later. The adapter verifies the HMAC signature on every delivery, checks for replayed requests, and rejects requests that don't match. > **Note:** Both providers deliver to public HTTPS endpoints only, not to `http://` or private addresses like `localhost`. For local development, use a tunnel as described in the [Channels overview](https://mastra.ai/docs/channels). ## Duplicate deliveries Photon and Linq retry failed deliveries with backoff and deliver at least once, so the same message can arrive twice. Chat SDK drops the repeat using the channel state adapter, and Mastra's default keeps those dedup keys in memory. That covers a single long-running server. A repeat can still reach the agent after a restart, or on serverless where the retry is routed to a different instance. Pass a shared state adapter on `channels.state` so dedup keys are visible everywhere. Install one alongside the adapter: **npm**: ```bash npm install @chat-adapter/state-redis ``` **pnpm**: ```bash pnpm add @chat-adapter/state-redis ``` **Yarn**: ```bash yarn add @chat-adapter/state-redis ``` **Bun**: ```bash bun add @chat-adapter/state-redis ``` `createRedisState()` reads the `REDIS_URL` environment variable: ```typescript import { createRedisState } from '@chat-adapter/state-redis' channels: { adapters: { imessage: { adapter: createiMessageAdapter(), toolDisplay: 'text', }, }, state: createRedisState(), }, ``` This matters most for tools with side effects, where handling the same message twice is visible to the user. ## Read receipts iPhone Messages sends a read receipt for every message the agent posts, and the Photon adapter delivers those receipts as inbound messages with no text and no attachments. Mastra skips them, so the agent doesn't answer its own reply in a loop. Their message IDs carry a `:read:` suffix, and they show up as skipped messages at `debug` log level. The same rule applies to every adapter: an inbound message with neither text nor attachments never starts an agent run. A custom `onDirectMessage`, `onMention`, or `onSubscribedMessage` handler still receives it and can act on it before calling `defaultHandler`. ## Gateway listener The Photon adapter can hold an open connection and stream messages in real time instead of receiving webhooks. This works in both hosted and self-hosted modes. The Linq adapter is webhook-driven and has no gateway mode. Mastra starts this listener during initialization and reconnects it if it drops, so no cron job or extra route is needed on a long-running server. Set `gateway: false` on the adapter config to turn it off when you use webhooks: ```typescript imessage: { adapter: createiMessageAdapter(), toolDisplay: 'text', gateway: false, }, ``` On serverless platforms, prefer webhooks. A gateway listener needs a process that stays alive. See [Serverless deployment](https://mastra.ai/docs/channels). ## Related - [Channels overview](https://mastra.ai/docs/channels) - [More](https://mastra.ai/docs/channels)