Skip to main content
Images and other files use the same conversation key as text. Encrypt bytes with the Chat XDK (encrypt_stream / decrypt_stream), upload via the /2/chat/media/upload routes (sidebar API reference → Media), then attach media_hash_key on encrypt_message. Include media.write with your DM scopes when uploading. Use hyphenated conversation ids in paths (:-). Prefer MIME/dimensions from decrypted bytes. This path is not the Posts media model (expansions=attachments.media_keys, media.fields=variants, etc.). Those parameters apply to Posts; E2EE X Chat blobs are addressed by media_hash_key and X Chat media download.

Encrypt

encrypt_stream / decrypt_stream process the whole payload in memory. For large files, stream_encryptor() / stream_decryptor() return incremental objects (StreamEncryptor / StreamDecryptor): feed chunks with push, then call finish once—finish errors if the stream was truncated.

Upload

Use the request bodies on the OpenAPI pages under API reference → Media. Prefer encrypted blob size where size is required. Finalize yields media_hash_key for attachments and download. Retry transient 5xx with backoff. Python/TypeScript may use the XDK when media helpers exist; otherwise POST with a Bearer token in any language.

Send with an attachment

Encrypt with a media attachment, then POST the send-message body (same field mapping as Getting Started). The SDK generates the message_id and returns it on the payload—send that value, and reuse the same payload on retries so an id is never minted twice.
The conversation key pair can be omitted entirely: with set_cache_keys(true) enabled, encrypt_message resolves the key and version from the conversation’s latest verified key change (see Getting Started).

Download and decrypt

Path: GET /2/chat/media/{conversation_id}/{media_hash_key}. Response body is ciphertext. On inbound messages, read media_hash_key from decrypted attachments / media_hashes. Pick the key by the event’s key version. Each decrypted message event carries the keyVersion (JS; key_version in the other bindings) its content was encrypted under. Decrypt an attachment with the conversation key for that version—conversationKeys.keys[event.keyVersion]—not the latest. After a key rotation (for example a member add), the latest key cannot decrypt media attached to older messages.

Tips

  • Use the same conversation key version as when the media was encrypted
  • Do not log plaintext media or raw keys
  • Detect MIME after decrypt
  • Web clients: encrypt/decrypt on the client when possible; keep OAuth tokens on your server
Full request and response schemas for each media route are under API reference → Media in the sidebar (initialize upload, append chunk, finalize upload, and download media).