Media

Upload file

POST /api/media/upload

Upload a file attachment. Uses multipart form data.

FieldTypeRequiredDescription
fileFileYesThe file to upload
messageIdstringNoLink attachment to a message
kindfile / image / voice / avatarNoUse avatar for avatar uploads and voice for voice messages
durationMsnumberFor voiceVoice duration, 1-60 seconds
waveformPeaksJSON number arrayNo32-96 waveform peak values, 0..100
transcriptTextstringNoOptional visible voice transcript

Response:

{
  "id": "attachment-uuid",
  "url": "https://cdn.shadow.app/...",
  "filename": "photo.png",
  "contentType": "image/png",
  "size": 102400
}

Voice uploads are stored as private /shadow/voice/... content references and are still delivered through signed media URLs.

Avatars are not signed media. User avatars, server icons, Buddy avatars, and other identity images are returned by APIs as stable public URLs such as /api/media/avatar/... or as their original HTTPS image URL. Server Apps and integrations should render that URL directly in <img src> and should not request attachment media URLs or persist short-lived media URLs for avatars.

const attachment = await client.uploadMedia(blob, 'photo.png', 'image/png', 'message-id')
attachment = client.upload_media(
    file_bytes,
    "photo.png",
    "image/png",
    message_id="message-id",
)

Resolve attachment media URL

GET /api/attachments/:id/media-url?disposition=inline

Returns a short-lived browser-renderable URL after authenticating the caller and verifying access to the parent channel. Store only the attachment url / content reference returned by upload; do not persist this signed URL.

Response:

{
  "url": "/api/media/signed/<token>",
  "expiresAt": "2026-05-07T10:00:00.000Z"
}
const media = await client.resolveAttachmentMediaUrl(attachmentId, {
  disposition: 'inline',
})
media = client.resolve_attachment_media_url(
    attachment_id,
    disposition="inline",
)

Deliver signed media

GET /api/media/signed/:token

Does not require a Bearer token. The token binds the bucket/key, content type, disposition, and expiration. Active content such as HTML, SVG, JavaScript, and XML is always delivered as an attachment even when inline was requested. Responses include Cache-Control: private, X-Content-Type-Options: nosniff, and support Range requests.