Use a remote attachment to send a file stored outside the message. The backend must offer attachment storage for SDK-managed uploads. An app can also use its own storage through an upload callback.
The SDK has built-in file transfers through client.attachments on Browser and
Node, and client.attachments() on Kotlin and Swift. These calls encrypt,
stage, upload, verify, and decrypt files. An upload callback is not required.
The Agent SDK uses the same transfer service for ctx.sendRemoteAttachment.
For uploads, the operator must configure
attachment storage. Check
client.attachments.offered on Browser and Node or
client.attachments().offered() on Kotlin and Swift before you show the upload
action. Downloads can use any permitted HTTPS host, including
another deployment or app-owned storage. They do not require an attachment
offer from the recipient’s backend.
Call create with a path or bytes, a MIME type, and an optional filename.
Get the RemoteAttachment from the returned pending attachment.
Send that record with RemoteAttachmentCodec.
Call pending.upload() and handle its result.
Creation reads the source once and stores both a local plaintext file and
staged ciphertext. It returns before any upload request. After creation, the
source can move or be removed without affecting the upload. Prefer a path
source for large native files. Path creation and transfers stream the file;
a byte source already holds the file in application memory.
The following examples use an existing client and group. The path must name
a readable file in the application’s filesystem. Browser paths name OPFS entries.
For an in-memory file, use AttachmentSource.Bytes(bytes, filename, mimeType)
on Kotlin or .bytes(bytes:filename:mimeType:) on Swift. Browser path sources
name OPFS entries. Browser byte sources copy the caller’s bytes at the SDK
boundary.
Sending the record does not start the upload. With the sequence above, a
recipient can receive the message before the file is available. Show a pending
state and allow another download attempt if the first returns not_found.
An app can also complete the upload before sending when it needs the object
to be available first.
val downloaded = client.attachments().download(remote)
val path = downloaded.path
val mimeType = downloaded.mimeType
val filename = downloaded.filename
let downloaded =tryawait client.attachments().download(remote: remote)
let path = downloaded.path
let mimeType = downloaded.mimeType
let filename = downloaded.filename
DownloadedAttachment.path names a file with the original content bytes, not
the encoded envelope or ciphertext. The returned MIME type and filename come
from the decrypted envelope. Use the local path to open or display the file.
The SDK does not create a preview or thumbnail.
The SDK checks the ciphertext digest, decrypts and authenticates the envelope,
and checks its attachment type before it puts the plaintext at its final path.
A failed download leaves no partial file there. If a file already exists at
that path, the SDK returns it without a network request. Receiving a message
does not download its attachment. Request downloads when the user opens the
file, or when your app explicitly chooses to fetch it.
Read waiting, uploading, complete, or failed. A failed status includes its cause.
listPending()
Find incomplete uploads after an application restart.
pending(remote)
Recover the pending handle for a known remote attachment.
pending.upload()
Start an upload, join a running upload, or return success for a completed upload.
localPath(remote)
Derive the local path without fetching the object or checking that the file exists.
listLocal()
List local plaintext files. Each record’s path is relative to the attachment directory.
deleteLocal(remote)
Remove local plaintext, staged ciphertext, and attachment records.
Kotlin uses the calls in the table without argument labels. Swift uses
pending(remote:), localPath(remote:), and deleteLocal(remote:). Browser
and Node expose remoteAttachment as a property on the pending handle;
Kotlin and Swift use remoteAttachment().
Pending records survive a restart when the app keeps the same database and
attachment directory. The default maximum pending age is 86,400 seconds.
AttachmentOptions.maxPendingAgeSeconds changes it. Expired records and staged
ciphertext are removed at client creation and by the background cleanup worker.
Successful uploads release staged ciphertext and disappear from listPending.
The local plaintext remains until the app deletes it.
The SDK does not retry failed transfers automatically. Handle the typed
attachment error and its retryable detail. A failed status also carries an
AttachmentFailure with a cause and, when available, an HTTP status. A network
failure can succeed after a delay. Invalid data, an oversized file, or unusable
staged ciphertext needs a corrected input. A credential failure needs the
credential action reported by the error. See the
failure causes before you retry.
Subscribe to client events to show transfer state. Upload and download events
report started, completed, and failed transfers; attachment.deleted reports
local deletion. These are state events, not byte progress updates. Each client
reports only the changes it makes.
Keep the whole client storage directory when you need to resume transfers.
Local deletion does not delete the remote object or its XMTP message. It ends
transfers managed by this client for the deleted attachment. A later download
can fetch the object again while storage still serves it. An unchanged
attachment forwarded in another message shares the same local file.
XMTP messages have a maximum size limit. Files that exceed this limit can’t be sent inline and are instead handled as remote attachments. For this, the file is encrypted, uploaded to an external storage provider, and a reference URL is sent in the message. The recipient then downloads and decrypts the file using the metadata from the message.
The upload and download limits still apply. SDK-managed uploads use the backend’s maxUploadBytes setting, which defaults to 100 MiB. Downloads use the app’s maxDownloadBytes limit, or the backend’s upload limit when the app sets none. Without an attachment offer, the default download limit is 100 MiB. A download is also bounded by the advertised contentLength and the 32-bit length limit.
For app-owned storage, you need three things:
A file to attach (image, document, etc.)
A storage provider to host the encrypted file at an HTTPS URL that answers GET with status 200
An upload callback that tells the SDK how to upload the encrypted bytes and return a download URL
The SDK encrypts the attachment before the upload callback runs. The host stores an encrypted EncodedContent envelope of type xmtp.org/attachment:1.0, which contains the file bytes and metadata.
Property
Value
Cipher
AES-256-GCM
Key derivation
HKDF-SHA256 with a random 32-byte secret and 32-byte salt
Nonce
Random, 12 bytes
Authentication tag
16 bytes, appended to the ciphertext
Integrity
Hex SHA-256 contentDigest of the encrypted bytes
The upload callback’s attachment.payload contains encrypted bytes. The download URL and decryption metadata travel in the XMTP message. A storage host that does not have that metadata cannot decrypt the file.
Type ID: xmtp.org/attachment:1.0. Its fallback is Can't display <filename>. This app doesn't support attachments.. shouldPush defaults to true on all four platforms.
Type ID: xmtp.org/multiRemoteStaticAttachment:1.0. The payload is a MultiRemoteAttachment protobuf with repeated RemoteAttachmentInfo entries. The fallback says that the app does not support multiple remote attachments. shouldPush defaults to true on all four platforms.
Each RemoteAttachmentInfo contains url, contentDigest, secret, salt, nonce, scheme, optional encrypted contentLength, and optional filename. Each new encrypted file has separate encryption material. The SDK record uses byte arrays for secret, salt, and nonce; the single-attachment codec writes those arrays as hexadecimal parameters.
Each attachment in the attachments array contains a URL that points to an encrypted EncodedContent object. The content must be accessible by an HTTP GET request to the URL.
Use MultiRemoteAttachmentCodec with an attachments array of remote attachment records. Each file has its own download and size checks. The message that contains the array must fit the backend’s message envelope limit.
Create an agent from XMTP_* variables. XMTP_BACKEND_URL overrides options.backend; one must be supplied. Pass backend.credentials in options for backend authentication.
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker threads.
In other words, the following example would not work:
Assigning a property on process.env will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
import { env } from'node:process';
env.test =null;
console.log(env.test);
// => 'null'
env.test =undefined;
console.log(env.test);
// => 'undefined'
Use delete to delete a property from process.env.
import { env } from'node:process';
env.TEST=1;
delete env.TEST;
console.log(env.TEST);
// => undefined
On Windows operating systems, environment variables are case-insensitive.
import { env } from'node:process';
env.TEST=1;
console.log(env.test);
// => 1
Unless explicitly specified when creating a Worker instance,
each Worker thread has its own copy of process.env, based on its
parent thread's process.env, or whatever was specified as the env option
to the Worker constructor. Changes to process.env will not be visible
across Worker threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner
unlike the main thread.
@since ― v0.1.27
env.
string |undefined
PINATA_JWT;
const
constpinataGateway:string|undefined
pinataGateway=
var process:NodeJS.Process
process.
NodeJS.Process.env: NodeJS.ProcessEnv
The process.env property returns an object containing the user environment.
See environ(7).
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker threads.
In other words, the following example would not work:
Assigning a property on process.env will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
import { env } from'node:process';
env.test =null;
console.log(env.test);
// => 'null'
env.test =undefined;
console.log(env.test);
// => 'undefined'
Use delete to delete a property from process.env.
import { env } from'node:process';
env.TEST=1;
delete env.TEST;
console.log(env.TEST);
// => undefined
On Windows operating systems, environment variables are case-insensitive.
import { env } from'node:process';
env.TEST=1;
console.log(env.test);
// => 1
Unless explicitly specified when creating a Worker instance,
each Worker thread has its own copy of process.env, based on its
parent thread's process.env, or whatever was specified as the env option
to the Worker constructor. Changes to process.env will not be visible
across Worker threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner
unlike the main thread.
@since ― v0.1.27
env.
string |undefined
PINATA_GATEWAY;
if (!
constpinataJwt:string|undefined
pinataJwt||!
constpinataGateway:string|undefined
pinataGateway) {
thrownew
var Error:ErrorConstructor
new (message?:string, options?:ErrorOptions) =>Error (+1 overload)
Upload encrypted bytes to app-owned storage and return its URL.
AttachmentUploadCallback=async (
attachment: HostedAttachment
attachment) => {
const
constpinata:PinataSDK
pinata=new
newPinataSDK(config?: PinataConfig): PinataSDK
PinataSDK({
pinataJwt?: string |undefined
pinataJwt,
pinataGateway?: string |undefined
pinataGateway,
});
const
constmimeType:"application/octet-stream"
mimeType="application/octet-stream";
const
constencryptedBlob:Blob
encryptedBlob=new
var Blob:new (blobParts?:BlobPart[], options?:BlobPropertyBag) =>Blob
The Blob interface represents a blob, which is a file-like object of immutable, raw data; they can be read as text or binary data, or converted into a ReadableStream so its methods can be used for processing the data.
If array is an Array-like object (that is, one with a length property of
type number), it is treated as if it is an array, unless it is a Buffer or
a Uint8Array. This means all other TypedArray variants get treated as an
Array. To create a Buffer from the bytes backing a TypedArray, use
Buffer.copyBytesFrom().
A TypeError will be thrown if array is not an Array or another type
appropriate for Buffer.from() variants.
Buffer.from(array) and Buffer.from(string) may also use the internal
Buffer pool like Buffer.allocUnsafe() does.
@since ― v5.10.0
from(
attachment: HostedAttachment
attachment.
payload: Uint8Array<ArrayBufferLike>
Encrypted bytes to upload.
payload)], {
BlobPropertyBag.type?: string |undefined
type:
constmimeType:"application/octet-stream"
mimeType,
});
const
constencryptedFile:File
encryptedFile=new
var File:new (fileBits:BlobPart[], fileName:string, options?:FilePropertyBag) =>File
The File interface provides information about files and allows JavaScript in a web page to access their content.
Adds the listener function to the end of the listeners array for the
event named eventName. No checks are made to see if the listener has
already been added. Multiple calls passing the same combination of eventName
and listener will result in the listener being added, and called, multiple
times.
server.on('connection', (stream) => {
console.log('someone connected!');
});
Returns a reference to the EventEmitter, so that calls can be chained.
By default, event listeners are invoked in the order they are added. The
emitter.prependListener() method can be used as an alternative to add the
event listener to the beginning of the listeners array.
// receivedAttachment.content contains the decrypted file bytes
});
The downloadRemoteAttachment utility uses client.attachments.download(). Core checks the URL, size, digest, and authentication tag before it returns the local file. The utility reads that file and returns its filename, MIME type, and bytes.
Downloads require HTTPS. Loopback HTTP is permitted only with allowPrivateNetwork enabled. Native clients check resolved addresses and redirects. Browser downloads require CORS support and reject redirects. See Remote attachments specification for the full transfer rules.
Use remote attachments for files near the message limit. Inline files share that limit with the content envelope and MLS framing.
The backend’s default message envelope limit is 1 MiB. Use the deployment’s current maxEnvelopeBytes limit. A file smaller than 1 MiB can still exceed the limit after encoding and MLS framing.
To handle unsupported content types, refer to the fallback section.