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.

im.attachments uploads file bytes to the server, reads attachment metadata, and downloads attachments in chunks. Sending an attachment message is a two-step flow: upload the file to get an attachment GUID, then pass that GUID to the messages API. Message sends accept server attachment GUIDs. They do not accept local file paths.

What You Can Do

NeedUse this when
Upload a regular attachmentYou are sending an image, video, audio file, document, or archive
Upload a Live PhotoYou have a HEIC/HEIF still image plus the matching MOV companion video
Read metadataYou need the file name, MIME type, size, or transfer state
Stream a downloadYou need the attachment bytes; Live Photos may include a companion stream

Upload an Attachment

A regular attachment needs two fields: fileName and data. After upload, send uploaded.attachment.guid.
const uploaded = await im.attachments.upload({
  fileName: "photo.jpg",
  data: await readFile("photo.jpg"),
});

await im.messages.sendAttachment(chat.guid, uploaded.attachment.guid);
upload(...) returns UploadAttachmentResult. For a regular attachment, use the attachment field:
{
  "attachment": {
    "guid": "attachment-guid",
    "fileName": "photo.jpg",
    "mimeType": "image/jpeg",
    "totalBytes": 123456,
    "transferState": "finished"
  }
}
Input shape:
{
  "fileName": "photo.jpg",           // Display file name; keep the extension when you can
  "data":     [255, 216, 255]        // Uint8Array; raw file bytes
}
InputRule
fileNameDisplay file name. Include an extension when possible. The server sanitizes path characters; an empty value falls back to attachment.
dataRaw file bytes. Must not be empty.
The file extension is not strictly required. The server first tries to detect MIME / UTI from the bytes, then falls back to the fileName extension. Files without an extension can still upload, but unknown types may be labeled application/octet-stream / public.data, which usually gives recipients a worse preview. Keep extensions for documents, archives, and Office files. Each uploaded file is limited to 100 MiB by default.

Common Formats

The SDK uploads raw bytes and the server stores them as-is. Messages.app and Apple’s delivery path decide how the recipient sees the file: inline preview, file attachment, transcoded media, or an iCloud link.
TypeCommon formatsNotes
ImagesHEIC, HEIF, JPEG, PNG, GIF, TIFF, BMP, WebP, AVIF, SVGNon-Apple environments may need conversion
VideoMOV, MP4, WebMApple may transcode depending on recipient device support
AudioM4A, MP3, WAV, AIFF, FLAC, CAFPass isAudioMessage: true to sendAttachment(...) for the audio-message UI
DocumentsPDF, DOCX, XLSX, PPTX, TXT, CSV, JSON, HTML, XML, RTFPreview behavior depends on the recipient device
ArchivesZIP, TAR, GZ, BZ2, XZUsually delivered as file attachments
Apple may reject, compress, or convert payloads that are too large or not supported by the recipient path. The SDK uploads bytes; it does not control the final presentation.

Upload a Live Photo

A Live Photo is a paired upload: the primary file is a HEIC/HEIF image, and companion.data is the matching QuickTime .MOV video. When sending, still pass only livePhoto.attachment.guid.
const livePhoto = await im.attachments.upload({
  fileName: "live-photo.HEIC",
  data: await readFile("live-photo.HEIC"),
  companion: {
    data: await readFile("live-photo.MOV"),
  },
});

await im.messages.sendAttachment(chat.guid, livePhoto.attachment.guid);
When the Live Photo upload succeeds, UploadAttachmentResult includes both attachment and companion:
{
  "attachment": {                    // HEIC/HEIF primary image
    "guid": "attachment-guid",
    "fileName": "live-photo.HEIC",
    "mimeType": "image/heic",
    "totalBytes": 123456,
    "transferState": "finished"
  },
  "companion": {                     // Paired MOV video
    "fileName": "live-photo.MOV",
    "mimeType": "video/quicktime",
    "totalBytes": 456789,
    "kind": "live-photo-video"
  }
}
The primary file and companion file are counted separately. Each defaults to the 100 MiB upload limit.
Do not pass a .MOV file as the primary fileName. A Live Photo primary file should be HEIC/HEIF; the MOV belongs in companion.data.

