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.

Spectrum exposes a family of content builders — text, attachment, voice, contact, richlink, poll, group, custom, reaction, reply, edit, and typing — plus a string shortcut that’s equivalent to text(). Any API that takes a accepts a plain string or a .

Text

import { text } from "spectrum-ts";

await space.send(text("Hello, world."));

// Plain strings are equivalent:
await space.send("Hello, world.");

Attachments

Pass a file path or a Buffer. MIME types are detected from the file extension; override with options.mimeType when you already have the bytes.
import { attachment } from "spectrum-ts";

// From a file path — name and MIME type inferred
await space.send(attachment("/path/to/photo.jpg"));

// From a buffer — provide name and MIME type
await space.send(attachment(buffer, {
  name: "report.pdf",
  mimeType: "application/pdf",
}));
If the MIME type can’t be inferred from the name and you didn’t pass options.mimeType, attachment() throws when the content is built.

Voice

Send a voice note. Same input shape as attachment — a path or a Buffer plus optional metadata.
import { voice } from "spectrum-ts";

// From a file path
await space.send(voice("/path/to/note.m4a"));

// From a buffer — duration in seconds is optional but useful for waveform UIs
await space.send(voice(buffer, {
  name: "note.m4a",
  mimeType: "audio/mp4",
  duration: 12,
}));
Platforms that don’t support voice notes typically downgrade to a regular audio attachment. If the MIME type can’t be inferred and options.mimeType is omitted, the builder throws at send time.
FieldTypeDescription
mimeTypestringThe audio MIME type (e.g. audio/mp4).
namestring (optional)Filename for the underlying clip.
durationnumber (optional)Length in seconds.
sizenumber (optional)Byte length, when known up front.
read()() => Promise<Buffer>Materialize the bytes.
stream()() => Promise<ReadableStream>Stream the bytes — preferred for large clips.

Contacts

Share contact cards. The contact() builder takes either a structured ContactInput, a vCard string, a vcf instance, or a known User paired with optional ContactDetails.
import { contact } from "spectrum-ts";

await space.send(contact({
  name: { first: "Ada", last: "Lovelace" },
  phones: [{ value: "+15551234567", type: "mobile" }],
  emails: [{ value: "ada@example.com", type: "work" }],
}));
fromVCard(vcf) parses a vCard string into a ContactInput; toVCard(contact) serializes a resolved Contact back to vCard.
FieldTypeDescription
name{ formatted?, first?, last?, middle?, prefix?, suffix? }Structured display name.
phonesArray<{ value, type? }>Phone numbers. type is "mobile" | "home" | "work" | "other".
emailsArray<{ value, type? }>Email addresses. type is "home" | "work" | "other".
addressesArray<{ street?, city?, region?, postalCode?, country?, type? }>Postal addresses.
org{ name?, title?, department? }Employer / org info.
urlsstring[]Associated URLs.
birthdaystringISO date.
notestringFree-form note.
photo{ mimeType, read() }Profile photo bytes.
rawunknownProvider-specific extras passed through untouched.
Render a URL as a rich preview card with title, summary, and cover image. Spectrum scrapes Open Graph metadata at send time; pass just the URL and the builder fills in the rest.
import { richlink } from "spectrum-ts";

await space.send(richlink("https://example.com/article"));
title(), summary(), and cover() are lazy async accessors — the metadata fetch happens only if the receiving platform needs it. Platforms without rich-link support fall back to the URL as plain text.

Polls

Send a poll with a title and a list of choices. Each choice can be a plain string or a object — use option() when you want the explicit form.
import { poll, option } from "spectrum-ts";

// Variadic strings
await space.send(poll("Lunch?", "Pizza", "Sushi", "Tacos"));

// Or an array, optionally using option() for clarity
await space.send(poll("Lunch?", [
  option("Pizza"),
  option("Sushi"),
  option("Tacos"),
]));
Poll responses arrive as poll_option content — see Messages for narrowing on incoming votes.

Groups

A group bundles multiple messages into one logical unit (an album of images, a multi-attachment reply). Each item is delivered as its own Message, but they ship together so the receiving platform can render them as a single visual group when supported.
import { group, attachment } from "spectrum-ts";

await space.send(group(
  attachment("/path/to/photo-1.jpg"),
  attachment("/path/to/photo-2.jpg"),
  attachment("/path/to/photo-3.jpg"),
));
Groups don’t nest, and reactions can’t be group members — the builder enforces both at construction time. Platforms that don’t support grouping fall back to sending each item sequentially.

Custom

Send structured, platform-specific payloads. Use this when the receiving platform supports rich content types that don’t fit into the built-in builders.
import { custom } from "spectrum-ts";

await space.send(custom({ type: "card", title: "Order Confirmed" }));
The raw payload round-trips through the provider’s send action — it’s up to the provider to interpret it.

Replies

Send a threaded reply by wrapping content with the message being replied to. See Reactions and replies for the full details and sugar methods.
import { reply, text } from "spectrum-ts";

await space.send(reply(text("Got it"), message));
reply() cannot wrap reply, edit, reaction, group, or typing content.

Edits

Rewrite the content of a previously-sent outbound message. Edits are fire-and-forget — space.send(edit(...)) resolves to undefined.
import { edit, text } from "spectrum-ts";

const sent = await space.send("Draft");
await space.send(edit(text("Final version"), sent));
edit() cannot wrap edit, reply, reaction, group, or typing content.

Typing indicators

Send a typing indicator signal through the content pipeline. Defaults to "start".
import { typing } from "spectrum-ts";

await space.send(typing());        // start typing
await space.send(typing("stop"));  // stop typing
space.startTyping(), space.stopTyping(), and space.responding(fn) are sugar over space.send(typing(...)). Platforms without a typing-indicator API silently no-op.

Composing multiple items

All send methods take a variadic list. Items are sent sequentially as separate messages:
await space.send(
  "Here's the file you requested:",
  attachment("/path/to/document.pdf"),
);
This runs one send() per item on the underlying provider — not a single compound message. Reach for group(...) instead when you specifically want them rendered as one bundled unit.

Replies (sugar)

message.reply(...) has the same variadic signature and delegates to space.send(reply(...)) internally:
await message.reply(
  "Thanks for the question.",
  attachment("/path/to/answer.png"),
);
On platforms without thread support, reply() resolves as a no-op. If you need guaranteed delivery, use space.send(...) instead. See Reactions and replies for the canonical form and more details.