Skip to content

JavaScript / TypeScript Integration Guide

This guide covers building browser and Node.js applications on top of the Beyond Retrieval v2 API using the standard fetch API.


Prerequisites

  • Browser: Any modern browser (Chrome, Firefox, Safari, Edge)
  • Node.js: 18+ (built-in fetch) or 16 with node-fetch

No additional packages are required for Node.js 18+. For TypeScript, the type definitions below give you full type safety.


Authentication Setup

const BASE_URL = "http://localhost:8000/api";
const TOKEN = "dev"; // Any string works in bypass mode

const headers = {
  Authorization: `Bearer ${TOKEN}`,
  "Content-Type": "application/json",
};

Production Tokens

In production, obtain a JWT from your authentication provider. See the Authentication docs for details.


TypeScript Type Definitions

Core types for the API response envelope and common schemas:

// ── Response Envelope ────────────────────────────────────────────────
interface APIResponse<T = any> {
  success: boolean;
  data: T;
  error: string | null;
}

// ── Notebooks ────────────────────────────────────────────────────────
interface Notebook {
  notebook_id: string;
  notebook_title: string;
  notebook_description: string;
  icon: string;
  db_type: "cloud" | "local";
  storage_provider: "supabase" | "s3" | "local" | "none";
  number_of_documents: number;
  created_at: string;
  updated_at: string;
}

interface NotebookCreate {
  notebook_id: string;
  notebook_title: string;
  notebook_description?: string;
  icon?: string;
  embedding_model?: string;
  db_type?: "cloud" | "local";
  storage_provider?: "supabase" | "s3" | "local" | "none";
  user_id: string;
}

// ── Documents ────────────────────────────────────────────────────────
interface UploadedFile {
  file_id: string;
  file_name: string;
  file_type: string;
  storage_path: string;
  size: number;
}

interface IngestSettings {
  parser?: "Docling Parser" | "Mistral OCR";
  chunking_strategy?: "Recursive Chunking" | "Agentic Chunking";
  chunk_size?: number;
  chunk_overlap?: number;
  enable_contextual_retrieval?: boolean;
  enable_multimodal_processing?: boolean;
}

// ── Chat ─────────────────────────────────────────────────────────────
interface Conversation {
  conversation_id: string;
  notebook_id: string;
  user_id: string;
  title: string;
  chat_mode: string;
  is_pinned: boolean;
  is_archived: boolean;
  message_count: number;
  last_message_at: string;
  created_at: string;
}

interface Citation {
  citation_id: number;
  rank: number;
  content: string;
  metadata: Record<string, any>;
  similarity: number;
}

interface Message {
  id: string;
  conversation_id: string;
  role: "user" | "assistant";
  content: string;
  citations: Citation[];
  run_metadata: Record<string, any> | null;
  created_at: string;
}

interface SendMessageRequest {
  content: string;
  chat_mode?: "rag";
  strategy_id?: string;
  persona?: "funny" | "professional" | "mentor" | "storyteller" | "clear" | "custom";
  language?: "en" | "de" | "es" | "fr" | "it" | "pt" | "nl" | "ru" | "zh" | "ja";
  custom_persona_text?: string | null;
  selected_file_ids?: string[];
}

interface SendMessageResponse {
  user_message: Message;
  assistant_message: Message;
}

// ── Retrieval ────────────────────────────────────────────────────────
interface RetrievalChunk {
  rank: number;
  content: string;
  score: number;
  metadata: Record<string, any>;
  file_name: string;
  file_id: string;
  chunk_index: number;
}

interface RetrievalResult {
  strategy_id: string;
  query: string;
  chunks: RetrievalChunk[];
  total_results: number;
  execution_time_ms: number;
}

Client Wrapper

A reusable wrapper that handles the response envelope, error checking, and content types:

class BeyondRetrievalClient {
  constructor(baseUrl = "http://localhost:8000/api", token = "dev") {
    this.baseUrl = baseUrl.replace(/\/+$/, "");
    this.token = token;
  }

  get headers() {
    return {
      Authorization: `Bearer ${this.token}`,
      "Content-Type": "application/json",
    };
  }

  async request(method, path, { body, params, timeout = 60000 } = {}) {
    let url = `${this.baseUrl}${path}`;
    if (params) {
      const qs = new URLSearchParams(params).toString();
      url += `?${qs}`;
    }

    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeout);

