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

# Native iMessage polls

> Handle titleless iMessage polls with stable poll GUIDs and option identifiers.

Native iMessage polls have no title. Their snapshots and nested vote context
omit the `title` field; option labels still use `title`. Match a poll with
`content.pollMessageGuid`. Match a selected option with
`content.option.optionIdentifier`, not its label — different options can have
identical labels.

## Poll creation and option additions

Poll creation and option additions arrive as `content.type === "poll"`, with
`content.change` set to `"created"` or `"optionAdded"`. Both contain the full
current option set. An option addition does not represent a vote.

```ts theme={null}
for await (const [space, message] of app.messages) {
  if (message.content.type === "poll" && message.content.change) {
    console.log(message.content.pollMessageGuid, message.content.change);
    for (const opt of message.content.options) {
      console.log(opt.title, opt.optionIdentifier);
    }
  }
}
```

## Votes

Votes and removed votes arrive as `content.type === "poll_option"`, with
`selected` set to `true` or `false`. The nested `content.poll` retains the poll
GUID and option identifiers.

```ts theme={null}
if (message.content.type === "poll_option") {
  const { pollMessageGuid, option, selected } = message.content;
  // Correlate by pollMessageGuid and option.optionIdentifier.
}
```

Use the vote message ID for duplicate detection and the poll GUID for
correlation. A created poll uses its root GUID as its message ID.

## Compatibility

These identity fields are optional on shared Spectrum content because other
providers can still supply polls without native identifiers. The `poll(title, ...)`
builder still requires a nonempty title for outbound polls. Native snapshots
with a poll GUID require distinct option identifiers, even when their labels are
equal.

The native `app.webhook()` handler preserves the same poll content types and
identifiers. Hosted delivery also requires the corresponding webhook-worker
update and SDK rollout; updating only the receiving application is not enough.
Previously skipped events are not automatically replayed.


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