> ## Documentation Index
> Fetch the complete documentation index at: https://docs.photon.codes/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Use Stable documentation by default. Honor an explicit Beta request or a URL under /docs/beta/. If the requested version conflicts with the installed CLI package or API origin, clarify the target before writing integration code.
> Pages under /docs/beta/ document Beta; other product pages document Stable. Keep the CLI package, commands, API origin, and credentials within the selected version. State the documentation version in your answer.
> For MCP search, always pass version: Stable or version: Beta. Unfiltered search mixes both versions. For filesystem reads, keep Beta queries under /beta/ and exclude /beta/ from Stable queries; discover paths before reading them.
> The public docs base is https://photon.codes/docs. Convert MCP page paths to public URLs under that base, preserving /beta/ when present. Read https://photon.codes/docs/skill.md for version selection and https://photon.codes/docs/llms.txt for the version indexes.

# Chat SDK

> Connect Chat SDK bots to iMessage, SMS, WhatsApp, WhatsApp Business, Telegram, and email with the Photon adapter.

> [!WARNING]
>
> **Beta is a preview.** The Beta (2.0) platform does not have all the features
> of [Stable](https://docs.photon.codes/docs/spectrum-ts/introduction.md) yet. For example, iMessage on Beta
> supports only text and images, so tapbacks, polls, message effects, and voice
> notes are [only on Stable](https://docs.photon.codes/docs/spectrum-ts/providers/imessage/messaging-features.md)
> for now. Use Stable for production agents, and confirm that Beta supports the
> features you need before you buy a plan.

[`@photon-ai/chat-adapter`](https://www.npmjs.com/package/@photon-ai/chat-adapter)
connects [Chat SDK](https://chat-sdk.dev/docs) bots to the channels connected to a
Photon Beta project: iMessage, SMS, WhatsApp, WhatsApp Business, Telegram, and
email. It calls the Photon API with a project API key and receives events through
signed webhooks or a WebSocket connection.

This guide covers the published **0.1.0** adapter and Chat SDK **4.44.0**.
The [source and issue tracker](https://github.com/photon-hq/chat-adapter) are
maintained by Photon.

> [!NOTE]
>
> This replaces the retired `@photon-ai/chat-adapter-imessage` adapter. It uses
> `createPhotonAdapter()` and `adapters.photon`, not `createiMessageAdapter()`
> and `adapters.imessage`. See [Migrating from the iMessage adapter](#migrating-from-the-imessage-adapter).

## Installation

Use Node.js 20 or later in a server-side application:

```bash theme={null}
pnpm add chat @photon-ai/chat-adapter @chat-adapter/state-redis
```

The examples use Redis for Chat SDK state, subscriptions, deduplication, and the
WebSocket resume cursor. Any Chat SDK state adapter works; an in-memory adapter
loses this state on restart and does not share it between serverless invocations.

## Configuration

1. [Set up a Beta project and API key](https://docs.photon.codes/docs/beta/get-started/project.md). Use a project
   API key with `project:read` and `project:write` for receiving messages and
   sending replies.
2. Connect the channels you want to use in the
   [Beta dashboard](https://app.beta.photon.codes). Start with the
   [iMessage](https://docs.photon.codes/docs/beta/get-started/imessage.md) or [email](https://docs.photon.codes/docs/beta/get-started/email.md) guide.
3. Set these variables in your server's environment or secret manager. For local
   development, use a gitignored `.env.local` file:

```dotenv .env.local theme={null}
PHOTON_PROJECT_ID=pho_prj_REPLACE_WITH_YOUR_PROJECT_ID
PHOTON_API_KEY=pho_sk_REPLACE_WITH_YOUR_PROJECT_API_KEY
REDIS_URL=redis://localhost:6379
```

Run Redis at `REDIS_URL`, or replace it with your hosted Redis connection URL.
Never put keys, signing secrets, or Redis credentials in browser code or commit
them to source control.

| Environment variable | Adapter option | Purpose |
| - | - | - |
| `PHOTON_PROJECT_ID` | `projectId` | Required. Your Beta project ID. |
| `PHOTON_API_KEY` | `apiKey` | Required. The project's API key, not a Spectrum project secret. |
| `PHOTON_WEBHOOK_SECRET` | `webhookSecret` | Required for webhooks. The destination's `whsec_...` signing secret. Not needed for WebSocket delivery. |
| `PHOTON_AGENT_USER_ID` | `agentUserId` | Required only for `openDM`. The Agent User (`pho_usr_...`) that starts the conversation. |
| `PHOTON_API_URL` | `baseUrl` | Defaults to `https://api.photon.codes`. Supply an origin, without `/v1`. |

Options passed to `createPhotonAdapter()` override environment variables. The
optional `userName` option controls group mention matching; it defaults to the
Chat instance's `userName`. `REDIS_URL` belongs to `createRedisState()`, not the
Photon adapter.

## Add the adapter

Register the adapter as `photon`. Direct messages reach `onDirectMessage`.
In groups, this example subscribes when someone mentions `@mybot`, then replies
to subsequent messages in that Space.

```ts lib/bot.ts theme={null}
import { createRedisState } from "@chat-adapter/state-redis";
import { createPhotonAdapter } from "@photon-ai/chat-adapter";
import { Chat } from "chat";

export const bot = new Chat({
  userName: "mybot",
  adapters: { photon: createPhotonAdapter() },
  state: createRedisState(),
});

bot.onDirectMessage(async (thread, message) => {
  await thread.post(`You said: ${message.text}`);
});

bot.onNewMention(async (thread) => {
  await thread.subscribe();
  await thread.post({ markdown: "Hi, I'm **here**." });
});

bot.onSubscribedMessage(async (thread, message) => {
  await thread.post(`You said: ${message.text}`);
});
```

> [!WARNING]
>
> Test an echo bot from your own phone or account, not another automated line in
> the same project. Photon can report that other line as a human sender, so two
> echo bots can keep replying to each other.

## Receiving messages

Choose a webhook for a public HTTPS backend, or WebSocket delivery for a
process without a public URL. Both deliver `message.received`
events to the same Chat SDK handlers.

### Webhooks

[Create a webhook destination](https://docs.photon.codes/docs/beta/message-delivery/webhook.md) pointing at your
bot's HTTPS route and subscribe to **`message.received`**. Save its signing
secret as `PHOTON_WEBHOOK_SECRET` in the server environment.

For example, in Next.js, with the `@/` alias pointing at your application root:

```ts app/api/webhooks/photon/route.ts theme={null}
import { after } from "next/server";
import { bot } from "@/lib/bot";

export async function POST(request: Request) {
  return bot.webhooks.photon(request, {
    waitUntil: (task) => after(() => task),
  });
}
```

Pass the original `Request`; do not parse or reserialize its body first.
`bot.webhooks.photon` initializes the bot and verifies the Standard Webhooks
headers: `webhook-id`, `webhook-timestamp`, and `webhook-signature`. Invalid or
expired signatures return `401`; a missing signing secret returns `500`.
Events other than `message.received` are acknowledged without invoking handlers.

On serverless hosts, pass the host's background-task hook as `waitUntil` so
handlers can finish after the response. The `200` response acknowledges the
event, not successful completion of your handler or delivery of its reply.

### WebSocket gateway

The gateway uses Photon's [Socket.IO event stream](https://docs.photon.codes/docs/beta/message-delivery/websocket.md).
It needs the project ID and API key, but no webhook destination or signing secret.
Initialize the bot before starting the listener:

```ts gateway.ts theme={null}
import { bot } from "./lib/bot.ts";

await bot.initialize();
await bot.getAdapter("photon").startGatewayListener();
```

For this TypeScript runner, use Node.js 24 in a project with `"type": "module"`:

```bash theme={null}
node --env-file=.env.local gateway.ts
```

The listener runs in the background for three minutes by default. It reconnects
after transient disconnects and stores its event sequence cursor in the Chat SDK
state adapter, so a later connection can resume. For serverless deployments,
prefer the webhook example above.

## Threads and direct messages

A thread represents a Photon Space and the Agent that received the message:
`photon:<spaceId>:<agentUserId>:<spaceKind>`. Replies use that Agent, including
in a fresh webhook invocation. Direct Spaces are DMs; group Spaces use the
mention and subscription handlers.

To start a conversation, set `PHOTON_AGENT_USER_ID` to a connected Agent User and
pass an existing Photon User ID, not a phone number, to the adapter's `openDM`:

```ts theme={null}
await bot.initialize();
const threadId = await bot.getAdapter("photon").openDM("pho_usr_REPLACE_WITH_RECIPIENT_ID");
await bot.thread(threadId).post("Hello from Photon!");
```

Call the adapter explicitly: Chat SDK 4.44.0's `bot.openDM()` does not recognize
Photon User IDs. The adapter finds or creates the direct Space between the Agent and that User.
Use an Agent and recipient on the same platform; the platform's messaging rules
still apply.

## Feature support

Support depends on the connected platform. An unsupported operation raises
Chat SDK's `NotImplementedError`; typing is a no-op on platforms without it.

| Chat SDK capability | Photon adapter behavior |
| - | - |
| Send text or Markdown | Sends text content through Photon; each platform renders what it supports. Raw HTML is dropped. |
| Files | Uploads files as attachments. Text and each file are sent as separate Messages. |
| Inbound attachments | Includes name, type, size, and `fetchData()` for downloading. |
| Streaming | Collects the stream and sends one completed message, not incremental edits. |
| Edit / delete | Sends `message.update` / `message.delete` where supported. |
| Add a reaction | Sends `reaction` where supported. |
| `onReaction` | Receives incoming reactions and reaction removals. |
| `onMessageUpdated` / `onMessageDeleted` | Receives edits and deletions using the original Message ID. |
| `openDM` | Opens a direct Space using the configured Agent User. |
| `fetchMessages` / `fetchThread` | Reads paginated Space history / Space information. |
| `markAsRead` | Sends a read receipt. |

Reactions, edits, and deletions arrive as content inside `message.received`;
they do not require separate webhook event subscriptions.

## Migrating from the iMessage adapter

This is a new adapter and API, not a drop-in package rename. Keep the
[legacy guide](https://docs.photon.codes/docs/integrations/chat-sdk.md) for an existing Spectrum integration.

| Legacy Spectrum integration | Photon Beta integration |
| - | - |
| `@photon-ai/chat-adapter-imessage` (formerly `chat-adapter-imessage`) | `@photon-ai/chat-adapter` |
| `createiMessageAdapter()` / `adapters.imessage` | `createPhotonAdapter()` / `adapters.photon` |
| Spectrum project ID and secret, `IMESSAGE_*` options | Beta project ID and API key, `PHOTON_*` options |
| `bot.webhooks.imessage` and Spectrum webhook signatures | `bot.webhooks.photon` and a new `whsec_...` signing secret |
| iMessage-specific thread IDs | Photon Space and Agent User IDs |

Create a Beta project, connect its channels, replace the credentials and webhook
destination, and review [feature support](#feature-support) before switching
traffic. Old thread IDs, subscriptions, and gateway cursors are not portable;
use a separate state store or namespace for the new bot.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.