app.messages as a
read message. Unlike group events this needs no dedicated line — receipts
ride the message stream that every mode already consumes.
message.senderis the reader and is always present — receipts Apple could not attribute are dropped rather than surfaced with an unknown sender.message.content.targetis 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.timestampis 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.idagainstspace.getMembers()to answer “has everyone read it”. - The agent’s own reads never echo back:
space.read(...)andmessage.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.
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.