> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superdocs.app/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript Examples

> Complete working JavaScript examples for the SuperDocs API using fetch and EventSource.

# JavaScript Examples

All examples use the native `fetch` API and work in browsers and Node.js 18+.

<Note>
  For other languages (Python, Go, Ruby, .NET) used in server-side / batch / queue-worker integrations, see [Server Integration](/guides/server-integration). For AI agent tool registration patterns (OpenAI, Anthropic, LangChain, LlamaIndex), see [Agent Tool Integration](/guides/agent-tool-integration).
</Note>

## Basic chat with document

```javascript theme={null}
const API_KEY = "sk_YOUR_API_KEY";
const BASE = "https://api.superdocs.app";

async function chat(message, sessionId, documentHtml = null) {
  const response = await fetch(`${BASE}/v1/chat`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      message,
      session_id: sessionId,
      document_html: documentHtml
    })
  });

  return response.json();
}

// Usage
const data = await chat(
  "Add an introduction paragraph",
  "js-demo",
  "<h1>My Document</h1><p>Content here.</p>"
);

console.log("AI:", data.response);
if (data.document_changes) {
  console.log("Updated HTML:", data.document_changes.updated_html);
}
```

## EventSource streaming

```javascript theme={null}
async function streamChat(message, sessionId, documentHtml) {
  // 1. Start async job
  const response = await fetch(`${BASE}/v1/chat/async`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      message,
      session_id: sessionId,
      document_html: documentHtml
    })
  });

  const { job_id } = await response.json();

  // 2. Open SSE stream
  return new Promise((resolve, reject) => {
    const url = `${BASE}/v1/chat/${sessionId}/stream?job_id=${job_id}&api_key=${API_KEY}`;
    const eventSource = new EventSource(url);

    eventSource.addEventListener("intermediate", (event) => {
      const data = JSON.parse(event.data);
      console.log("Progress:", data.content);
    });

    eventSource.addEventListener("final", (event) => {
      const data = JSON.parse(event.data);
      eventSource.close();
      resolve(data.result);
    });

    eventSource.addEventListener("usage", (event) => {
      const data = JSON.parse(event.data);
      console.log(`Operations: ${data.monthly_used}/${data.monthly_limit}`);
    });

    eventSource.addEventListener("error", (event) => {
      eventSource.close();
      if (event.data) {
        reject(new Error(JSON.parse(event.data).error));
      }
    });
  });
}

// Usage
const result = await streamChat(
  "Rewrite section 2 to be more formal",
  "streaming-demo",
  currentHtml
);
console.log("AI:", result.response);
```

## Upload document and export

```javascript theme={null}
const API_KEY = "sk_YOUR_API_KEY";
const BASE = "https://api.superdocs.app";
const headers = { Authorization: `Bearer ${API_KEY}` };

// Upload a file as the active document
async function uploadDocument(file, sessionId) {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("session_id", sessionId);

  const res = await fetch(`${BASE}/v1/documents/upload`, {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}` },
    body: formData,
  });
  return res.json(); // { html, session_id, filename, chunks_count, version_id }
}

// Export and download a document. Supported formats: "docx" (default),
// "pdf", "html", "markdown", "txt".
async function exportDocument(sessionId, format = "docx", options = {}) {
  const res = await fetch(`${BASE}/v1/documents/export`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({ session_id: sessionId, format, options }),
  });

  // Surface any non-fatal warnings from the export pipeline
  const warningsHeader = res.headers.get("X-Export-Warnings");
  if (warningsHeader) {
    const warnings = JSON.parse(atob(warningsHeader));
    console.warn(`Export completed with ${warnings.length} warning(s):`, warnings);
  }

  const blob = await res.blob();

  // File extension mirrors the format value, with one exception
  const ext = format === "markdown" ? "md" : format;

  // Trigger download in browser
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `document.${ext}`;
  a.click();
  URL.revokeObjectURL(url);
}

// Example: PDF export with paper-size override
await exportDocument("my-session", "pdf", { paper_size: "A4", watermark_text: "DRAFT" });
```

See [Exporting documents](/concepts/documents#exporting-documents) for the full options reference and the upload-then-export pattern for documents over 20 MB.

## File upload

```javascript theme={null}
async function uploadAttachment(file, sessionId) {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("session_id", sessionId);

  const response = await fetch(`${BASE}/v1/attachments/upload`, {
    method: "POST",
    headers: { "Authorization": `Bearer ${API_KEY}` },
    body: formData
  });

  return response.json();
}

// Browser usage
const fileInput = document.querySelector('input[type="file"]');
const result = await uploadAttachment(fileInput.files[0], "my-session");
console.log("Job ID:", result.job_id);
```

## Job polling

```javascript theme={null}
async function pollJob(jobId) {
  while (true) {
    const response = await fetch(`${BASE}/v1/jobs/${jobId}`, {
      headers: { "Authorization": `Bearer ${API_KEY}` }
    });
    const job = await response.json();

    if (job.status === "completed") return job.result;
    if (job.status === "failed") throw new Error(job.error);
    if (job.status === "awaiting_approval") return job;

    await new Promise(r => setTimeout(r, 2000));
  }
}

// Usage
const { job_id } = await uploadAttachment(file, "my-session");
const result = await pollJob(job_id);
console.log("Attachment processed:", result.attachment_id);
```

## HITL approval

```javascript theme={null}
async function chatWithApproval(message, sessionId, documentHtml) {
  // Start with approval mode
  const response = await fetch(`${BASE}/v1/chat/async`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      message,
      session_id: sessionId,
      document_html: documentHtml,
      approval_mode: "ask_every_time"
    })
  });

  const { job_id } = await response.json();

  // Poll and approve
  while (true) {
    const jobResponse = await fetch(`${BASE}/v1/jobs/${job_id}`, {
      headers: { "Authorization": `Bearer ${API_KEY}` }
    });
    const job = await jobResponse.json();

    if (job.status === "completed") return job.result;
    if (job.status === "failed") throw new Error(job.error);

    if (job.status === "awaiting_approval") {
      const changes = job.metadata.pending_changes;
      console.log(`${changes.length} change(s) proposed`);

      // Auto-approve all (replace with UI in production)
      await fetch(`${BASE}/v1/chat/${sessionId}/approve`, {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${API_KEY}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          job_id,
          approved: true,
          changes: changes.map(c => ({ change_id: c.change_id, approved: true }))
        })
      });
    }

    await new Promise(r => setTimeout(r, 2000));
  }
}
```

## HITL approval with SSE streaming (real-time progress)

The polling pattern above is fine for server-to-server flows where no human is watching. For interactive editor integrations — where a user is staring at the chat panel waiting for their edit — open the SSE stream alongside the async job so progress events arrive in real time and the UI never goes silent for more than a few seconds.

```javascript theme={null} theme={null}
async function chatWithApprovalAndStreaming(message, sessionId, documentHtml, callbacks) {
  // 1. Start async job
  const r = await fetch(`${BASE}/v1/chat/async`, {
    method: "POST",
    headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      message,
      session_id: sessionId,
      document_html: documentHtml,
      approval_mode: "ask_every_time",
    }),
  });
  const { job_id } = await r.json();

  // 2. Open SSE stream and surface every event to the UI in real time
  return new Promise((resolve, reject) => {
    const es = new EventSource(
      `${BASE}/v1/chat/${sessionId}/stream?job_id=${job_id}&api_key=${API_KEY}`
    );

    // Render in-flight progress so the UI never looks frozen
    es.addEventListener("intermediate", (event) => {
      const data = JSON.parse(event.data);
      callbacks.onProgress?.(data.content);
    });

    // The whole batch of proposed changes arrives in one event — even a
    // single-change turn comes through here as a one-element changes[] array.
    // (You may also register "proposed_change" defensively, but it never fires
    // today — proposed_change_batch is the event that's emitted.)
    es.addEventListener("proposed_change_batch", (event) => {
      const envelope = JSON.parse(event.data);
      const batch = JSON.parse(envelope.content);  // double-parse — required
      // Surface every change in the batch for review.
      for (const change of batch.changes) {
        callbacks.onProposedChange?.(change, async (decision) => {
          // Send this change's approve/deny decision back. SSE stays open through the cycle.
          await fetch(`${BASE}/v1/chat/${sessionId}/approve`, {
            method: "POST",
            headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
            body: JSON.stringify({
              job_id,
              approved: true,
              changes: [{ change_id: change.change_id, ...decision }],
            }),
          });
        });
      }
    });

    es.addEventListener("final", (event) => {
      es.close();
      resolve(JSON.parse(event.data));
    });

    es.addEventListener("error", (event) => {
      es.close();
      reject(new Error(event.data ? JSON.parse(event.data).error : "SSE stream error"));
    });
  });
}

// Usage in a chat UI:
chatWithApprovalAndStreaming(
  "Tighten the OBLIGATIONS section",
  "session-abc",
  document.querySelector("#editor").innerHTML,
  {
    onProgress: (text) => {
      // Update the in-flight chat bubble — see Streaming guide for the bubble pattern
      updateInFlightBubble(text);
    },
    onProposedChange: (change, respond) => {
      showInlineDiffCard({
        chunkId: change.chunk_id,
        oldHtml: change.old_html,
        newHtml: change.new_html,
        explanation: change.ai_explanation,
        onApprove: () => respond({ approved: true }),
        onDeny: (feedback) => respond({ approved: false, feedback }),
      });
    },
  }
).then((result) => {
  // Replace the in-flight bubble with the final response, swap in result.updated_html if present
  finalizeBubble(result);
});
```

**When to use streaming vs polling.**

* **Streaming** — interactive editor UIs, any flow where a user is watching. Users see progress text within 1–5 seconds, and the turn's proposed changes arrive together in one `proposed_change_batch` event ready to render as a review list.
* **Polling** — background batch processing, server-to-server flows, queues where the user isn't waiting. Simpler to implement; perfectly fine when latency feedback isn't a UX requirement.

See [Streaming → Rendering intermediate events](/guides/streaming#rendering-intermediate-events-in-your-ui) for the in-flight bubble pattern, and [Async jobs → Latency expectations](/guides/async-jobs#latency-expectations-and-timeout-strategy) for typical operation durations.

## Frontend layout

Most products that integrate SuperDocs converge on a two-pane layout: the document editor on the left (primary area) and a chat panel on the right (fixed width, 380–440 px). The skeleton below is framework-agnostic — the same shape works in React, Vue, Svelte, or plain HTML.

```html theme={null} theme={null}
<!-- Framework-agnostic skeleton — adapt to React, Vue, Svelte, or plain HTML. -->
<main class="layout">
  <header class="topbar">
    <span class="app-name">Your App</span>
    <span class="session">session: <code>my-session</code></span>
    <span class="usage-chip">42 / 500 ops</span>
    <button class="export-btn">Export .docx</button>
  </header>

  <section class="editor-pane">
    <!--
      Your rich-text editor mounts here. Whatever editor you use must:
      1. Load SuperDocs HTML via a setHtml() method.
      2. Expose a getHtml() method the chat panel calls before each send.
      3. Preserve data-chunk-id — see /guides/editor-integration.
    -->
  </section>

  <aside class="chat-pane">
    <!-- Message log (user messages + AI progress lines + final responses). -->
    <div class="log"></div>

    <!-- Optional: Approve-All / Deny-All batch bar when multiple proposed changes are pending. -->
    <div class="batch-bar hidden">
      <span class="count">2 proposed changes — review inline</span>
      <button class="approve-all">Approve all</button>
      <button class="deny-all">Deny all</button>
    </div>

    <!-- Input. Reads editor HTML on submit. -->
    <form class="input">
      <input placeholder="Ask the AI to edit the doc..." />
      <button type="submit">Send</button>
    </form>
  </aside>
</main>

<style>
  .layout {
    display: grid;
    grid-template-columns: 1fr 420px;
    grid-template-rows: auto 1fr;
    gap: 24px;
    padding: 24px;
    min-height: 100vh;
  }
  .topbar { grid-column: 1 / 3; display: flex; align-items: center; gap: 16px; }
  .editor-pane { grid-column: 1; min-height: 70vh; }
  .chat-pane { grid-column: 2; display: flex; flex-direction: column; height: calc(100vh - 96px); }
  .log { flex: 1; overflow-y: auto; }
</style>
```

Notes on the pattern:

* **Inline diff overlays** render inside the editor pane (see [Rendering diffs inline in your editor](/guides/human-in-the-loop#rendering-diffs-inline-in-your-editor)). The chat pane only holds the message log and the batch-approve bar.
* **The chat panel reads editor HTML before every send** and writes it back on every `final` event. Expose this through a ref / store so the chat doesn't have to know about your editor's internals.
* **One session per tab.** Generate a random `session_id` on page load and reuse it for every request in that tab. SuperDocs persists conversations across server restarts.
* **API key stays server-side.** If you're building a browser app, proxy SuperDocs calls through your own backend so `sk_...` never reaches the browser. See the [streaming guide](/guides/streaming) for the `api_key` query-parameter workaround for `EventSource`, which cannot set custom headers.
