Skip to main content

JavaScript Examples

All examples use the native fetch API and work in browsers and Node.js 18+.
For other languages (Python, Go, Ruby, .NET) used in server-side / batch / queue-worker integrations, see Server Integration. For AI agent tool registration patterns (OpenAI, Anthropic, LangChain, LlamaIndex), see Agent Tool Integration.

Basic chat with document

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

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

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 as Word file
async function exportDocument(sessionId, format = "docx") {
  const res = await fetch(`${BASE}/v1/documents/export`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({ session_id: sessionId, format }),
  });
  const blob = await res.blob();

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

File upload

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

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

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.
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);
    });

    // Each proposed change appears as soon as the AI generates it
    es.addEventListener("proposed_change", (event) => {
      const envelope = JSON.parse(event.data);
      const change = JSON.parse(envelope.content);  // double-parse — required
      callbacks.onProposedChange?.(change, async (decision) => {
        // Send the user'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,
            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 proposed changes appear inline the moment the AI generates them.
  • 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 for the in-flight bubble pattern, and Async jobs → Latency expectations 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.
<!-- 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). 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 for the api_key query-parameter workaround for EventSource, which cannot set custom headers.