Skip to main content

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.

Every incoming message arrives through app.messages as a [Space, Message] tuple. The space is already bound to the originating conversation — you don’t need to resolve it yourself to reply.

Receiving messages

for await (const [space, message] of app.messages) {
  // handle the message
}
Messages from every configured provider merge into this single stream. The order reflects arrival time.

The Message shape

Every message conforms to .
FieldDescription
idPlatform-assigned message identifier.
contentDiscriminated union on type — see Narrowing content for the full set of variants.
senderThe who sent the message.
spaceThe the message was sent into.
platformName of the provider that delivered the message (e.g. “iMessage”, “terminal”).
timestampDate of when the message was sent.
react(reaction)React to this message. No-op on platforms that don’t support reactions.
reply(…content)Reply threaded to this message. Falls back silently on platforms without thread support.

Narrowing content

Content is a discriminated union. Narrow on message.content.type before accessing fields:
for await (const [space, message] of app.messages) {
  switch (message.content.type) {
    case "text":
      console.log(message.content.text);
      break;
    case "attachment":
      console.log(
        `${message.content.name} (${message.content.mimeType})`,
        await message.content.read(),
      );
      break;
    case "voice":
      console.log(`voice note (${message.content.duration}s)`);
      break;
    case "contact":
      console.log(message.content.name?.formatted, message.content.phones);
      break;
    case "richlink":
      console.log(message.content.url, await message.content.title());
      break;
    case "reaction":
      console.log(`${message.content.emoji} on ${message.content.target.id}`);
      break;
    case "poll":
      console.log(message.content.title, message.content.options);
      break;
    case "poll_option":
      console.log(`vote ${message.content.selected ? "+" : "-"}`, message.content.title);
      break;
    case "group":
      console.log(`group of ${message.content.items.length} items`);
      break;
    case "custom":
      console.log(message.content.raw);
      break;
  }
}
TypeFields
"text"text: string
"attachment"name: string, mimeType: string, size?: number, read(), stream()
"voice"name?: string, mimeType: string, duration?: number, size?: number, read(), stream()
"contact"name?, phones?, emails?, addresses?, org?, urls?, birthday?, note?, photo?, user?
"richlink"url: string, title(), summary(), cover()
"reaction"emoji: string, target: Message
"poll"title: string, options: { title: string }[]
"poll_option"option: { title }, poll: Poll, selected: boolean, title: string — sent as a vote
"group"items: Message[] — bundled multi-message unit
"reply"content: Content, target: Message — threaded reply wrapping inner content
"edit"content: Content, target: Message — rewrite of a previously-sent message
"typing"state: "start" | "stop" — typing indicator signal
"custom"raw: unknown — platform-specific structured data
Outgoing-only variants like "effect" (an iMessage screen effect wrapping inner content) appear on messages you sent and are echoed by the platform; see iMessage for the builder.

Filtering out your own messages

On platforms where your account also receives its own sends, guard with a platform-specific check — for example, iMessage carries an isFromMe flag on the raw message extra you can expose through a provider message.schema. For unified logic, compare the sender to a known identity:
for await (const [space, message] of app.messages) {
  if (message.sender.id === myAccountId) continue;
  // handle incoming
}

Acting on a message

Every message is its own context. You can reply, react, or send new content into the space:
for await (const [space, message] of app.messages) {
  await message.react("love");              // react
  await message.reply("Got it");            // threaded reply
  await space.send("Here's more context");  // fresh send into the space
}
See Content for all the ways to build outgoing messages, and Reactions and replies for the details of react and reply.