Request Management API
This API is NOT part of Epic 1 - TTS Submission and Get Link.
Epic 1 uses the simplified TTS Submission API (/api/v1/text-to-speech) for direct audio generation.
This Request Management API describes a manual admin-driven workflow that has been deferred to future releases.
Epic 1 Flow: See Text-to-Speech Submission API instead.
Overview
The Request Management API handles all operations for text-to-audio requests including:
- User request submission
- Admin request viewing
- Admin status updates
- Admin audio upload
- QR code generation
Base URL: /api/v1
Authentication:
- User endpoints: JWT Bearer token (user role)
- Admin endpoints: JWT Bearer token (admin role)
User Endpoints
Submit Request
Endpoint: POST /requests
Auth: Required (User)
Description: User submits text for audio generation
Request Body:
{
"text": "Welcome to MicDots! Scan the QR code...",
"voiceId": "voice-001"
}
Response (201 Created):
{
"success": true,
"data": {
"requestId": "REQ-123456789",
"status": "pending",
"text": "Welcome to MicDots! Scan the QR code...",
"voiceId": "voice-001",
"createdAt": "2024-01-22T14:30:00Z"
},
"message": "Request submitted successfully. You will receive a confirmation email shortly."
}
Side Effects:
- Confirmation email sent to user
- Request stored in database with status "pending"
Validation:
text: Required, 1-1000 charactersvoiceId: Required, must be valid voice ID
Admin Endpoints
List All Requests
Endpoint: GET /admin/requests
Auth: Required (Admin)
Description: View all requests with filtering and pagination
Query Parameters:
{
status?: 'all' | 'pending' | 'processing' | 'done';
search?: string; // Search by request ID, user email, or text
page?: number; // Default: 1
limit?: number; // Default: 10, Max: 100
}
Response (200 OK):
{
"success": true,
"data": {
"requests": [
{
"id": "REQ-123456789",
"userId": "user-abc123",
"userEmail": "john@example.com",
"text": "Welcome to MicDots! Scan the QR code...",
"voiceId": "voice-001",
"voiceName": "Rachel",
"status": "pending",
"audioUrl": null,
"qrCodeUrl": null,
"playbackUrl": null,
"slug": null,
"createdAt": "2024-01-22T14:30:00Z",
"updatedAt": "2024-01-22T14:30:00Z"
}
],
"pagination": {
"currentPage": 1,
"totalPages": 3,
"totalRequests": 17,
"limit": 10
},
"statistics": {
"pending": 12,
"processing": 5,
"done": 143,
"total": 160
}
}
}
Get Request Details
Endpoint: GET /admin/requests/:id
Auth: Required (Admin)
Description: View detailed information for a single request
Response (200 OK):
{
"success": true,
"data": {
"id": "REQ-123456789",
"userId": "user-abc123",
"user": {
"email": "john@example.com",
"name": "John Doe"
},
"text": "Welcome to MicDots! Scan the QR code to hear your personalized message.",
"voiceId": "voice-001",
"voice": {
"name": "Rachel",
"gender": "female",
"accent": "american"
},
"status": "pending",
"audioUrl": null,
"qrCodeUrl": null,
"playbackUrl": null,
"slug": null,
"createdAt": "2024-01-22T14:30:00Z",
"updatedAt": "2024-01-22T14:30:00Z",
"statusHistory": [
{
"status": "pending",
"timestamp": "2024-01-22T14:30:00Z",
"changedBy": null
}
]
}
}
Error (404 Not Found):
{
"success": false,
"error": {
"code": "REQUEST_NOT_FOUND",
"message": "Request with ID 'REQ-123456789' not found."
}
}
Update Request Status
Endpoint: PATCH /admin/requests/:id/status
Auth: Required (Admin)
Description: Update request status (pending → processing)
Request Body:
{
"status": "processing"
}
Response (200 OK):
{
"success": true,
"data": {
"id": "REQ-123456789",
"status": "processing",
"updatedAt": "2024-01-22T15:00:00Z"
},
"message": "Status updated to 'processing'. Processing email sent to user."
}
Side Effects:
- Status updated in database
- Processing email sent to user
- Status history logged
Validation:
- Status transition must be valid (pending → processing, not done → pending)
File Upload Strategy: S3 Pre-Signed URLs
For audio file uploads, MicDots uses S3 pre-signed URLs to optimize performance and scalability:
Benefits:
- Direct Upload: Frontend uploads directly to S3, bypassing backend
- Faster: No backend bottleneck for large audio files
- Scalable: Backend only handles metadata, not file transfers
- Secure: Pre-signed URLs expire after 15 minutes
- Progress Tracking: Frontend can track upload progress
Upload Flow:
- Admin requests pre-signed URL from backend
- Backend generates S3 pre-signed URL (15-minute expiration)
- Admin uploads audio file directly to S3 using pre-signed URL
- Admin submits S3 URL to backend for processing
- Backend generates QR code, updates status to "done", sends email
Get Audio Upload URL
Endpoint: POST /admin/requests/:id/upload-url
Auth: Required (Admin)
Description: Get pre-signed S3 URL for audio upload
Request Body:
{
"fileName": "request-audio.mp3",
"fileType": "audio/mpeg",
"fileSize": 5242880
}
Request Body Fields:
fileName(string, required) - Original file namefileType(string, required) - MIME type (must be "audio/mpeg")fileSize(number, required) - File size in bytes (max 10MB = 10485760 bytes)
Response (200 OK):
{
"success": true,
"data": {
"uploadUrl": "https://micdots-audio.s3.amazonaws.com/requests/REQ-123456789-1706025600.mp3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...",
"audioUrl": "https://micdots-audio.s3.amazonaws.com/requests/REQ-123456789-1706025600.mp3",
"fileKey": "requests/REQ-123456789-1706025600.mp3",
"expiresIn": 900
},
"message": "Pre-signed upload URL generated. Valid for 15 minutes."
}
Response Fields:
uploadUrl(string) - Pre-signed S3 URL for uploading (valid 15 minutes)audioUrl(string) - Final S3 URL for the uploaded filefileKey(string) - S3 object keyexpiresIn(number) - Expiration time in seconds (900 = 15 minutes)
Validation:
fileTypemust beaudio/mpeg(MP3 only)fileSizemust be ≤ 10MB (10485760 bytes)- Request must exist and be in "pending" or "processing" status
Error (400 Bad Request):
{
"success": false,
"error": {
"code": "INVALID_FILE_TYPE",
"message": "Only MP3 files are supported. File type must be 'audio/mpeg'."
}
}
Error (413 Payload Too Large):
{
"success": false,
"error": {
"code": "FILE_TOO_LARGE",
"message": "Audio file must be less than 10MB (10485760 bytes)."
}
}
Upload Audio to S3 (Client-Side)
After getting the pre-signed URL, upload the file directly to S3 from the frontend:
Basic Upload (JavaScript/TypeScript):
async function uploadAudioToS3(uploadUrl: string, file: File): Promise<void> {
const response = await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type
},
body: file
});
if (!response.ok) {
throw new Error(`S3 upload failed: ${response.status}`);
}
}
Upload with Progress Tracking:
function uploadAudioWithProgress(
uploadUrl: string,
file: File,
onProgress: (percent: number) => void
): Promise<void> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// Track upload progress
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete);
}
});
// Handle completion
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve();
} else {
reject(new Error(`S3 upload failed: ${xhr.status}`));
}
});
// Handle errors
xhr.addEventListener('error', () => {
reject(new Error('S3 upload failed'));
});
// Upload file
xhr.open('PUT', uploadUrl);
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
});
}
// Usage example
const file = document.querySelector('input[type="file"]').files[0];
const requestId = 'REQ-123456789';
// Step 1: Get pre-signed URL
const urlResponse = await fetch(`/api/v1/admin/requests/${requestId}/upload-url`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${adminToken}`
},
body: JSON.stringify({
fileName: file.name,
fileType: file.type,
fileSize: file.size
})
});
const { data } = await urlResponse.json();
// Step 2: Upload to S3 with progress tracking
await uploadAudioWithProgress(data.uploadUrl, file, (percent) => {
console.log(`Upload progress: ${percent.toFixed(1)}%`);
// Update progress bar in UI
});
// Step 3: Complete the upload by submitting the audio URL
await fetch(`/api/v1/admin/requests/${requestId}/upload`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${adminToken}`
},
body: JSON.stringify({
audioUrl: data.audioUrl
})
});
Complete Audio Upload
Endpoint: POST /admin/requests/:id/upload
Auth: Required (Admin)
Description: Complete audio upload with S3 URL, auto-generate QR code, mark as done
Request Body:
{
"audioUrl": "https://micdots-audio.s3.amazonaws.com/requests/REQ-123456789-1706025600.mp3"
}
Request Body Fields:
audioUrl(string, required) - S3 URL of the uploaded audio file
Response (200 OK):
{
"success": true,
"data": {
"id": "REQ-123456789",
"status": "done",
"audioUrl": "https://micdots-audio.s3.amazonaws.com/requests/REQ-123456789-1706025600.mp3",
"qrCodeUrl": "https://micdots-qr.s3.amazonaws.com/REQ-123456789-1706025600.png",
"playbackUrl": "https://micdots.com/play/welcome-message-1706025600",
"slug": "welcome-message-1706025600",
"updatedAt": "2024-01-22T16:00:00Z"
},
"message": "Audio uploaded successfully. QR code generated and done email sent to user."
}
Processing Steps (Automatic):
- Verify audio file exists at provided S3 URL
- Generate unique slug
- Generate QR code (PNG, 500x500px) pointing to playback URL
- Upload QR code to S3
- Update request status to "done"
- Send done email with audio link + QR code
Validation:
audioUrlmust be valid S3 URL- Audio file must exist at the provided URL
- Request must be in "pending" or "processing" status
Error (400 Bad Request):
{
"success": false,
"error": {
"code": "INVALID_AUDIO_URL",
"message": "The provided audio URL is invalid or the file does not exist."
}
}
Error (409 Conflict):
{
"success": false,
"error": {
"code": "INVALID_STATUS",
"message": "Request is already marked as done."
}
}
Public Endpoints
Get Playback Data
Endpoint: GET /play/:slug
Auth: Not required (Public)
Description: Get audio data for public playback page
Response (200 OK):
{
"success": true,
"data": {
"slug": "welcome-message-1234567890",
"audioUrl": "https://s3.amazonaws.com/micdots-audio/REQ-123456789-1234567890.mp3",
"text": "Welcome to MicDots! Scan the QR code...",
"voiceName": "Rachel",
"createdAt": "2024-01-22T14:30:00Z"
}
}
Error (404 Not Found):
{
"success": false,
"error": {
"code": "PLAYBACK_NOT_FOUND",
"message": "Audio with slug 'welcome-message-1234567890' not found."
}
}
Implementation Example
Express.js Server
import express from 'express';
import { authenticate, requireAdmin } from './middleware/auth';
import { RequestController } from './controllers/RequestController';
const app = express();
const requestController = new RequestController();
// User endpoints
app.post('/api/v1/requests',
authenticate,
requestController.submitRequest
);
// Admin endpoints
app.get('/api/v1/admin/requests',
authenticate,
requireAdmin,
requestController.listRequests
);
app.get('/api/v1/admin/requests/:id',
authenticate,
requireAdmin,
requestController.getRequestDetails
);
app.patch('/api/v1/admin/requests/:id/status',
authenticate,
requireAdmin,
requestController.updateStatus
);
app.post('/api/v1/admin/requests/:id/upload-url',
authenticate,
requireAdmin,
requestController.getUploadUrl
);
app.post('/api/v1/admin/requests/:id/upload',
authenticate,
requireAdmin,
requestController.completeAudioUpload
);
// Public endpoints
app.get('/api/v1/play/:slug',
requestController.getPlaybackData
);
Database Schema
interface Request {
id: string; // REQ-{timestamp}-{random}
userId: string;
text: string; // 1-1000 chars
voiceId: string;
status: 'pending' | 'processing' | 'done';
audioUrl: string | null; // S3 URL
qrCodeUrl: string | null; // S3 URL
playbackUrl: string | null; // micdots.com/play/{slug}
slug: string | null; // URL-friendly identifier
createdAt: Date;
updatedAt: Date;
}
interface StatusHistory {
id: string;
requestId: string;
status: string;
changedBy: string | null; // Admin ID or null for system
timestamp: Date;
}
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid authentication token |
FORBIDDEN | 403 | User does not have required permissions |
REQUEST_NOT_FOUND | 404 | Request ID does not exist |
PLAYBACK_NOT_FOUND | 404 | Slug does not exist |
INVALID_STATUS_TRANSITION | 400 | Cannot change from current status to requested status |
FILE_TOO_LARGE | 413 | Audio file exceeds 10MB limit |
INVALID_FILE_FORMAT | 400 | File is not MP3 format |
TEXT_TOO_LONG | 400 | Text exceeds 1000 characters |
INVALID_VOICE_ID | 400 | Voice ID does not exist |
EMAIL_SEND_FAILED | 500 | Email service unavailable (logged, doesn't block request) |
Related Documentation
- Email Notifications - Email triggers
- Admin Backoffice - Admin UI
- Admin Request Details - Request processing UI
- User Flow - Complete workflow