    try {
      const options = {
        method,
        headers: this.headers,
        signal: controller.signal,
      };
      if (body !== undefined) {
        options.body = JSON.stringify(body);
      }

      const response = await fetch(url, options);

      if (!response.ok) {
        const text = await response.text().catch(() => "");
        throw new Error(`HTTP ${response.status}: ${text}`);
      }

      const json = await response.json();
      if (!json.success) {
        throw new Error(`API error: ${json.error || "Unknown error"}`);
      }
      return json.data;
    } finally {
      clearTimeout(timer);
    }
  }

  get(path, opts) {
    return this.request("GET", path, opts);
  }

  post(path, body, opts) {
    return this.request("POST", path, { body, ...opts });
  }

  put(path, body, opts) {
    return this.request("PUT", path, { body, ...opts });
  }

  patch(path, body, opts) {
    return this.request("PATCH", path, { body, ...opts });
  }

  delete(path, opts) {
    return this.request("DELETE", path, opts);
  }

  async uploadFiles(notebookId, files) {
    const formData = new FormData();
    for (const file of files) {
      formData.append("files", file);
    }

    const response = await fetch(
      `${this.baseUrl}/notebooks/${notebookId}/documents/upload`,
      {
        method: "POST",
        headers: { Authorization: `Bearer ${this.token}` },
        body: formData,
      }
    );

    const json = await response.json();
    if (!json.success) {
      throw new Error(`Upload error: ${json.error}`);
    }
    return json.data;
  }
}

Usage:

const client = new BeyondRetrievalClient();
const notebooks = await client.get("/notebooks/");
console.log(`Found ${notebooks.length} notebooks`);

End-to-End Example

A complete workflow: create a notebook, upload files, ingest, chat, and collect feedback.

const BASE_URL = "http://localhost:8000/api";
const TOKEN = "dev";

const headers = {
  Authorization: `Bearer ${TOKEN}`,
  "Content-Type": "application/json",
};

// ── 1. Create a notebook ──────────────────────────────────────────────
const notebookId = crypto.randomUUID();
let response = await fetch(`${BASE_URL}/notebooks/`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    notebook_id: notebookId,
    notebook_title: "JS Integration Test",
    user_id: "dev-user",
    embedding_model: "openai/text-embedding-3-small",
  }),
});
let notebook = (await response.json()).data;
console.log(`Created notebook: ${notebook.notebook_id}`);

// ── 2. Upload files ──────────────────────────────────────────────────
const formData = new FormData();
formData.append("files", fileInput.files[0]); // from <input type="file">

response = await fetch(
  `${BASE_URL}/notebooks/${notebookId}/documents/upload`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}` },
    body: formData,
  }
);
const uploaded = (await response.json()).data;
console.log(`Uploaded ${uploaded.length} file(s)`);

// ── 3. Start ingestion ───────────────────────────────────────────────
const filesPayload = uploaded.map((f) => ({
  file_id: f.file_id,
  file_name: f.file_name,
  file_path: f.storage_path,
}));

response = await fetch(
  `${BASE_URL}/notebooks/${notebookId}/documents/ingest`,
  {
    method: "POST",
    headers,
    body: JSON.stringify({
      files: filesPayload,
      settings: {
        parser: "Docling Parser",
        chunking_strategy: "Recursive Chunking",
        chunk_size: 1000,
        chunk_overlap: 200,
      },
      notebook_name: "JS Integration Test",
    }),
  }
);
const jobs = (await response.json()).data.jobs;
console.log(`Started ${jobs.length} ingestion job(s)`);

// ── 4. Poll ingestion status ─────────────────────────────────────────
const fileId = uploaded[0].file_id;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

while (true) {
  response = await fetch(
    `${BASE_URL}/notebooks/${notebookId}/documents/${fileId}/stage`,
    { headers: { Authorization: `Bearer ${TOKEN}` } }
  );
  const stage = (await response.json()).data;
  console.log(`  Status: ${stage.status}, Stage: ${stage.stage ?? "N/A"}`);

  if (stage.status === "success" || stage.status === "error") break;
  await sleep(3000);
}

// ── 5. Create a conversation ─────────────────────────────────────────
response = await fetch(
  `${BASE_URL}/notebooks/${notebookId}/conversations`,
  {
    method: "POST",
    headers,
    body: JSON.stringify({ title: "Test Chat", chat_mode: "rag" }),
  }
);
const conv = (await response.json()).data;
const conversationId = conv.conversation_id;
console.log(`Created conversation: ${conversationId}`);

// ── 6. Send a RAG message ────────────────────────────────────────────
response = await fetch(
  `${BASE_URL}/notebooks/${notebookId}/conversations/${conversationId}/messages`,
  {
    method: "POST",
    headers,
    body: JSON.stringify({
      content: "What is the refund policy?",
      chat_mode: "rag",
      strategy_id: "fusion",
      persona: "professional",
      language: "en",
    }),
  }
);
const result = (await response.json()).data;
const assistantMsg = result.assistant_message;
console.log(`\nAI: ${assistantMsg.content.slice(0, 200)}...`);

// ── 7. Print citations ───────────────────────────────────────────────
for (const cite of assistantMsg.citations ?? []) {
  console.log(
    `  [${cite.citation_id}] ${cite.metadata?.file_name ?? "N/A"} (score: ${cite.similarity})`
  );
}

// ── 8. Submit feedback ───────────────────────────────────────────────
response = await fetch(
  `${BASE_URL}/notebooks/${notebookId}/messages/${assistantMsg.id}/feedback`,
  {
    method: "POST",
    headers,
    body: JSON.stringify({
      is_positive: true,
      feedback_text: "Accurate and helpful",
    }),
  }
);
console.log(`Feedback saved: ${JSON.stringify((await response.json()).data)}`);

File Upload with FormData

// From an <input type="file" multiple> element
const input = document.getElementById("file-input");

const formData = new FormData();
for (const file of input.files) {
  formData.append("files", file);
}

const response = await fetch(
  `${BASE_URL}/notebooks/${notebookId}/documents/upload`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}` },
    // Do NOT set Content-Type -- the browser sets it with the boundary
    body: formData,
  }
);
const uploaded = (await response.json()).data;
import { readFile } from "node:fs/promises";
import { basename } from "node:path";

