---
title: Photon
description: Connect an eve agent to iMessage through Photon.
url: "https://eve.dev/docs/channels/photon"
docs_index: /llms.txt
---

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

Use `photonIMessageChannel` to receive and reply to iMessages through a Photon project.

Run `eve add channel/photon-imessage` to create or use a Photon project and register
your phone number. It then asks whether to configure Vercel Connect or portable
credentials. With Vercel Connect, eve signs you in and creates or links a Vercel
project when needed, then creates the connector and configures Photon’s webhook
through Connect. The channel resolves credentials lazily when
the adapter first initializes:

```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"),
});
```

The default webhook route is `/eve/v1/photon`. The channel verifies forwarded
webhooks with Vercel OIDC from the same project and environment by default, derives user auth from each
message author, marks accepted messages as read, and continues the same eve
session for every message in an iMessage conversation. A new accepted message
steers the active turn. Steering can interrupt pending model generation before
answer output or local tool execution begins. Executing tools finish first;
after answer output starts, steering waits for the next committed workflow
boundary. See [Overlapping messages](./overview#overlapping-messages).

Set `turnPolicy: "queue"` when every response should finish before Photon starts the next message.

Photon uses inbound message IDs as [delivery idempotency keys](./custom#retried-deliveries).
Replicas that share a durable Workflow World ignore retries while the ID remains
in the session's recent-key window.

Images and other files people send reach the agent as file parts. Photon's
webhook lists each file without its bytes, so eve downloads it from Photon by
its attachment id with the project's credentials. The webhook omits the id
for voice notes, so eve refetches those messages from Photon to get it. A
failed download, a file over 25 MB, or a file eve can't find an id for reaches
the agent as a short note.

Customize inbound dispatch with `onMessage`. Return `null` to ignore a message, or return `title` alongside `auth` to set the title when the dispatch 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 override 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.

## 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 can also
accept a single emoji; the list does not enumerate those.

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) };
  },
});
```

The agent reads an accepted tapback as a message in the form iMessage uses when
it shows a tapback as text, such as `Tapback: Loved “See you at 7”`. Photon
reports only added tapbacks, so `tapback.added` is always `true` and removing a
tapback never reaches `onTapback`. The quoted text appears when Photon can return the message the tapback is on. `tapback.tapback` is `love`,
`like`, `dislike`, `laugh`, `emphasize`, or `question`, or the emoji itself for
an emoji tapback. `tapback.messageId` and `tapback.author` identify the message
and the person. An accepted tapback starts or steers a turn like any other
message, following `turnPolicy`. The agent's own tapbacks never reach `onTapback`. `onTapback` runs instead of `onMessage` for tapbacks, so repeat any sender checks from `onMessage` there.

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": "🎉" }`, and 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`](../concepts/built-in-tools#no_reply). The tool is never added on its own, and subagents don't receive it. In a session that didn't come through this channel, such as a web chat, or before anyone has sent a message, the call fails with an error the model reads.

### Answer approvals with a tapback

Return `answer` from `onTapback` to answer the input request eve posted in the message the tapback is on. A [tool approval](/docs/human-in-the-loop#approvals) offers the options `approve` and `cancel`, so a like or dislike on the approval prompt can approve or cancel 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` settles only the request posted in that message, and only while it is still waiting and offers that option. A tapback on any other message, or on a prompt that was already answered, reaches the agent as an ordinary tapback, such as `Tapback: Liked “Got it”`. The approval's response policy still decides whether the person who sent the tapback may answer. For a [question](/docs/human-in-the-loop#questions), return one of its option ids. To replace the text the agent receives for a tapback, return `message`. Don't set `message` to an option's id, label, or number: eve reads that text like a typed reply, which answers the first waiting approval no matter which message the tapback is on.

## Other hosts

For 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, use lazy environment-backed credentials:

```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.

---

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)