Get Metadata

You do not need get(...) before sending an attachment. Use it when you need to inspect attachment state, display file information, or confirm that a file is ready before downloading.
const attachment = await im.attachments.get(uploaded.attachment.guid);

console.log(attachment.fileName, attachment.mimeType, attachment.totalBytes);
Returns AttachmentInfo. Use transferState to decide whether the attachment is ready to download. Missing or unresolvable attachments throw NotFoundError.
{
  "guid":          "attachment-guid",   // Attachment GUID used for sending and downloading
  "fileName":      "photo.jpg",         // Stored file name
  "mimeType":      "image/jpeg",        // Server-detected MIME type
  "uti":           "public.jpeg",       // Apple Uniform Type Identifier
  "totalBytes":    123456,              // File size in bytes
  "transferState": "finished",          // Current transfer state
  "isOutgoing":    true,                // Uploaded by the current account
  "isHidden":      false,               // Hidden by Apple, for example inline preview artifacts
  "isSticker":     false,               // Used as a sticker
  "companionKind": "live-photo-video",  // Companion kind; may be absent
  "originalGuid":  "original-guid"      // Original attachment GUID; may be absent
}
transferState can be:
ValueMeaning
"pending"Waiting to transfer
"transferring"Transfer in progress
"failed"Transfer failed
"finished"Ready
"unknown"Server could not classify the state

Stream Downloads

Before downloading, check that transferState === "finished" when you can. Otherwise the server may throw attachmentNotReady.
for await (const frame of im.attachments.downloadStream(attachment.guid)) {
  switch (frame.type) {
    case "header":
      console.log(frame.info.fileName, frame.info.mimeType);
      break;

    case "primaryChunk":
      // Append frame.data to the primary output file.
      break;

    case "companionChunk":
      // Append frame.data to the Live Photo companion output file.
      break;
  }
}
The stream emits one header frame first, followed by data chunks. Regular attachments only emit primaryChunk; Live Photos may also emit companionChunk.
frame.typeMeaningExtra fields
headerFirst frame with metadatainfo, companionInfo?
primaryChunkChunk from the primary filedata
companionChunkChunk from the Live Photo companion videodata
{
  "type": "header",
  "info": {                            // Primary AttachmentInfo
    "guid": "attachment-guid",
    "fileName": "photo.jpg"
  },
  "companionInfo": {                   // Present only for Live Photo downloads
    "fileName": "live-photo.MOV",
    "kind": "live-photo-video",
    "mimeType": "video/quicktime",
    "totalBytes": 456789
  }
}
Breaking out of the for await loop cancels the download.
CaseResult
Attachment does not existThrows NotFoundError
Attachment is not readyThrows ValidationError, with error.code === ErrorCode.attachmentNotReady
header.companionInfo is presentThis is a Live Photo download; later frames may include companionChunk
If an attachment is not ready, poll get(...) until transferState becomes "finished", then call downloadStream(...). Minimal save-to-file example:
const { writeFile } = await import("node:fs/promises");

let primaryFileName = "attachment";
let companionFileName: string | undefined;
const primaryChunks: Uint8Array[] = [];
const companionChunks: Uint8Array[] = [];

for await (const frame of im.attachments.downloadStream(attachment.guid)) {
  switch (frame.type) {
    case "header":
      primaryFileName = frame.info.fileName;
      companionFileName = frame.companionInfo?.fileName;
      break;

    case "primaryChunk":
      primaryChunks.push(frame.data);
      break;

    case "companionChunk":
      companionChunks.push(frame.data);
      break;
  }
}

await writeFile(primaryFileName, Buffer.concat(primaryChunks));

if (companionFileName && companionChunks.length > 0) {
  await writeFile(companionFileName, Buffer.concat(companionChunks));
}

Next Steps

  1. Messages — send attachment messages with attachment GUIDs
  2. Error Handling — handle NotFoundError, ValidationError, and attachmentNotReady
  3. Chats — create a chat and get chat.guid