Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /node-sdk-payloads.md.
Verified · 8/11/2026

Primitive Payloads: Streaming Large Attachments

Upload and download large, content-addressed, end-to-end-encrypted attachment objects in bounded memory, then attach them to email by reference instead of inlining bytes.

Use Primitive Payloads, the streaming client for large, content-addressed, end-to-end-encrypted attachment objects, when a file is too big to inline on send, reply, or forward. Inline attachments (base64 content) cover anything under the inline cap (~30 MiB of combined raw bytes); Payloads uploads and downloads in bounded memory instead.

A finalized Payloads object is identified by PushResult.merkleRoot, a 64-character lowercase-hex Merkle root, and decrypted with PushResult.cek, a hex content-encryption key the caller holds. Reference the object on a send instead of inlining its bytes.

Tip

For anything under the inline cap, skip Payloads entirely and pass attachments directly to client.send / client.reply / client.forward. Reach for pushFile / pushBytes only once you're above that threshold, or use client.sendAttachment, which picks inline-vs-reference for you based on size.

Upload and attach a large file#

Upload the file with pushFile, then deliver it by reference through payloadAttachments on client.send. Two steps, no inline bytes.

  1. 1

    Push the file to Payloads storage#

    Stream the file from disk with pushFile (use pushBytes for in-memory data instead). Both are exported from the payloads module of @primitivedotdev/sdk.

    // upload.ts
    import { pushFile } from "@primitivedotdev/sdk/payloads";
    
    const pushed = await pushFile({
      path: "./recording.mp4",
      apiKey: process.env.PRIMITIVE_API_KEY!,
    });
    
    console.log(pushed.merkleRoot); // 64-char lowercase hex
    console.log(pushed.cek);        // hex content-encryption key
    

    pushed is a PushResult: merkleRoot is the content-addressed id for the finalized object, and cek is the hex content-encryption key the recipient needs to decrypt it. Keep both; you need them to reference or download the object later.

  2. 2

    Reference the uploaded object as an attachment#

    Build a SendPayloadReference from the push result and pass it on send via payloadAttachments instead of attachments. Only one payload attachment is supported per send in v1.

    // send.ts
    import primitive from "@primitivedotdev/sdk";
    
    const client = primitive.client({
      apiKey: process.env.PRIMITIVE_API_KEY!,
    });
    
    await client.send({
      from: "Support <support@example.com>",
      to: "alice@example.com",
      subject: "Hello",
      bodyText: "Your recording is attached.",
      payloadAttachments: [
        {
          root: pushed.merkleRoot,
          filename: "recording.mp4",
          contentType: "video/mp4",
          cek: pushed.cek,
        },
      ],
    });
    

    The SDK converts cek (hex, matching PushResult.cek verbatim) to the base64url encoding the wire format expects, so the entire SDK surface, push, pull, and reference, stays hex to the caller.

Tip

Skip the manual push-then-reference dance with client.sendAttachment(...). Pass the normal send fields plus a single attachment (content for in-memory bytes, or path to stream a file from disk), and the SDK sends it inline when it's at or below inlineThreshold (defaults to 25 MiB, the server's inline/offload threshold) and uploads-then-references it otherwise. Provide exactly one of content or path.

Download a payload object#

Use pullFile to stream a Payloads object back down and decrypt it, passing the same merkleRoot and cek the push returned.

// download.ts
import { pullFile } from "@primitivedotdev/sdk/payloads";

await pullFile({
  root: pushed.merkleRoot,
  cek: pushed.cek,
  path: "./downloaded-recording.mp4",
  apiKey: process.env.PRIMITIVE_API_KEY!,
});

pullFile streams and decrypts in bounded memory, so a multi-gigabyte object never has to be resident.

Warning

The CEK is client-held: the SDK's push/pull/reference surface carries it, so if you lose the cek for a merkleRoot you cannot decrypt the object. Persist both together (for example alongside the email record) if you need to re-download later.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer