Partner API
Integration Guide

Integration Guide

Step-by-step guide to integrate TutorQ search into your application.

Prerequisites

  • A TutorQ partner account with API key
  • Course materials uploaded and processed
  • Your backend server (API calls should be server-to-server, not from client)

Step 1: Get Your API Key

Contact us at venkat@q3learners.com with:

  • Your organization name
  • What course materials you want to process
  • Your app/platform description

We'll set up:

  1. A university tenant for your organization
  2. A professor account for uploading materials
  3. An API key tied to your tenant

Step 2: Upload Course Materials

Log in to the professor dashboard and upload your course materials:

  • Supported formats: PDF, PPTX, lecture notes
  • Processing time: 5-45 minutes depending on size
  • What happens: MinerU extraction → chunking → metadata enrichment → dual embeddings

You'll see the status change from PENDINGPROCESSINGCOMPLETED.

Step 3: Integrate the Search API

Server-Side Integration (Recommended)

Your backend calls TutorQ, not the mobile/web client directly.

Python — Full Example:

import requests
import os
 
API_URL = 'https://api.tutorq.ai/api/v1/partner'
HEADERS = {
    'Content-Type': 'application/json',
    'x-api-key': os.getenv('TUTORQ_API_KEY'),
}
 
# Step 1: Get available courses (call once, cache the IDs)
def get_courses() -> list:
    response = requests.get(f'{API_URL}/courses', headers=HEADERS)
    response.raise_for_status()
    data = response.json()
    return data['courses']
 
# Step 2: Search a specific course
def search(query: str, course_id: str, top_k: int = 5) -> dict:
    response = requests.post(
        f'{API_URL}/search',
        headers=HEADERS,
        json={
            'query': query,
            'course_id': course_id,
            'top_k': top_k,
        },
    )
    response.raise_for_status()
    return response.json()
 
# Usage
courses = get_courses()
print(f"Available courses: {[c['name'] for c in courses]}")
 
# Search the first course
results = search('What is the anatomy of the retina?', courses[0]['id'])
 
for r in results['results']:
    print(f"\n📖 {r['title']}")
    print(f"   {r['content'][:200]}...")
    if r.get('key_terms'):
        print(f"   Terms: {', '.join(r['key_terms'])}")
    if r.get('images'):
        for img in r['images']:
            print(f"   🖼️  {img}")

Node.js — Full Example:

const API_URL = 'https://api.tutorq.ai/api/v1/partner';
const HEADERS = {
  'Content-Type': 'application/json',
  'x-api-key': process.env.TUTORQ_API_KEY,
};
 
// Step 1: Get available courses (call once, cache the IDs)
async function getCourses() {
  const res = await fetch(`${API_URL}/courses`, { headers: HEADERS });
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  const data = await res.json();
  return data.courses;
}
 
// Step 2: Search a specific course
async function search(query, courseId, topK = 5) {
  const res = await fetch(`${API_URL}/search`, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({ query, course_id: courseId, top_k: topK }),
  });
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json();
}
 
// Usage
const courses = await getCourses();
console.log('Available:', courses.map(c => c.name));
 
const results = await search('What causes glaucoma?', courses[0].id);
results.results.forEach(r => {
  console.log(`\n📖 ${r.title}`);
  console.log(`   ${r.content.slice(0, 200)}...`);
});

Flutter/Dart — Full Example:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
 
class TutorQClient {
  static const _baseUrl = 'https://api.tutorq.ai/api/v1/partner';
  final _headers = {
    'Content-Type': 'application/json',
    'x-api-key': dotenv.env['TUTORQ_API_KEY']!,
  };
 
  // Step 1: Get available courses
  Future<List<Map<String, dynamic>>> getCourses() async {
    final res = await http.get(
      Uri.parse('$_baseUrl/courses'),
      headers: _headers,
    );
    if (res.statusCode != 200) throw Exception('API error: ${res.statusCode}');
    final data = jsonDecode(res.body);
    return List<Map<String, dynamic>>.from(data['courses']);
  }
 
  // Step 2: Search a course
  Future<Map<String, dynamic>> search(String query, String courseId) async {
    final res = await http.post(
      Uri.parse('$_baseUrl/search'),
      headers: _headers,
      body: jsonEncode({
        'query': query,
        'course_id': courseId,
        'top_k': 5,
      }),
    );
    if (res.statusCode != 200) throw Exception('API error: ${res.statusCode}');
    return jsonDecode(res.body);
  }
}
 
// Usage in your widget
final client = TutorQClient();
final courses = await client.getCourses();
final results = await client.search('anatomy of retina', courses[0]['id']);

Step 4: Parse and Display Results

Each result contains text content, metadata, and optional image descriptions. Here's how to handle them.

Response Structure

{
  "success": true,
  "results": [
    {
      "lesson_title": "Glaucoma - Pathophysiology",
      "content": "Glaucoma is characterized by progressive optic nerve damage...",
      "summary": "Overview of glaucoma causes and mechanisms",
      "key_terms": ["intraocular pressure", "optic nerve", "trabecular meshwork"],
      "similarity": 0.8723,
      "lesson_type": "CONCEPT",
      "difficulty": "INTERMEDIATE",
      "material_id": "mat_1773962909486_dkt7e7",
      "chunk_index": 42,
      "image_descriptions": [
        "Cross-section diagram of the eye showing aqueous humor drainage pathways",
        "Comparison of normal vs glaucomatous optic disc"
      ]
    }
  ],
  "total_results": 3,
  "query": "What causes glaucoma?",
  "course_id": "068038d2-...",
  "duration_ms": 892.3
}

Python — Parse and Display

