ToothFairyAI TS Integration
Simple TypeScript guide to upload files and chat with AI agents using documents.
Prerequisites
const API_KEY = "your-api-key";
const WORKSPACE_ID = "your-workspace-id";
const AGENT_ID = "your-agent-id";
File Path Structure
All uploaded files follow this format:
{folder}/{workspace-id}/{timestamp}{filename}
Folder prefixes by type:
- Documents & Images: imported_doc_files/
- Videos: imported_video_files/
- Audios: imported_audio_files/
Example:
imported_doc_files/6586b7e6-683e-4ee6-a6cf-24c19729b5ff/1760886623830contract.pdf
Step 1: Upload a File
TypeScript Function
interface UploadResult {
  uploadURL: string;
  filePath: string;
}
async function uploadFile(
  file: File,
  workspaceId: string,
  apiKey: string
): Promise<string> {
  // Generate timestamp
  const timestamp = Date.now();
  // Determine folder and import type based on file type
  const fileType = file.type;
  let folder: string;
  let importType: string;
  if (fileType.startsWith('image/')) {
    folder = 'imported_doc_files';
    importType = 'image';
  } else if (fileType.startsWith('video/')) {
    folder = 'imported_video_files';
    importType = 'document';
  } else if (fileType.startsWith('audio/')) {
    folder = 'imported_audio_files';
    importType = 'document';
  } else if (fileType === 'application/pdf') {
    folder = 'imported_doc_files';
    importType = 'pdf';
  } else if (fileType.includes('spreadsheet') || fileType.includes('excel')) {
    folder = 'imported_doc_files';
    importType = 'spreadsheet';
  } else {
    folder = 'imported_doc_files';
    importType = 'document';
  }
  // Build filename with folder prefix
  const filename = `${folder}/${workspaceId}/${timestamp}${file.name}`;
  // Step 1: Get pre-signed URL
  const params = new URLSearchParams({
    filename,
    importType,
    contentType: fileType
  });
  const urlResponse = await fetch(
    `https://api.toothfairyai.com/documents/requestPreSignedURL?${params}`,
    {
      method: 'GET',
      headers: {
        'x-api-key': apiKey,
        'Content-Type': 'application/json'
      }
    }
  );
  const { uploadURL, filePath }: UploadResult = await urlResponse.json();
  // Step 2: Upload file to S3
  const fileBuffer = await file.arrayBuffer();
  await fetch(uploadURL, {
    method: 'PUT',
    headers: {
      'Content-Type': fileType
    },
    body: fileBuffer
  });
  // Step 3: Extract filename for chat
  const chatFilename = filePath.replace(/^s3:\/\/[^/]+\//, '');
  return chatFilename;
}
Usage Example
// Upload a PDF
const pdfFile = document.querySelector('input[type="file"]').files[0];
const uploadedPath = await uploadFile(pdfFile, WORKSPACE_ID, API_KEY);
console.log('Uploaded:', uploadedPath);
// Output: "imported_doc_files/6586b7e6-683e-4ee6-a6cf-24c19729b5ff/1760886623830contract.pdf"
Step 2: Chat with Uploaded Files
TypeScript Function
interface ChatMessage {
  text: string;
  role: 'user' | 'assistant';
  files?: string[];
  images?: string[];
  videos?: string[];
  audios?: string[];
}
interface ChatRequest {
  workspaceid: string;
  agentid: string;
  chatid: string | null;
  messages: ChatMessage[];
}
interface ChatResponse {
  message?: string;
  text?: string;
  chatid?: string;
}
async function chatWithFiles(
  message: string,
  filePaths: string[],
  workspaceId: string,
  agentId: string,
  apiKey: string,
  chatId: string | null = null
): Promise<ChatResponse> {
  // Categorize files by type
  const files: string[] = [];
  const images: string[] = [];
  const videos: string[] = [];
  const audios: string[] = [];
  filePaths.forEach(path => {
    if (path.startsWith('imported_video_files/')) {
      videos.push(path);
    } else if (path.startsWith('imported_audio_files/')) {
      audios.push(path);
    } else if (/\.(jpg|jpeg|png|gif|webp)$/i.test(path)) {
      images.push(path);
    } else {
      files.push(path);
    }
  });
  // Build message object
  const chatMessage: ChatMessage = {
    text: message,
    role: 'user'
  };
  if (files.length > 0) chatMessage.files = files;
  if (images.length > 0) chatMessage.images = images;
  if (videos.length > 0) chatMessage.videos = videos;
  if (audios.length > 0) chatMessage.audios = audios;
  // Build request payload
  const payload: ChatRequest = {
    workspaceid: workspaceId,
    agentid: agentId,
    chatid: chatId,
    messages: [chatMessage]
  };
  // Send request
  const response = await fetch('https://ais.toothfairyai.com/agent', {
    method: 'POST',
    headers: {
      'x-api-key': apiKey,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
  });
  const result: ChatResponse = await response.json();
  return result;
}
Usage Examples
Example 1: Chat with a PDF
const uploadedPdf = await uploadFile(pdfFile, WORKSPACE_ID, API_KEY);
const response = await chatWithFiles(
  "Summarize this contract",
  [uploadedPdf],
  WORKSPACE_ID,
  AGENT_ID,
  API_KEY
);
console.log('AI Response:', response.message || response.text);
console.log('Chat ID:', response.chatid); // Save for follow-up questions
Example 2: Chat with an Image
const uploadedImage = await uploadFile(imageFile, WORKSPACE_ID, API_KEY);
const response = await chatWithFiles(
  "What's in this image?",
  [uploadedImage],
  WORKSPACE_ID,
  AGENT_ID,
  API_KEY
);
console.log('AI Response:', response.message);
Example 3: Chat with Multiple Files
const pdfPath = await uploadFile(pdfFile, WORKSPACE_ID, API_KEY);
const imagePath = await uploadFile(imageFile, WORKSPACE_ID, API_KEY);
const response = await chatWithFiles(
  "Compare the data in the PDF with the chart in the image",
  [pdfPath, imagePath],
  WORKSPACE_ID,
  AGENT_ID,
  API_KEY
);
console.log('AI Response:', response.message);
Example 4: Follow-up Questions (Multi-turn Conversation)
// First question
const response1 = await chatWithFiles(
  "What is this document about?",
  [uploadedPdf],
  WORKSPACE_ID,
  AGENT_ID,
  API_KEY
);
const chatId = response1.chatid;
console.log('Answer 1:', response1.message);
// Follow-up question (use the chatId)
const response2 = await chatWithFiles(
  "Can you elaborate on the payment terms?",
  [uploadedPdf],
  WORKSPACE_ID,
  AGENT_ID,
  API_KEY,
  chatId // Pass chat ID to maintain context
);
console.log('Answer 2:', response2.message);
Complete Example: Upload and Chat
async function processDocument(file: File) {
  try {
    // Step 1: Upload file
    console.log('Uploading file...');
    const filePath = await uploadFile(file, WORKSPACE_ID, API_KEY);
    console.log('✅ Uploaded:', filePath);
    // Step 2: Ask first question
    console.log('Asking first question...');
    const response1 = await chatWithFiles(
      "What is the main topic of this document?",
      [filePath],
      WORKSPACE_ID,
      AGENT_ID,
      API_KEY
    );
    console.log('Answer:', response1.message);
    // Step 3: Ask follow-up question
    console.log('Asking follow-up...');
    const response2 = await chatWithFiles(
      "Can you provide more details?",
      [filePath],
      WORKSPACE_ID,
      AGENT_ID,
      API_KEY,
      response1.chatid // Use chat ID from first response
    );
    console.log('Follow-up Answer:', response2.message);
  } catch (error) {
    console.error('Error:', error);
  }
}
// Usage
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
fileInput.addEventListener('change', () => {
  if (fileInput.files && fileInput.files[0]) {
    processDocument(fileInput.files[0]);
  }
});
Quick Reference
File Path Format
// Format: {folder}/{workspace-id}/{timestamp}{filename}
// Documents/Images
`imported_doc_files/${workspaceId}/${Date.now()}${filename}`
// Videos
`imported_video_files/${workspaceId}/${Date.now()}${filename}`
// Audios
`imported_audio_files/${workspaceId}/${Date.now()}${filename}`
Message Fields by File Type
| File Type | Message Field | Example | 
|---|---|---|
| PDF, Excel, Word, Text | files | { files: ["imported_doc_files/..."] } | 
| PNG, JPG, GIF, WebP | images | { images: ["imported_doc_files/..."] } | 
| MP4, MOV, AVI | videos | { videos: ["imported_video_files/..."] } | 
| MP3, WAV, M4A | audios | { audios: ["imported_audio_files/..."] } | 
Import Types
| File Type | importType Value | 
|---|---|
| "pdf" | |
| Images | "image" | 
| Excel/Spreadsheets | "spreadsheet" | 
| Other Documents | "document" | 
Error Handling
async function safeUploadAndChat(file: File, question: string) {
  try {
    const filePath = await uploadFile(file, WORKSPACE_ID, API_KEY);
    const response = await chatWithFiles(
      question,
      [filePath],
      WORKSPACE_ID,
      AGENT_ID,
      API_KEY
    );
    return response.message || response.text;
  } catch (error) {
    if (error instanceof Error) {
      if (error.message.includes('401')) {
        console.error('Invalid API key');
      } else if (error.message.includes('404')) {
        console.error('File not found - upload may have failed');
      } else {
        console.error('Error:', error.message);
      }
    }
    throw error;
  }
}
Tips
- Always save the chatidfrom responses for follow-up questions
- Use correct folder prefixes: Check file type before building the path
- Timestamp format: Always use Date.now()for millisecond precision
- File size limit: Maximum 15 MB per file
- Multiple files: Pass array of file paths to chatWithFiles()
Last Updated: 2025-10-19
API Method 3: Stream Messages (Real-time)
createStreamingSession(message, agentId, options)
Creates a streaming session that receives AI responses in real-time using Server-Sent Events (SSE).
Parameters
Same as sendToAgent():
createStreamingSession(
  message: string,
  agentId: string,
  options?: {
    attachments?: {
      files?: string[];
      images?: string[];
    };
    showProgress?: boolean;
    chatId?: string;
  }
)
Returns
An event emitter object with these methods:
interface StreamingSession {
  on(event: string, callback: Function): void;
  emit(event: string, data?: any): void;
}
Available Events
| Event | Description | Payload | 
|---|---|---|
| sse_event | Raw Server-Sent Event data | { type, text, chatid, chat_created, ... } | 
| data | Message text chunk | { text: string } | 
| progress | Progress update | { status: string } | 
| status | Status update | { status: string } | 
| chat_created | New chat created | { chatid: string, chat_created: true } | 
| complete | Response complete | { text: string } | 
| end | Stream ended | void | 
| error | Error occurred | Error | 
| unknown | Unknown event type | any | 
Example: Basic Streaming
const session = client.createStreamingSession(
  "Tell me a story",
  "your-agent-id"
);
let fullResponse = "";
// Listen for data chunks
session.on("data", (chunk) => {
  fullResponse = chunk.text;
  console.log("Current response:", chunk.text);
});
// Listen for completion
session.on("end", () => {
  console.log("Final response:", fullResponse);
});
// Listen for errors
session.on("error", (error) => {
  console.error("Stream error:", error);
});
Example: Streaming with Document Attachment
// Upload document first
const uploadResult = await client.uploadFromBase64(base64Data, {
  filename: "report.pdf",
  contentType: "application/pdf"
});
// Create streaming session with document
const session = client.createStreamingSession(
  "Analyze this report and provide key insights",
  "your-agent-id",
  {
    attachments: {
      files: [uploadResult.filename]
    },
    showProgress: true
  }
);
let fullResponse = "";
// Handle SSE events (most detailed)
session.on("sse_event", (eventData) => {
  console.log("SSE Event:", eventData);
  // Extract text from message events
  if (eventData.type === "message" && eventData.text) {
    fullResponse = eventData.text;
    // Update UI in real-time
    updateChatUI(fullResponse);
  }
  // Capture chat ID for follow-up messages
  if (eventData.chat_created && eventData.chatid) {
    saveChatId(eventData.chatid);
  }
});
// Handle completion
session.on("complete", (data) => {
  console.log("Complete response:", data.text);
});
// Handle stream end
session.on("end", () => {
  console.log("Stream finished");
});
// Handle errors
session.on("error", (error) => {
  console.error("Error:", error);
});
Example: Multi-turn Conversation with Streaming
let currentChatId = null;
// First message
const session1 = client.createStreamingSession(
  "What's in this document?",
  "your-agent-id",
  {
    attachments: {
      files: ["workspace-id/document.pdf"]
    }
  }
);
session1.on("sse_event", (eventData) => {
  // Capture chat ID
  if (eventData.chat_created && eventData.chatid) {
    currentChatId = eventData.chatid;
    console.log("Chat created:", currentChatId);
  }
  if (eventData.type === "message" && eventData.text) {
    console.log("Response:", eventData.text);
  }
});
session1.on("end", () => {
  // After first response, send follow-up
  sendFollowUp();
});
function sendFollowUp() {
  const session2 = client.createStreamingSession(
    "Can you elaborate on the first point?",
    "your-agent-id",
    {
      chatId: currentChatId,  // Include chat ID for context
      attachments: {
        files: ["workspace-id/document.pdf"]
      }
    }
  );
  session2.on("sse_event", (eventData) => {
    if (eventData.type === "message" && eventData.text) {
      console.log("Follow-up response:", eventData.text);
    }
  });
}
Complete Workflow Example
Upload Document → Ask Questions → Follow-up
import { ToothFairyAPIClient } from "@/utils/toothfairyApi";
class DocumentChat {
  private client: ToothFairyAPIClient;
  private agentId: string;
  private currentChatId: string | null = null;
  constructor(apiKey: string, workspaceId: string, agentId: string) {
    this.client = new ToothFairyAPIClient(apiKey, workspaceId);
    this.agentId = agentId;
  }
  // Step 1: Upload document
  async uploadDocument(file: File): Promise<string> {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = async () => {
        try {
          const base64 = (reader.result as string).split(',')[1];
          const result = await this.client.uploadFromBase64(base64, {
            filename: file.name,
            contentType: file.type,
            onProgress: (percent) => {
              console.log(`Upload: ${percent}%`);
            }
          });
          console.log('✅ Uploaded:', result.filename);
          resolve(result.filename);
        } catch (error) {
          reject(error);
        }
      };
      reader.readAsDataURL(file);
    });
  }
  // Step 2: Ask question with streaming
  async askQuestion(
    message: string,
    documentPath: string,
    onUpdate: (text: string) => void
  ): Promise<string> {
    return new Promise((resolve, reject) => {
      // Determine if image or file
      const isImage = /\.(jpg|jpeg|png|gif|webp)$/i.test(documentPath);
      const session = this.client.createStreamingSession(
        message,
        this.agentId,
        {
          attachments: isImage
            ? { images: [documentPath] }
            : { files: [documentPath] },
          showProgress: true,
          ...(this.currentChatId && { chatId: this.currentChatId })
        }
      );
      let fullResponse = "";
      session.on("sse_event", (data) => {
        // Capture chat ID
        if (data.chat_created && data.chatid) {
          this.currentChatId = data.chatid;
        }
        // Update with latest text
        if (data.type === "message" && data.text) {
          fullResponse = data.text;
          onUpdate(fullResponse);
        }
      });
      session.on("end", () => {
        resolve(fullResponse);
      });
      session.on("error", (error) => {
        reject(error);
      });
    });
  }
  // Step 3: Ask follow-up question
  async followUp(
    message: string,
    documentPath: string,
    onUpdate: (text: string) => void
  ): Promise<string> {
    if (!this.currentChatId) {
      throw new Error("No active chat. Start with askQuestion() first.");
    }
    // Same as askQuestion, but chatId is already set
    return this.askQuestion(message, documentPath, onUpdate);
  }
}
// Usage
const chat = new DocumentChat(
  "your-api-key",
  "your-workspace-id",
  "your-agent-id"
);
// Upload
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
const documentPath = await chat.uploadDocument(file);
// First question
await chat.askQuestion(
  "What is this document about?",
  documentPath,
  (text) => console.log("Streaming:", text)
);
// Follow-up question (maintains context)
await chat.followUp(
  "Can you summarize the key points?",
  documentPath,
  (text) => console.log("Streaming:", text)
);
Attachment Types: Files vs Images
When to Use files vs images
The API distinguishes between document files and images:
// For PDFs, Excel, Word, etc.
attachments: {
  files: ["workspace-id/document.pdf"]
}
// For images (JPG, PNG, GIF, WebP)
attachments: {
  images: ["workspace-id/photo.jpg"]
}
// You can attach both simultaneously
attachments: {
  files: ["workspace-id/report.pdf"],
  images: ["workspace-id/chart.png"]
}
Automatic Detection Helper
function getAttachmentType(filename: string) {
  const extension = filename.split('.').pop()?.toLowerCase();
  const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
  return imageExtensions.includes(extension || '')
    ? { images: [filename] }
    : { files: [filename] };
}
// Usage
const attachments = getAttachmentType(uploadResult.filename);
const session = client.createStreamingSession(message, agentId, { attachments });
Error Handling
Upload Errors
try {
  const result = await client.uploadFromBase64(base64Data, options);
} catch (error) {
  if (error.message.includes("exceeds 15MB limit")) {
    console.error("File too large");
  } else if (error.message.includes("Invalid base64")) {
    console.error("Invalid file encoding");
  } else if (error.message.includes("Failed to get upload URL")) {
    console.error("Server error - check API key and workspace ID");
  } else {
    console.error("Upload failed:", error.message);
  }
}
Streaming Errors
const session = client.createStreamingSession(message, agentId, options);
session.on("error", (error) => {
  if (error.message.includes("HTTP error")) {
    console.error("Network error - check internet connection");
  } else if (error.message.includes("No response body")) {
    console.error("Invalid response from server");
  } else {
    console.error("Stream error:", error.message);
  }
});
Best Practices
1. Cache Upload Results
const uploadCache = new Map<string, string>();
async function getCachedUpload(file: File): Promise<string> {
  const cacheKey = `${file.name}-${file.size}-${file.lastModified}`;
  if (uploadCache.has(cacheKey)) {
    return uploadCache.get(cacheKey)!;
  }
  const result = await uploadDocument(file);
  uploadCache.set(cacheKey, result.filename);
  return result.filename;
}
2. Always Store Chat IDs
let chatHistory = new Map<string, string>(); // documentId → chatId
session.on("sse_event", (data) => {
  if (data.chat_created && data.chatid) {
    chatHistory.set(documentId, data.chatid);
  }
});
3. Use Progress Callbacks
await client.uploadFromBase64(base64Data, {
  filename: file.name,
  contentType: file.type,
  onProgress: (percent, loaded, total) => {
    updateProgressBar(percent);
    console.log(`${loaded} / ${total} bytes`);
  }
});
4. Handle All Stream Events
const session = client.createStreamingSession(message, agentId, options);
session.on("sse_event", handleSSE);
session.on("data", handleData);
session.on("progress", handleProgress);
session.on("chat_created", handleChatCreated);
session.on("complete", handleComplete);
session.on("end", handleEnd);
session.on("error", handleError);
API Limits
| Limit | Value | 
|---|---|
| Max file size | 15 MB | 
| Supported formats | PDF, Excel, Images (JPG, PNG, GIF, WebP), Documents | 
| Base URL (documents) | https://api.toothfairyai.com | 
| Base URL (agents) | https://ais.toothfairyai.com | 
Troubleshooting
Issue: Upload fails with "Invalid base64"
Solution: Ensure you remove the data URL prefix:
// ❌ Wrong
const base64 = "data:application/pdf;base64,JVBERi0xLj..."
// ✅ Correct
const base64 = "JVBERi0xLj..."
// OR
const base64 = dataUrl.split(',')[1];
Issue: AI doesn't reference the document
Solution: Verify you're using the correct attachment type (files vs images) and the correct filename from upload result:
const result = await client.uploadFromBase64(...);
// Use result.filename, NOT result.originalFilename
const attachments = { files: [result.filename] };
Issue: Follow-up questions lose context
Solution: Make sure to capture and reuse the chat ID:
let chatId = null;
session.on("sse_event", (data) => {
  if (data.chat_created && data.chatid) {
    chatId = data.chatid;  // Store this!
  }
});
// Use in next message
const nextSession = client.createStreamingSession(message, agentId, {
  chatId: chatId,  // Include for context
  attachments: { files: [filename] }
});
TypeScript Type Definitions
// Client
class ToothFairyAPIClient {
  constructor(apiKey: string, workspaceId: string);
  uploadFromBase64(
    base64Data: string,
    options: Base64FileUploadOptions
  ): Promise<FileUploadResult>;
  sendToAgent(
    message: string,
    agentId: string,
    options?: AgentOptions
  ): Promise<string>;
  createStreamingSession(
    message: string,
    agentId: string,
    options?: AgentOptions
  ): StreamingSession;
}
// Options
interface Base64FileUploadOptions {
  filename: string;
  contentType: string;
  importType?: string;
  onProgress?: (percent: number, loaded: number, total: number) => void;
}
interface AgentOptions {
  attachments?: {
    files?: string[];
    images?: string[];
  };
  showProgress?: boolean;
  chatId?: string;
}
// Results
interface FileUploadResult {
  success: boolean;
  originalFilename: string;
  sanitizedFilename: string;
  filename: string;
  importType: string;
  contentType: string;
  size: number;
  sizeInMB: number;
}
// Streaming
interface StreamingSession {
  on(event: string, callback: Function): void;
  emit(event: string, data?: any): void;
}
Last Updated: 2025-10-19