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:
- A university tenant for your organization
- A professor account for uploading materials
- 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 PENDING → PROCESSING → COMPLETED.
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 materialStep 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
- Email: venkat@q3learners.com
- Response time: Typically within 24 hours