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

# Inbound iMessage read receipts

> Observe when iMessage recipients read messages sent by your agent.

When someone reads a message the agent sent, it arrives on `app.messages` as a
`read` message. Unlike group events this needs no dedicated line — receipts
ride the message stream that every mode already consumes.

<Warning>
  Read receipts are high-volume. A group emits **one message per reader per
  message read**, and because marking a chat read clears *every* unread message
  in it, one recipient opening a chat with several unread agent messages can
  produce a burst. Make sure your `for await` loop handles `"read"` explicitly —
  a `default:` arm that replies to the user will reply to every receipt.
</Warning>

| Someone…                 | `content.type` | `message.sender` | `message.content.target`                            |
| ------------------------ | -------------- | ---------------- | --------------------------------------------------- |
| reads a message you sent | `"read"`       | the reader       | the message that was read (`direction: "outbound"`) |

```ts theme={null}
for await (const [space, message] of app.messages) {
  if (message.content.type === "read") {
    const target = message.content.target;
    console.log(
      `${message.sender?.id} read ${target.id} at ${message.timestamp.toISOString()}`
    );
  }
}
```

Semantics to rely on:

* `message.sender` is the **reader** and is always present — receipts Apple
  could not attribute are dropped rather than surfaced with an unknown sender.
  `message.content.target` is always *your* message.
* **DMs are the reliable case.** Apple does not name the reader on a read event:
  the event's actor is the *receiving line*, not the person who read. In a DM
  the reader is recovered from the conversation, which is unambiguous. A group
  conversation carries no participant information, so a group receipt is
  dropped unless Apple names an actor other than your own line — treat group
  read receipts as best-effort.
* `message.timestamp` is the reader's read time, propagated from their device —
  not when your process observed the event.
* Where readers are reported, it is one message per reader. Aggregate by
  `message.content.target.id` against `space.getMembers()` to answer "has
  everyone read it".
* The agent's own reads never echo back: `space.read(...)` and `message.read()`
  are suppressed from the inbound stream.
* Receipts ride the same durable catch-up log as messages, so those that arrive
  while the app is down are replayed on reconnect. A receipt whose target can
  no longer be resolved (an old message evicted from the in-process cache and
  since deleted upstream) is dropped rather than surfaced with a stub.

If receipts are not arriving, run with `LOG_LEVEL=debug` (or
`Spectrum({ options: { logLevel: "debug" } })`). Every receipt logs its outcome
under `spectrum.imessage.read`, and each drop states its reason — no reader
identified, target unresolved, or a target that is not one of yours. If nothing
logs at all, the platform is not sending the event: check that the recipient
has **Send Read Receipts** enabled on their device, which is off by default and
is what Apple gates the whole signal on.
