Skip to main content
Use these provider-specific helpers after you have configured the iMessage provider.

Message effects

iMessage supports bubble effects, which animate the sent message bubble, and screen effects, which play a full-screen animation on receive. Wrap any content with effect():
import { attachment } from "spectrum-ts";
import { effect, imessage } from "spectrum-ts/providers/imessage";

await space.send(effect("Happy birthday!", imessage.effect.message.celebration));
await space.send(effect(attachment("/path/to/photo.jpg"), imessage.effect.message.confetti));
The wrapped content can be a string, markdown(...), or any attachment(...). Effects only apply on iMessage. Other platforms see the inner content unchanged.
ConstantValue
imessage.effect.message.slam"com.apple.MobileSMS.expressivesend.impact"
imessage.effect.message.loud"com.apple.MobileSMS.expressivesend.loud"
imessage.effect.message.gentle"com.apple.MobileSMS.expressivesend.gentle"
imessage.effect.message.invisible"com.apple.MobileSMS.expressivesend.invisibleink"
ConstantValue
imessage.effect.message.confetti"com.apple.messages.effect.CKConfettiEffect"
imessage.effect.message.fireworks"com.apple.messages.effect.CKFireworksEffect"
imessage.effect.message.balloons"com.apple.messages.effect.CKBalloonEffect"
imessage.effect.message.heart"com.apple.messages.effect.CKHeartEffect"
imessage.effect.message.lasers"com.apple.messages.effect.CKLasersEffect"
imessage.effect.message.celebration"com.apple.messages.effect.CKHappyBirthdayEffect"
imessage.effect.message.sparkles"com.apple.messages.effect.CKSparklesEffect"
imessage.effect.message.spotlight"com.apple.messages.effect.CKSpotlightEffect"
imessage.effect.message.echo"com.apple.messages.effect.CKEchoEffect"

Chat renaming

Rename a group chat using space.rename() or the canonical rename() content builder:
import { rename } from "spectrum-ts";

// Sugar
await space.rename("Book Club");

// Canonical
await space.send(rename("Book Club"));
Renaming requires cloud mode and only works on group chats. In local mode or on a DM, rename() throws an UnsupportedError.

Group avatars

Set or clear the group chat icon using space.avatar() or the canonical avatar() content builder:
import { avatar } from "spectrum-ts";

// Sugar - set from a file path
await space.avatar("./icon.png");

// Sugar - clear the current avatar
await space.avatar("clear");

// Canonical
await space.send(avatar("./icon.png"));
Read the current icon back with space.getAvatar() — it resolves { data, mimeType } (or undefined when the group has no icon), and the result round-trips into the setter:
const icon = await space.getAvatar();
if (icon) {
  await otherGroup.avatar(icon.data, { mimeType: icon.mimeType });
}
Group avatars require cloud mode and only work on group chats. In local mode or on a DM, avatar() and getAvatar() throw an UnsupportedError.

Group membership

Add or remove members with space.add() / space.remove(), or leave the group with space.leave() — or use the canonical addMember() / removeMember() / leaveSpace() content builders:
import { addMember, leaveSpace, removeMember } from "spectrum-ts";

// Sugar
await space.add("+15553333333");
await space.add([alice, "[email protected]"]); // batches land in one call
await space.remove("+15553333333");
await space.leave();

// Canonical
await space.send(addMember("+15553333333"));
await space.send(removeMember("+15553333333"));
await space.send(leaveSpace());
Members are E.164 phone numbers or emails — the same handles space.create accepts. List the current roster with space.getMembers(). Each entry’s id is the canonical address, so the results feed straight back into add() / remove() / space.create(); the agent’s own number is excluded:
const members = await space.getMembers();
await space.remove(members.filter((m) => m.id.endsWith("@example.com")));

// Typed extras (address/country/service) via the narrowed instance
const im = imessage(app);
const detailed = await im.getMembers(space);
for (const member of detailed) {
  console.log(member.address, member.service);
}
Membership management requires cloud mode and only works on group chats. In local mode or on a DM it throws an UnsupportedError — a DM cannot be converted into a group, so to add someone to a 1:1 conversation, create a group with space.create instead. getMembers() has the same constraints: a DM has no roster to list.

Inbound group events

Inbound group events require a dedicated cloud line, available on the Business plan. Group changes made by other members then arrive as inbound messages on app.messages, carrying the same content types you send:
Shared-pool and local modes do not subscribe to the group-event stream. Your app will not receive membership or group-metadata changes through app.messages in either mode. If you use space.get(chatGuid) to reference an existing group in shared mode, that reference does not enable group events.
Someone…content.typemessage.sender
adds a member"addMember" (members = who was added)who added them
removes a member"removeMember" (members = who was removed)who removed them
leaves the group"leaveSpace"the leaver
renames the group"rename" (displayName)who renamed it
changes the icon"avatar" (action.kind === "set", bytes behind action.read())who changed it
clears the icon"avatar" (action.kind === "clear")who cleared it
for await (const [space, message] of app.messages) {
  if (message.content.type === "addMember") {
    console.log(`${message.sender?.id ?? "someone"} added ${message.content.members.join(", ")}`);
  }
}
Semantics to rely on:
  • message.sender may be undefined — Apple does not always record the actor. The affected member is always present (in members, or as the sender for leaveSpace).
  • The agent’s own actions never echo back: space.add(...), space.rename(...), and friends are suppressed from the inbound stream, matching how self-sent messages behave.
  • Icon-change events carry a snapshot of the icon fetched at event time; an icon that was already replaced or removed by then is skipped (the follow-up event carries the current state).
  • On dedicated lines, group events ride the same durable catch-up log as messages and polls, so changes that happen while the app is down are replayed on reconnect. After a cursor gap, reconcile with space.getMembers() / space.getAvatar().