const filePath = "./handbook.pdf";
const fileBuffer = await readFile(filePath);
const fileName = basename(filePath);

const formData = new FormData();
formData.append("files", new Blob([fileBuffer]), fileName);

const response = await fetch(
  `${BASE_URL}/notebooks/${notebookId}/documents/upload`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}` },
    body: formData,
  }
);
const uploaded = (await response.json()).data;
console.log(`Uploaded: ${uploaded[0].file_name}`);

Error Handling

async function apiCall(method, url, options = {}) {
  try {
    const response = await fetch(url, {
      method,
      headers: {
        Authorization: `Bearer ${TOKEN}`,
        "Content-Type": "application/json",
      },
      ...options,
    });

    // HTTP-level errors
    if (response.status === 401) {
      throw new Error("Unauthorized: check your authentication token");
    }
    if (response.status === 403) {
      throw new Error("Forbidden: admin access required");
    }
    if (response.status === 404) {
      throw new Error("Not found: the requested resource does not exist");
    }
    if (response.status === 409) {
      throw new Error("Conflict: resource is being processed or storage disabled");
    }
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${await response.text()}`);
    }

    // Application-level errors
    const json = await response.json();
    if (!json.success) {
      throw new Error(`API error: ${json.error ?? "Unknown error"}`);
    }

    return json.data;
  } catch (error) {
    if (error.name === "AbortError") {
      throw new Error("Request timed out");
    }
    if (error.name === "TypeError" && error.message.includes("fetch")) {
      throw new Error("Cannot connect to the API. Is the server running?");
    }
    throw error;
  }
}

// Usage
try {
  const notebooks = await apiCall("GET", `${BASE_URL}/notebooks/`);
  console.log(`Found ${notebooks.length} notebooks`);
} catch (error) {
  console.error(`Error: ${error.message}`);
}

Browser-Specific Patterns

CORS

When calling the API from a browser on a different origin, the server must include the correct CORS headers. Beyond Retrieval v2 is configured with allow_origins=["*"] in development. For production, set the CORS_ORIGINS environment variable:

CORS_ORIGINS=https://app.example.com,https://admin.example.com

Credentials

If your authentication provider sets cookies, pass credentials: "include":

const response = await fetch(`${BASE_URL}/notebooks/`, {
  headers: { Authorization: `Bearer ${TOKEN}` },
  credentials: "include",
});

Retrieval Strategies

const notebookId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";

// List strategies
const strategies = await client.get(
  `/notebooks/${notebookId}/retrieval/strategies`
);
for (const s of strategies) {
  const tag = s.requires_llm ? " [LLM]" : "";
  console.log(`  ${s.id}: ${s.name}${tag}`);
}

// Execute a search
const result = await client.post(
  `/notebooks/${notebookId}/retrieval/retrieve`,
  {
    query: "What are the return policies?",
    strategy_id: "fusion",
    top_k: 10,
  }
);
console.log(`Found ${result.total_results} chunks in ${result.execution_time_ms}ms`);
for (const chunk of result.chunks) {
  console.log(`  [${chunk.rank}] ${chunk.content.slice(0, 80)}... (${chunk.score.toFixed(3)})`);
}

Enhancement Pipeline

const notebookId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";

// 1. List files
const files = await client.get(`/notebooks/${notebookId}/enhance/files`);
const pendingFiles = files.filter((f) => f.pending > 0);

// 2. Start enhancement
if (pendingFiles.length > 0) {
  await client.post(`/notebooks/${notebookId}/enhance`, {
    file_ids: pendingFiles.map((f) => f.file_id),
  });

  // 3. Poll until complete
  for (const f of pendingFiles) {
    while (true) {
      const status = await client.get(
        `/notebooks/${notebookId}/enhance/status`,
        { params: { file_id: f.file_id } }
      );
      console.log(`  ${f.file_name}: ${status.progress_pct.toFixed(1)}%`);
      if (status.all_terminated) break;
      await new Promise((r) => setTimeout(r, 4000));
    }

    // 4. Publish
    const result = await client.post(
      `/notebooks/${notebookId}/enhance/publish`,
      {
        file_id: f.file_id,
        file_name: f.file_name,
        notebook_title: "My Notebook",
      }
    );
    console.log(`  Published ${result.published_count} chunks`);
  }
}