def display_results(response: dict):
    if not response.get('success') or not response.get('results'):
        print("No results found.")
        return
 
    for i, result in enumerate(response['results'], 1):
        # Title and confidence
        similarity_pct = round(result['similarity'] * 100)
        print(f"\n{'='*60}")
        print(f"Result {i}: {result['lesson_title']} ({similarity_pct}% match)")
        print(f"Type: {result.get('lesson_type', 'N/A')} | Difficulty: {result.get('difficulty', 'N/A')}")
 
        # Key terms
        if result.get('key_terms'):
            print(f"Key terms: {', '.join(result['key_terms'])}")
 
        # Content (main passage)
        print(f"\n{result['content'][:500]}...")
 
        # Image descriptions (if the passage references diagrams)
        if result.get('image_descriptions'):
            print(f"\n📷 Diagrams in this section:")
            for desc in result['image_descriptions']:
                print(f"  - {desc}")
 
# Usage
response = search_course_content('What causes glaucoma?', 'your-course-id')
display_results(response)

Node.js — Parse for Frontend

function formatResults(response) {
  if (!response.success || !response.results.length) {
    return { empty: true, message: 'No relevant content found.' };
  }
 
  return response.results.map((result) => ({
    // Display fields
    title: result.lesson_title,
    content: result.content,
    confidence: Math.round(result.similarity * 100),
    type: result.lesson_type,
    difficulty: result.difficulty,
 
    // Highlight these terms in the content
    keyTerms: result.key_terms || [],
 
    // Show image descriptions if the section has diagrams
    hasImages: Boolean(result.image_descriptions?.length),
    imageDescriptions: result.image_descriptions || [],
 
    // For linking back to source material
    materialId: result.material_id,
    chunkIndex: result.chunk_index,
  }));
}
 
// Usage in your API handler
app.post('/api/ask', async (req, res) => {
  const { question, courseId } = req.body;
 
  const tutorqResponse = await searchCourseContent(question, courseId);
  const formatted = formatResults(tutorqResponse);
 
  res.json({
    question,
    answers: formatted,
    searchTime: tutorqResponse.duration_ms,
  });
});

Flutter/Dart — Parse for Mobile UI

class SearchResult {
  final String title;
  final String content;
  final int confidence;
  final List<String> keyTerms;
  final List<String> imageDescriptions;
  final String? difficulty;
 
  SearchResult.fromJson(Map<String, dynamic> json)
      : title = json['lesson_title'] ?? 'Untitled',
        content = json['content'] ?? '',
        confidence = ((json['similarity'] ?? 0) * 100).round(),
        keyTerms = List<String>.from(json['key_terms'] ?? []),
        imageDescriptions = List<String>.from(json['image_descriptions'] ?? []),
        difficulty = json['difficulty'];
}
 
List<SearchResult> parseResults(Map<String, dynamic> response) {
  if (response['success'] != true) return [];
 
  return (response['results'] as List)
      .map((r) => SearchResult.fromJson(r))
      .toList();
}
 
// Usage in your widget
final results = parseResults(await searchCourseContent(query, courseId));
 
// Display in a ListView
ListView.builder(
  itemCount: results.length,
  itemBuilder: (context, index) {
    final result = results[index];
    return Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Title with confidence badge
          Row(children: [
            Text(result.title, style: TextStyle(fontWeight: FontWeight.bold)),
            Chip(label: Text('${result.confidence}%')),
          ]),
 
          // Content
          Text(result.content, maxLines: 5, overflow: TextOverflow.ellipsis),
 
          // Key terms
          if (result.keyTerms.isNotEmpty)
            Wrap(
              children: result.keyTerms
                  .map((t) => Chip(label: Text(t, style: TextStyle(fontSize: 12))))
                  .toList(),
            ),
 
          // Image descriptions
          if (result.imageDescriptions.isNotEmpty)
            ...result.imageDescriptions.map(
              (desc) => Row(children: [
                Icon(Icons.image, size: 16),
                SizedBox(width: 4),
                Expanded(child: Text(desc, style: TextStyle(fontSize: 12, color: Colors.grey))),
              ]),
            ),
        ],
      ),
    );
  },
);

Handling Edge Cases

# No results
if response['total_results'] == 0:
    # Suggest lowering threshold or rephrasing
    retry = search_course_content(query, course_id, similarity_threshold=0.4)
 
# Low confidence results (< 50%)
low_confidence = [r for r in response['results'] if r['similarity'] < 0.5]
if low_confidence:
    # Show with a disclaimer: "These results may not be directly relevant"
    pass
 
# Results with images
has_diagrams = [r for r in response['results'] if r.get('image_descriptions')]
# Show image descriptions inline — the actual images are in the course material

Step 5: Monitor Usage

Check your API usage regularly to:

  • Track student engagement (which topics are searched most)
  • Monitor costs (requests per day/month)
  • Identify content gaps (queries with 0 results)

See Usage & Billing for details.

Architecture Diagram

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  Your Mobile │     │  Your Backend│     │   TutorQ API │
│     App      │────▶│    Server    │────▶│              │
│              │     │              │     │  /partner/   │
│  Student UI  │◀────│  API Key     │◀────│  search      │
└──────────────┘     └──────────────┘     └──────┬───────┘

                                          ┌───────▼───────┐
                                          │  PostgreSQL    │
                                          │  + pgvector    │
                                          │  (your tenant) │
                                          └───────────────┘

FAQ

Can I call the API directly from my mobile app? No. Always call from your backend server. Exposing the API key in client code is a security risk.

What if my query returns 0 results? Lower the similarity_threshold (e.g., 0.4) or rephrase the query. If consistently 0 results, the topic may not be covered in your uploaded materials.

Can I upload more materials later? Yes. Upload new materials via the professor dashboard. They'll be processed and become searchable automatically.

What languages are supported? The search works with any language in the uploaded materials. Voice tutoring supports English and Hindi currently.

Support