Chat backgrounds

Set or clear the chat background image. Import background from the iMessage provider and use the sugar method on a narrowed space:
import { background, imessage } from "spectrum-ts/providers/imessage";

const im = imessage(space);

// Set from a file path - MIME type inferred from the extension
await im.background("./wallpaper.jpg");

// Set from a buffer - mimeType is required
await im.background(buffer, { mimeType: "image/jpeg" });

// Clear the current background
await im.background("clear");
space.background(...) is sugar for space.send(background(...)). The canonical form works on any space reference:
await space.send(background("./wallpaper.jpg"));
await space.send(background("clear"));
After a successful set, the background usually syncs to other users’ devices within 30s. The background asset is uploaded to iCloud and then distributed to the other members of the conversation. Display time is not a hard SLA: network state, iCloud state, and the Messages client state can all affect when the UI appears.
StageWhat happens
Before background(...) resolvesThe provider waits until the background asset reaches a distributable state.
After background(...) resolvesThe conversation has accepted the background change; iCloud distributes the asset to other members.
Other members’ devicesThe background appears after the device receives the iCloud distribution.
Background UI may not appear in these cases:
CaseResult
The recipient’s network, iCloud, or Messages state is unhealthyThe background may appear late, often after reopening Messages.
A group member has never spoken, interacted, or is treated by the system as unknown or untrustedApple may not show the background UI to that member.
The second case is an Apple Messages display limit, not a Spectrum option. If one group member never sees the background, have that member send a message in the group, mark the sender as known, or reopen Messages before retrying the background change.
Chat backgrounds require cloud mode. In local mode, background() throws an UnsupportedError.
The string "clear" is a reserved sentinel. If you have a file literally named clear with no extension, pass "./clear" or load it as a Buffer.

Mini-app cards

Send a customized iMessage mini-app card: a remote-only rich bubble that shows app metadata, a deep link, and a visual layout. Import customizedMiniApp from the iMessage provider:
import { customizedMiniApp } from "spectrum-ts/providers/imessage";

const sent = await space.send(customizedMiniApp({
  appName: "My App",
  extensionBundleId: "com.example.myapp.imessage",
  teamId: "ABCDE12345",
  url: "https://example.com/deep-link",
  layout: {
    caption: "Check this out",
    subcaption: "Tap to open",
  },
}));
space.send(customizedMiniApp(...)) returns the sent message record. Unlike background and read, mini-app cards are real outbound messages. appStoreId is optional. Omit it to send a card whose extension isn’t published on the App Store. When set, recipients without the extension are directed to its App Store entry.
FieldTypeDescription
appNamestringDisplay name of the owning app, shown by Messages fallback UI.
appStoreIdnumber (optional)Apple App Store numeric ID. Omit for unpublished extensions.
extensionBundleIdstringBundle identifier of the iMessage extension target.
layoutCustomizedMiniAppLayoutVisible card layout. See below.
teamIdstring10-character uppercase alphanumeric Apple Team ID.
urlstringAbsolute URL delivered to the installed extension on tap.
FieldTypeDescription
captionstring (optional)Primary caption text.
subcaptionstring (optional)Secondary caption text.
trailingCaptionstring (optional)Right-aligned primary text.
trailingSubcaptionstring (optional)Right-aligned secondary text.
imageUint8Array (optional)Image data for the card. Must be set with imageTitle.
imageTitlestring (optional)Title for the image. Must be set with image.
imageSubtitlestring (optional)Subtitle for the image. Requires image.
summarystring (optional)Fallback text for surfaces that can’t render the card, such as notifications and lock screen.
At least one of caption, subcaption, trailingCaption, trailingSubcaption, or image must be set.
Mini-app cards require cloud mode. In local mode, customizedMiniApp() throws an UnsupportedError.

Native contact card sharing

Share the bot account’s own iMessage contact card directly in a chat. Use nativeContactCard() to build the content, or the space.shareContactCard() sugar method on a narrowed iMessage space:
import { imessage } from "spectrum-ts/providers/imessage";

const im = imessage(space);
await im.shareContactCard();
This shares the bot’s own contact card as it appears in iMessage. Recipients can tap it to save the contact. Use it in onboarding flows where you want users to add your bot to their contacts.
Native contact card sharing requires cloud mode. In local mode, nativeContactCard() throws an UnsupportedError.

Fetching attachments

Retrieve an attachment by its iMessage GUID using getAttachment on the narrowed platform instance. The returned Attachment is lazy. .read() and .stream() each trigger an independent download, so cache .read() if you need the bytes more than once.
import { imessage } from "spectrum-ts/providers/imessage";

const im = imessage(app);
const att = await im.getAttachment("p:0/GUID");

if (att) {
  console.log(att.name, att.mimeType, att.size);
  const bytes = await att.read();
}
In multi-phone mode, pass the phone number as the second argument to route the request through the correct instance:
const att = await im.getAttachment("p:0/GUID", "+15559999999");
When only one phone is configured, or in shared-pool mode, the phone parameter is optional.
getAttachment requires cloud mode. In local mode it throws an UnsupportedError.

Tapback constants

iMessage uses a fixed set of tapback reactions. The imessage object exposes them as constants:
ConstantValue
imessage.tapbacks.love"love"
imessage.tapbacks.like"like"
imessage.tapbacks.dislike"dislike"
imessage.tapbacks.laugh"laugh"
imessage.tapbacks.emphasize"emphasize"
imessage.tapbacks.question"question"
import { imessage } from "spectrum-ts/providers/imessage";

await message.react(imessage.tapbacks.laugh);
See Reactions and replies for the cross-platform reaction model.