---
title: "Primitive Payloads: Streaming Large Attachments"
canonical: "https://test.abhinandan.one/node-sdk-payloads"
markdown_url: "https://test.abhinandan.one/node-sdk-payloads.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Node.js SDK"
description: "pushFile, pushBytes, and pullFile stream E2E-encrypted attachment objects in bounded memory and reference them on send instead of inlining bytes."
keywords: ["pushFile", "pushBytes", "pullFile", "SendPayloadReference", "sendAttachment", "Primitive Payloads"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:54:54.485146+00:00"
source_files:
  - "sdk-node/src/api/index.ts"
sections:
  - {anchor: "upload-and-attach-a-large-file", title: "Upload and attach a large file"}
  - {anchor: "step-push-the-file-to-payloads-storage", title: "Push the file to Payloads storage"}
  - {anchor: "step-reference-the-uploaded-object-as-an-attachment", title: "Reference the uploaded object as an attachment"}
  - {anchor: "download-a-payload-object", title: "Download a payload object"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# 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`](https://test.abhinandan.one/node-sdk-sending-email.md). 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. 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`.

```typescript
// 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. 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.

```typescript
// 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.

```typescript
// 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.
