The TiQer Async Response API provides ultra-fast response submission (target <15ms) by returning immediate acknowledgment and processing responses in the background. This guide provides step-by-step integration instructions for the Frontend team.
POST /api/v2/tiqer/response-async/
- 95%+ performance improvement over synchronous processing
- Immediate acknowledgment (~15ms response time)
- Real-time notifications via Firebase RTDB when processing completes
- Comprehensive error handling with detailed error codes
- Automatic cleanup to prevent Firebase data accumulation
const submitResponse = async (responseData) => {
const response = await fetch('/api/v2/tiqer/response-async/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${userToken}`
},
body: JSON.stringify({
option_id: 24072,
card_number: 5,
question_id: 40795,
session_token: "677145c7-b80a-4ba8-b75b-24555300c07e",
response_time_seconds: 10, // Optional
confidence_score: 1.0 // Optional
})
});
return await response.json();
};{
"status": "SUCCESS",
"message": "SUBMITTED",
"processing_mode": "async_background",
"task_id": "12dd6374-e260-4a81-a2a4-93702ce00ddd",
"response_data": {
"session_token": "677145c7-b80a-4ba8-b75b-24555300c07e",
"question_id": 40795,
"card_number": 5,
"option_id": 24072
},
"performance_metrics": {
"response_time_ms": 12,
"target_threshold_ms": 15,
"performance_achieved": true
},
"async_processing": {
"task_scheduled": true,
"background_processing": true,
"estimated_completion_seconds": 2,
"firebase_notification_enabled": true
},
"firebase_subscription": {
"path": "tiqer_sessions/677145c7-b80a-4ba8-b75b-24555300c07e/async_responses",
"listen_for": "response_processed",
"notification_method": "Firebase RTDB"
},
"timestamp": "2025-09-22T02:57:32.438869+00:00"
}{
"status": "FAILURE",
"error_code": "VALIDATION_FAILED",
"error_message": "Missing required fields: question_id, session_token",
"missing_fields": ["question_id", "session_token"]
}{
"status": "FAILURE",
"error_code": "SESSION_NOT_FOUND",
"error_message": "Session invalid-session-token not found or not owned by current teacher",
"session_token": "invalid-session-token",
"teacher_id": 501
}{
"status": "SUCCESS",
"message": "SUBMITTED (duplicate ignored)",
"duplicate_detected": true,
"processing_mode": "immediate_response",
"response_time_ms": 8
}// Initialize Firebase listener for async response notifications
const setupAsyncResponseListener = (sessionToken) => {
const asyncResponsesRef = firebase.database()
.ref(`tiqer_sessions/${sessionToken}/async_responses`);
// Listen for new async response notifications
asyncResponsesRef.on('child_added', (snapshot) => {
const notification = snapshot.val();
const notificationId = snapshot.key;
console.log('Async notification received:', notification);
// Handle based on notification type
if (notification.event === 'response_processed') {
handleSuccessfulResponse(notification, notificationId, snapshot.ref);
} else if (notification.event === 'response_error') {
handleErrorResponse(notification, notificationId, snapshot.ref);
}
});
// Handle listener errors
asyncResponsesRef.on('error', (error) => {
console.error('Firebase listener error:', error);
// Implement fallback polling or retry logic
});
return asyncResponsesRef;
};{
"event": "response_processed",
"processing_id": "async_resp_1758510124",
"status": "completed",
"student_info": {
"student_id": 123,
"student_name": "John Doe",
"card_number": 5,
"roll_number": "001"
},
"response_info": {
"question_id": 40795,
"selected_option": 2,
"is_correct": true,
"obtained_score": 1.0,
"response_time_seconds": 10
},
"action": "created", // or "updated"
"timestamp": "2025-09-22T02:57:35.123456+00:00",
"task_id": "12dd6374-e260-4a81-a2a4-93702ce00ddd"
}{
"event": "response_error",
"processing_id": "async_resp_1758509892",
"status": "failed",
"error_info": {
"error_type": "ValueError",
"error_message": "Option 99999 does not exist for question 40795. Valid options: [24072, 24074, 24075, 24073]",
"error_code": "INVALID_OPTION",
"retries_attempted": 3,
"full_error": "INVALID_OPTION: Option 99999 does not exist for question 40795. Valid options: [24072, 24074, 24075, 24073]"
},
"request_info": {
"option_id": 99999,
"card_number": 2,
"question_id": 40795,
"session_token": "677145c7-b80a-4ba8-b75b-24555300c07e"
},
"task_id": "119fb3e1-534e-4ca1-9474-fc4032a36712",
"timestamp": "2025-09-22T02:58:13.089306+00:00",
"execution_time_seconds": 0.100886
}const handleSuccessfulResponse = (notification, notificationId, firebaseRef) => {
const { student_info, response_info, action, task_id } = notification;
// Update UI with student response
updateStudentResponseInUI({
studentId: student_info.student_id,
studentName: student_info.student_name,
cardNumber: student_info.card_number,
rollNumber: student_info.roll_number,
isCorrect: response_info.is_correct,
obtainedScore: response_info.obtained_score,
responseTime: response_info.response_time_seconds,
action: action // 'created' or 'updated'
});
// Update live analytics/progress
updateLiveAnalytics();
// Clean up Firebase notification (recommended)
cleanupFirebaseNotification(firebaseRef, task_id);
// Show success feedback (optional)
showSuccessToast(`Response recorded for ${student_info.student_name}`);
};
const updateStudentResponseInUI = (responseData) => {
// Example UI update logic
const studentElement = document.querySelector(`[data-student-id="${responseData.studentId}"]`);
if (studentElement) {
// Update visual indicators
studentElement.classList.add(responseData.isCorrect ? 'correct' : 'incorrect');
studentElement.querySelector('.score').textContent = responseData.obtainedScore;
studentElement.querySelector('.response-time').textContent = `${responseData.responseTime}s`;
// Update response count
updateResponseCount();
// Update progress bars
updateProgressBars();
}
};const handleErrorResponse = (notification, notificationId, firebaseRef) => {
const { error_info, request_info, task_id } = notification;
console.error('Async response processing failed:', error_info);
// Handle specific error types
switch (error_info.error_code) {
case 'INVALID_OPTION':
showErrorToast(`Invalid option selected. ${error_info.error_message}`);
// Optionally highlight the problematic option
highlightInvalidOption(request_info.option_id);
break;
case 'INVALID_CARD':
showErrorToast(`Invalid card ${request_info.card_number}. ${error_info.error_message}`);
// Show available cards to user
showAvailableCards();
break;
case 'SESSION_NOT_ACTIVE':
showErrorToast('Session is no longer active. Please refresh the page.');
// Optionally redirect or refresh
break;
case 'QUESTION_MISMATCH':
showErrorToast('Question synchronization error. Please refresh the page.');
// Force refresh current question
refreshCurrentQuestion();
break;
default:
showErrorToast(`Response processing failed: ${error_info.error_message}`);
}
// Log error for debugging
logErrorToConsole(notification);
// Clean up Firebase notification
cleanupFirebaseNotification(firebaseRef, task_id);
// Optionally retry submission with corrected data
if (shouldRetrySubmission(error_info.error_code)) {
promptForRetry(request_info);
}
};
const showErrorToast = (message) => {
// Your toast/notification implementation
console.error(message);
// Example: toast.error(message);
};// Clean up Firebase notification after processing
const cleanupFirebaseNotification = (firebaseRef, taskId) => {
try {
// Remove the specific notification
firebaseRef.remove().then(() => {
console.log(`Cleaned up Firebase notification for task ${taskId}`);
}).catch((error) => {
console.warn('Failed to cleanup Firebase notification:', error);
});
} catch (error) {
console.warn('Firebase cleanup error:', error);
}
};
// Alternative: Cleanup via API (for bulk cleanup)
const cleanupViaAPI = async (sessionToken, mode = 'partial') => {
try {
const response = await fetch('/api/v2/tiqer/firebase-cleanup/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${userToken}`
},
body: JSON.stringify({
session_token: sessionToken,
cleanup_mode: mode, // 'partial' or 'complete'
immediate: true
})
});
const result = await response.json();
console.log('API cleanup result:', result);
return result.cleanup_success;
} catch (error) {
console.error('API cleanup failed:', error);
return false;
}
};// Clean up all Firebase data when session ends
const cleanupSessionEnd = async (sessionToken) => {
// Remove Firebase listener
if (asyncResponsesListener) {
asyncResponsesListener.off();
}
// Complete Firebase cleanup
await cleanupViaAPI(sessionToken, 'complete');
console.log('Session cleanup completed');
};import React, { useEffect, useState, useCallback } from 'react';
import { initializeApp } from 'firebase/app';
import { getDatabase, ref, onChildAdded, off } from 'firebase/database';
const TiQerAsyncQuiz = ({ sessionToken, userToken }) => {
const [responses, setResponses] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [firebaseListener, setFirebaseListener] = useState(null);
// Initialize Firebase listener
useEffect(() => {
const database = getDatabase();
const asyncResponsesRef = ref(database, `tiqer_sessions/${sessionToken}/async_responses`);
const unsubscribe = onChildAdded(asyncResponsesRef, (snapshot) => {
const notification = snapshot.val();
const notificationId = snapshot.key;
if (notification.event === 'response_processed') {
handleSuccessfulResponse(notification, snapshot.ref);
} else if (notification.event === 'response_error') {
handleErrorResponse(notification, snapshot.ref);
}
});
setFirebaseListener({ ref: asyncResponsesRef, unsubscribe });
return () => {
if (unsubscribe) unsubscribe();
};
}, [sessionToken]);
// Submit response with async processing
const submitResponse = useCallback(async (responseData) => {
setIsSubmitting(true);
try {
const response = await fetch('/api/v2/tiqer/response-async/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${userToken}`
},
body: JSON.stringify({
...responseData,
session_token: sessionToken
})
});
const result = await response.json();
if (result.status === 'SUCCESS') {
console.log('Response submitted successfully:', result.task_id);
// Show immediate feedback
showSubmissionFeedback(responseData.card_number);
} else {
handleSubmissionError(result);
}
} catch (error) {
console.error('Submission failed:', error);
showErrorToast('Network error. Please try again.');
} finally {
setIsSubmitting(false);
}
}, [sessionToken, userToken]);
const handleSuccessfulResponse = (notification, firebaseRef) => {
const { student_info, response_info } = notification;
// Update local state
setResponses(prev => ({
...prev,
[student_info.student_id]: {
studentName: student_info.student_name,
cardNumber: student_info.card_number,
isCorrect: response_info.is_correct,
score: response_info.obtained_score,
responseTime: response_info.response_time_seconds,
timestamp: notification.timestamp
}
}));
// Cleanup Firebase
firebaseRef.remove().catch(console.warn);
};
const handleErrorResponse = (notification, firebaseRef) => {
const { error_info } = notification;
console.error('Response processing failed:', error_info);
// Show user-friendly error
showErrorToast(error_info.error_message);
// Cleanup Firebase
firebaseRef.remove().catch(console.warn);
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (firebaseListener) {
firebaseListener.unsubscribe();
}
// Cleanup Firebase data when component unmounts
cleanupViaAPI(sessionToken, 'complete');
};
}, []);
return (
<div className="tiqer-async-quiz">
{/* Your quiz UI components */}
<button
onClick={() => submitResponse({ option_id: 123, card_number: 5, question_id: 456 })}
disabled={isSubmitting}
>
{isSubmitting ? 'Submitting...' : 'Submit Response'}
</button>
{/* Display responses */}
<div className="responses">
{Object.entries(responses).map(([studentId, response]) => (
<div key={studentId} className={`response ${response.isCorrect ? 'correct' : 'incorrect'}`}>
<span>{response.studentName}</span>
<span>Card: {response.cardNumber}</span>
<span>Score: {response.score}</span>
<span>Time: {response.responseTime}s</span>
</div>
))}
</div>
</div>
);
};const submitWithRetry = async (responseData, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await submitResponse(responseData);
return result;
} catch (error) {
if (attempt === maxRetries) {
throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
}
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
};const setupRobustFirebaseListener = (sessionToken) => {
const database = getDatabase();
const asyncResponsesRef = ref(database, `tiqer_sessions/${sessionToken}/async_responses`);
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const setupListener = () => {
const unsubscribe = onChildAdded(asyncResponsesRef,
(snapshot) => {
// Handle notification
reconnectAttempts = 0; // Reset on successful message
},
(error) => {
console.error('Firebase listener error:', error);
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
setTimeout(() => {
console.log(`Reconnecting Firebase listener (attempt ${reconnectAttempts})`);
setupListener();
}, reconnectAttempts * 2000);
} else {
console.error('Max reconnection attempts reached. Consider fallback polling.');
// Implement fallback polling mechanism
startFallbackPolling(sessionToken);
}
}
);
return unsubscribe;
};
return setupListener();
};const startFallbackPolling = (sessionToken) => {
console.warn('Starting fallback polling for async responses');
const pollInterval = setInterval(async () => {
try {
// Poll a custom endpoint that checks for completed async tasks
const response = await fetch(`/api/v2/tiqer/poll-async-status/?session_token=${sessionToken}`, {
headers: { 'Authorization': `Token ${userToken}` }
});
const data = await response.json();
if (data.completed_responses?.length > 0) {
data.completed_responses.forEach(handleCompletedResponse);
}
} catch (error) {
console.error('Polling error:', error);
}
}, 2000); // Poll every 2 seconds
// Store interval ID for cleanup
return pollInterval;
};const monitorAsyncPerformance = () => {
const performanceMetrics = {
responseTimes: [],
errorRates: { total: 0, errors: 0 },
firebaseLatency: []
};
const trackSubmission = async (responseData) => {
const startTime = performance.now();
try {
const result = await submitResponse(responseData);
const responseTime = performance.now() - startTime;
performanceMetrics.responseTimes.push(responseTime);
performanceMetrics.errorRates.total++;
if (result.status !== 'SUCCESS') {
performanceMetrics.errorRates.errors++;
}
// Log if response time exceeds target
if (responseTime > 50) { // 50ms threshold including network
console.warn(`Slow response: ${responseTime}ms`);
}
return result;
} catch (error) {
performanceMetrics.errorRates.total++;
performanceMetrics.errorRates.errors++;
throw error;
}
};
const getPerformanceReport = () => {
const avgResponseTime = performanceMetrics.responseTimes.reduce((a, b) => a + b, 0) /
performanceMetrics.responseTimes.length;
const errorRate = (performanceMetrics.errorRates.errors / performanceMetrics.errorRates.total) * 100;
return {
averageResponseTime: avgResponseTime,
errorRate: errorRate,
totalRequests: performanceMetrics.errorRates.total,
medianResponseTime: calculateMedian(performanceMetrics.responseTimes)
};
};
return { trackSubmission, getPerformanceReport };
};| Error Code | Description | Frontend Action |
|---|---|---|
VALIDATION_FAILED |
Missing required fields | Show field validation errors |
INVALID_DATA_TYPE |
Wrong data types | Validate input types |
SESSION_NOT_FOUND |
Invalid/expired session | Redirect to session list |
INVALID_OPTION |
Option doesn't exist for question | Highlight invalid option, show valid options |
INVALID_CARD |
Card not found in classroom | Show available cards list |
CARD_INVALID_STATUS |
Card not available for use | Show card status, suggest alternatives |
CARD_NO_STUDENT |
Card not assigned to student | Contact teacher for card assignment |
SESSION_NOT_ACTIVE |
Session paused/inactive | Show session status, wait for activation |
QUESTION_MISMATCH |
Question sync issue | Refresh current question |
INVALID_RESPONSE_TIME |
Response time out of bounds | Validate timing constraints |
PROCESSING_ERROR |
Generic processing failure | Show retry option, contact support |
- Firebase SDK integrated in frontend
- Authentication token management in place
- Error handling framework ready
- UI components for real-time updates prepared
- Async response API integration complete
- Firebase listener setup implemented
- Success response handler implemented
- Error response handler implemented
- Firebase cleanup mechanism implemented
- Performance monitoring added
- Test with valid responses (should complete in ~2-3 seconds)
- Test with invalid option_id (should receive error notification)
- Test with invalid card_number (should receive error notification)
- Test network disconnection scenarios
- Test Firebase connection failures
- Test cleanup functionality
- Verify no Firebase data accumulation
- Error logging implemented
- Performance metrics collection added
- Fallback mechanisms tested
- User feedback mechanisms in place
- Documentation for debugging available
// Add to your application initialization
window.TIQER_DEBUG = true;
const debugLog = (message, data) => {
if (window.TIQER_DEBUG) {
console.log(`[TiQer Debug] ${message}`, data);
}
};// Add temporary Firebase data monitoring
const monitorFirebaseData = (sessionToken) => {
const database = getDatabase();
const sessionRef = ref(database, `tiqer_sessions/${sessionToken}`);
// Log all changes to session data
onValue(sessionRef, (snapshot) => {
console.log('Firebase session data:', snapshot.val());
});
};const debugApiResponse = (response, requestData) => {
console.group('TiQer API Debug');
console.log('Request:', requestData);
console.log('Response:', response);
console.log('Task ID:', response.task_id);
console.log('Performance:', response.performance_metrics);
console.groupEnd();
};For technical questions or issues with the async response integration:
- API Issues: Check server logs for task processing errors
- Firebase Issues: Verify Firebase configuration and connection
- Performance Issues: Monitor response times and implement fallbacks
- Integration Issues: Use debug logging and check network requests
Backend Team Contact: Available for debugging complex validation errors and performance optimization.
- v1.0 (Current): Initial async response implementation with comprehensive validation and Firebase cleanup
- Future: Planned enhancements include response batching and advanced analytics integration
This documentation is maintained by the Backend Team. Last updated: September 2025