---
title: Photon Reference
description: Photon iMessage channel options, credentials, webhook verification, steering, retries, attachments, inbound hooks, and tapbacks.
url: "https://eve.dev/docs/channels/photon/reference"
docs_index: /llms.txt
type: reference
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

This page documents every part of `photonIMessageChannel` from `eve/channels/photon`. For setup and common tasks, start with [Photon](/docs/channels/photon).

The Photon channel connects your agent to iMessage through a Photon project and `agent/channels/photon.ts`.

## Setup and credentials

Run `eve add channel/photon-imessage` to create or use a Photon project and register your phone number, then choose Vercel Connect or portable credentials. With Vercel Connect, eve signs you in, creates or links a Vercel project, creates the connector, and configures Photon's webhook through Connect:

```ts title="agent/channels/photon.ts"
import { connectPhotonCredentials } from "@vercel/connect/eve";
import { photonIMessageChannel } from "eve/channels/photon";

export default photonIMessageChannel({
  credentials: connectPhotonCredentials("photon/my-agent"),
});
```

### Other hosts

On a host without Vercel Connect, choose **Use portable credentials** during `eve add channel/photon-imessage`. eve scaffolds the channel and writes `IMESSAGE_PROJECT_ID` and `IMESSAGE_PROJECT_SECRET` to `.env.local`.

After deploying the agent:

1. Create a Photon webhook for your public `https://…/eve/v1/photon` URL.
2. Copy its signing secret into `IMESSAGE_WEBHOOK_SECRET`.
3. Set `IMESSAGE_PROJECT_ID`, `IMESSAGE_PROJECT_SECRET`, and `IMESSAGE_WEBHOOK_SECRET` in the host’s encrypted environment variables.

To configure the channel by hand, resolve credentials from the environment:

```ts title="agent/channels/photon.ts"
import { photonIMessageChannel } from "eve/channels/photon";

export default photonIMessageChannel({
  async credentials() {
    const projectId = process.env.IMESSAGE_PROJECT_ID;
    const projectSecret = process.env.IMESSAGE_PROJECT_SECRET;
    if (!projectId || !projectSecret) throw new Error("Photon project credentials are required.");
    return { projectId, projectSecret };
  },
  webhookSecret: process.env.IMESSAGE_WEBHOOK_SECRET,
});
```

For direct Photon webhooks, pass `webhookSecret` or set `IMESSAGE_WEBHOOK_SECRET`. The signing secret takes precedence over the default OIDC verifier.

## Inbound messages

The channel resolves credentials on first use. It receives webhooks at `/eve/v1/photon` and, by default, verifies them with Vercel OIDC from the same project and environment. It builds auth from the person who sent each message, marks accepted messages as read, and keeps one eve session per iMessage conversation.

A message that arrives during a turn joins that turn, so the agent can take the correction into account. Set `turnPolicy: "queue"` to let every reply finish before the next message starts. See [Overlapping messages](/docs/channels#overlapping-messages) for when a new message can interrupt the model.

When Photon retries a webhook, eve accepts the message once, even across replicas that share a Workflow World. See [retried deliveries](/docs/channels/custom#retried-deliveries).

## Attachments

Images and other files people send reach the agent as file parts. A failed download, a file over 25 MB, or a file without an attachment id reaches the agent as a short note instead.

## Message hook

Customize inbound handling with `onMessage`. Return `null` to ignore a message, or return `title` alongside `auth` to set the run title when the message starts a run:

```ts
export default photonIMessageChannel({
  credentials: connectPhotonCredentials("photon/my-agent"),
  onMessage(_ctx, message) {
    if (message.author.isBot) return null;
    return {
      auth: null,
      context: [`The sender is ${message.author.fullName}.`],
    };
  },
});
```

Set `route` to change the webhook path, or `webhookVerifier` to use a different trusted-forwarder verifier. `eve/channels/photon` exports the `PhotonIMessageWebhookVerifier`, `PhotonIMessageChannelCredentials`, and `PhotonProjectCredentials` types for these options.

## Options

`photonIMessageChannel(config)` requires `credentials`. Every other option is optional.

| Option            | Default                                                      | Description                                                                                  |
| ----------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `credentials`     | Required                                                     | `connectPhotonCredentials(...)`, or a function that returns `{ projectId, projectSecret }`.  |
| `webhookSecret`   | `IMESSAGE_WEBHOOK_SECRET`                                    | Signing secret for direct Photon webhooks.                                                   |
| `webhookVerifier` | Same-project Vercel OIDC verification when there's no secret | `(request, rawBody)` verifier for forwarded webhooks. Takes precedence over `webhookSecret`. |
| `route`           | `/eve/v1/photon`                                             | Webhook path.                                                                                |
| `userName`        | `"eve"`                                                      | Bot display name.                                                                            |
| `turnPolicy`      | `"steer"`                                                    | `"steer"` or `"queue"` for messages that arrive during a turn.                               |
| `onMessage`       | Dispatch with the author's auth                              | `(ctx, message)` hook. See [Message hook](#message-hook).                                    |
| `onTapback`       | None, so tapbacks are ignored                                | `(ctx, tapback)` hook. See [Tapbacks](#tapbacks).                                            |
| `events`          | eve's default handlers                                       | Per-event handler overrides.                                                                 |

The channel mounts `GET` and `POST` on `/eve/v1/photon`.

## Tapbacks

`IMESSAGE_TAPBACKS` from `eve/channels/photon` lists the built-in tapback names. Use it to populate a picker or validate a named reaction. Tapback APIs also accept a single emoji, which the list does not include.

eve ignores tapbacks people send unless you pass `onTapback`. It receives the tapback and returns the same result as `onMessage`, or `null` to ignore it:

```ts title="agent/channels/photon.ts"
import { connectPhotonCredentials } from "@vercel/connect/eve";
import { defaultPhotonAuth, photonIMessageChannel } from "eve/channels/photon";

export default photonIMessageChannel({
  credentials: connectPhotonCredentials("photon/my-agent"),
  onTapback(_ctx, tapback) {
    if (!tapback.added) return null;
    return { auth: defaultPhotonAuth(tapback) };
  },
});
```

| Field               | Value                                                                                          |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `tapback.tapback`   | `love`, `like`, `dislike`, `laugh`, `emphasize`, `question`, or the emoji for an emoji tapback |
| `tapback.messageId` | The message the tapback is on                                                                  |
| `tapback.author`    | The person who sent it                                                                         |
| `tapback.added`     | Always `true`, because Photon reports only added tapbacks                                      |

The agent reads an accepted tapback as iMessage's text form, such as `Tapback: Loved “See you at 7”`. The quoted text appears when Photon can return the original message. An accepted tapback starts or steers a turn like any other message, following `turnPolicy`.

`onTapback` runs instead of `onMessage` for tapbacks, so repeat any sender checks from `onMessage` there. The agent's own tapbacks never reach it.

To let the agent send tapbacks, export the channel's tapback tool from a file in `agent/tools/`. The file name becomes the tool name:

```ts title="agent/tools/tapback.ts"
import photon from "../channels/photon";

export default photon.tapbackTool();
```

The model calls it with a standard tapback, such as `{ "tapback": "love" }`, or any single emoji, such as `{ "tapback": "🎉" }`. eve reacts to the latest message someone sent in the conversation. A standard tapback's own emoji, such as `👍`, sends that tapback.

The model can send a tapback alongside a reply; to let a tapback be the whole answer, also add [`no_reply`](/docs/concepts/built-in-tools#no_reply). Subagents don't receive the tool. In a session from another channel, such as web chat, or before anyone has sent a message, the call returns an error to the model.

### Answer approvals with a tapback

Return `answer` from `onTapback` to answer the approval or question in the message the tapback is on. A [tool approval](/docs/human-in-the-loop#approvals) offers the options `approve` and `deny`, so a like or dislike on the approval prompt can approve or deny it:

```ts title="agent/channels/photon.ts"
import { connectPhotonCredentials } from "@vercel/connect/eve";
import { defaultPhotonAuth, photonIMessageChannel } from "eve/channels/photon";

export default photonIMessageChannel({
  credentials: connectPhotonCredentials("photon/my-agent"),
  onTapback(_ctx, tapback) {
    if (!tapback.added) return null;
    const auth = defaultPhotonAuth(tapback);
    if (tapback.tapback === "like") return { auth, answer: "approve" };
    if (tapback.tapback === "dislike") return { auth, answer: "deny" };
    return { auth };
  },
});
```

`answer` answers only the request posted in that message, and only while that request is still open and offers that option. A tapback on any other message, or on an answered prompt, reaches the agent as an ordinary tapback, such as `Tapback: Liked “Got it”`. The approval's response policy still decides whether the sender may answer. For a [question](/docs/human-in-the-loop#questions), return one of its option ids.

Return `message` to replace the text the agent receives for a tapback. Don't set `message` to an option's id, label, or number, because eve reads that text like a typed reply. A typed reply answers the first waiting approval, no matter which message the tapback is on.

## What to read next

- [Channels overview](/docs/channels): the channel contract and every built-in channel
- [Linq](/docs/channels/linq): iMessage and SMS through Linq

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)