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 withnode-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:
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`);
}
}