The Likeness Search system exposes six API endpoints: one public search endpoint, two user-facing upload endpoints, and three internal endpoints for the indexing pipeline.
All endpoints are under /api/indexing/:
https://app.zooly.ai/api/indexing/http://localhost:3004/api/indexing/The following endpoints require CRON_SECRET bearer token authentication:
GET /api/indexing/process-queuePOST /api/indexing/generate-tagsPOST /api/indexing/scrape-socialHeader: Authorization: Bearer <CRON_SECRET>
The search endpoint requires no authentication:
GET /api/indexing/searchPOST /api/indexing/searchThe asset upload endpoints require user authentication (via cookie):
POST /api/ui-api/user-image-assets/addPOST /api/ui-api/user-voice-assets/addFor detailed documentation on both upload endpoints, see Asset Uploads.
Endpoint: GET /api/indexing/process-queue
Location: apps/zooly-app/app/api/indexing/process-queue/route.ts
Authentication: Required (CRON_SECRET bearer token)
Description: Cron endpoint that processes the indexing queue. Called by Vercel cron job every 2 minutes.
Request:
GET /api/indexing/process-queue
Authorization: Bearer <CRON_SECRET>
Response (Success):
{
"success": true,
"message": "Events processed",
"timedOutCount": 2,
"retriedCount": 5,
"processedCount": 15
}
Response (Error):
{
"success": false,
"error": "Unauthorized"
}
Status Codes:
200 - Success401 - Unauthorized (invalid or missing CRON_SECRET)500 - Internal server errorMax Duration: 300 seconds (5 minutes)
Endpoint: POST /api/indexing/generate-tags
Location: apps/zooly-app/app/api/indexing/generate-tags/route.ts
Authentication: Required (CRON_SECRET bearer token)
Description: Processes unprocessed likeness assets for an account and generates tags using AI. Called by the indexing daemon when data sufficiency check indicates unprocessed assets.
Request:
{
"eventId": "abc123..."
}
Response (Success):
{
"success": true,
"message": "Processed 5 assets, 0 failed",
"successCount": 5,
"failureCount": 0
}
Response (Partial Success):
{
"success": false,
"message": "Processed 3 assets, 2 failed",
"successCount": 3,
"failureCount": 2,
"errors": [
"Asset xyz: Rate limited (will retry)",
"Asset abc: Invalid audio format"
]
}
Status Codes:
200 - Success (all or partial)400 - Bad request (missing or invalid eventId)401 - Unauthorized404 - Event or account not found500 - Internal server errorMax Duration: 300 seconds (5 minutes)
Process:
IN_PROGRESSsearchTags IS NULL, tagAttemptCount < 5)addVoice)voiceId to eleven_labs tablelikenessAssets.voiceSampleUrllikenessAssets.searchTagsCOMPLETED and creates new queue event for re-indexingEndpoint: POST /api/indexing/scrape-social
Location: apps/zooly-app/app/api/indexing/scrape-social/route.ts
Authentication: Required (CRON_SECRET bearer token)
Description: Scrapes social media links for an account to get follower counts and profile images. Called by the indexing daemon when data sufficiency check indicates social scraping is needed.
Request:
{
"eventId": "abc123..."
}
Response (Success):
{
"success": true,
"message": "Processed 3 links, 0 failed",
"successCount": 3,
"failureCount": 0,
"note": "Social scraping logic not yet implemented - this is a placeholder"
}
Status Codes:
200 - Success400 - Bad request (missing or invalid eventId)401 - Unauthorized404 - Event or account not found500 - Internal server errorMax Duration: 300 seconds (5 minutes)
Process:
IN_PROGRESSaccount_social_links tableaccount_social_linksaccount.imageUrllikenessAssets entryCOMPLETED and creates new queue event for re-indexingNote: Actual scraping logic is a placeholder. Platform-specific scrapers need to be implemented.
Endpoint: POST /api/ui-api/user-image-assets/add
Location: apps/zooly-app/app/api/ui-api/user-image-assets/add/route.ts
Authentication: Required (user cookie authentication)
Description: Upload image assets for likeness search indexing. Supports both file uploads and URL-based uploads. See Asset Uploads for full details.
Request:
POST /api/ui-api/user-image-assets/add
Content-Type: multipart/form-data
FormData:
files: File[] (one or more image files)
Accepted MIME types: image/jpeg, image/png, image/webp
Request:
{
"imageUrls": [
"https://example.com/photo1.jpg",
"https://example.com/photo2.png"
]
}
Response (Success):
{
"success": true,
"message": "Processed 2 URLs",
"created": 2,
"duplicates": 0,
"assetIds": ["abc123...", "def456..."]
}
Status Codes:
200 - Success (all or partial)400 - Bad request (no files/URLs provided, all invalid)401 - Unauthorized (not authenticated)500 - Internal server errorProcess:
likeness-image-assets/)contentUrl per accountlikenessAssets records with type: "IMAGE"IMAGE_ASSET)Endpoint: POST /api/ui-api/user-voice-assets/add
Location: apps/zooly-app/app/api/ui-api/user-voice-assets/add/route.ts
Authentication: Required (user cookie authentication)
Description: Upload voice assets for likeness search indexing. Supports both file uploads and URL-based uploads.
Request:
POST /api/ui-api/user-voice-assets/add
Content-Type: multipart/form-data
FormData:
files: File[] (one or more audio files)
Example:
const formData = new FormData();
formData.append('files', audioFile1);
formData.append('files', audioFile2);
const response = await fetch('/api/ui-api/user-voice-assets/add', {
method: 'POST',
body: formData,
});
Request:
{
"voiceUrls": [
"https://example.com/audio1.mp3",
"https://example.com/audio2.wav"
]
}
Response (Success):
{
"success": true,
"message": "Processed 2 URLs",
"created": 2,
"duplicates": 0,
"validationErrors": 0,
"assetIds": ["abc123...", "def456..."]
}
Response (Partial Success):
{
"success": true,
"message": "Processed 3 URLs",
"created": 2,
"duplicates": 1,
"validationErrors": 0,
"assetIds": ["abc123...", "def456..."]
}
Response (Validation Errors):
{
"success": false,
"error": "All URLs are invalid",
"validationErrors": [
"https://youtube.com/watch?v=...: Invalid audio URL: Video platform URLs are not supported",
"invalid-url: Invalid URL format"
]
}
Status Codes:
200 - Success (all or partial)400 - Bad request (no files/URLs provided, all invalid)401 - Unauthorized (not authenticated)500 - Internal server errorProcess:
likeness-voice-assets/)likenessAssets records with type: "VOICE"VOICE_ASSET)Validation:
contentUrl per accountNote: After upload, assets are automatically queued for AI tag generation and voice sample creation.
Endpoint: GET /api/indexing/search or POST /api/indexing/search
Location: apps/zooly-app/app/api/indexing/search/route.ts
Authentication: None (public endpoint)
Description: Search for likeness accounts using filters. Supports both GET (query parameters) and POST (JSON body) methods.
Query Parameters:
q - Text query (optional, currently searches by name)limit - Number of results (default: 20)offset - Pagination offset (default: 0)orderBy - Sort field: numberOfFollowers, birthYear, engagementRateorderDirection - Sort direction: asc, descExample:
GET /api/indexing/search?q=blonde&limit=10&orderBy=numberOfFollowers&orderDirection=desc
Body:
{
"filters": {
"category": "MODELS",
"gender": "FEMALE",
"hairColor": "BLONDE",
"minFollowers": 100000,
"hasImageAsset": true
},
"limit": 20,
"offset": 0,
"orderBy": "numberOfFollowers",
"orderDirection": "desc"
}
Response (Success):
{
"success": true,
"results": [
{
"accountId": "abc123...",
"likenessSearch": {
"category": "MODELS",
"gender": "FEMALE",
"hairColor": "BLONDE",
"numberOfFollowers": 1500000,
...
},
"account": {
"displayName": "Jane Doe",
"imageUrl": "https://...",
"slug": "jane-doe"
},
"id": "abc123...",
"displayName": "Jane Doe",
"profileImage": "https://...",
"images": ["https://...", "https://..."],
"followersCount": "1.5M",
"slug": "jane-doe",
"score": 0.95
}
],
"count": 1
}
Response (Error):
{
"success": false,
"error": "Invalid filter value"
}
Status Codes:
200 - Success400 - Bad request (invalid filters)500 - Internal server errorMax Duration: 60 seconds (1 minute)
Search Process:
searchLikeness() service functionThe API routes use service functions directly from their source packages:
Location: packages/likeness-search/src/
Import from: @zooly/likeness-search
addImageAssets(cookieHeader, imageUrls?, files?) - Upload image assets (S3 + DB + queue)addVoiceAssets(cookieHeader, voiceUrls?, files?) - Upload voice assets (S3 + DB + queue)See Asset Uploads for detailed documentation.
Location: packages/likeness-search/src/
Import from: @zooly/likeness-search
indexingDaemon() - Main daemon loopprocessNextEvent() - Process single queue eventprocessIndexingEvent(event) - Process specific eventupsertToIndex(accountId) - Upsert account to indexesremoveFromIndex(accountId) - Remove account from indexesLocation: packages/likeness-search/src/
Import from: @zooly/likeness-search
searchLikeness(filters, options) - Main search functionvectorFallbackSearch(filters, options) - Vector similarity searchformatSearchResults(results) - Format and enrich resultsfiltersToDescription(filters) - Convert filters to textLocation: packages/likeness-search/src/
Import from: @zooly/likeness-search
generateTagsFromImage(imageUrl) - Extract tags from imagegenerateTagsFromVoice(audioUrl) - Extract tags from voiceNote: generateEmbedding has been moved to @zooly/util-srv. Import it from there.
Location: packages/util-elevenlabs/src/
Import from: @zooly/util-elevenlabs
createVoiceSample(options) - Create voice sampleNote: generateVoiceSampleText is an internal function used by createVoiceSample.
Location: packages/likeness-search/src/
Import from: @zooly/likeness-search
aggregateAccountTags(accountId) - Aggregate tags from sourcesnormalizeTagsForVector(tags) - Normalize tags for embeddingtriggerAITagGeneration(eventId) - Trigger AI generation APItriggerSocialScraping(accountId, eventId) - Trigger scraping APItriggerDataCollection(event, missingData) - Orchestrate triggershandleApiError(error, retryCount) - Error classification and retryclassifyError(error) - Classify error typecalculateBackoffDelay(retryCount, baseDelay) - Calculate retry delayAll endpoints return consistent error responses:
{
"success": false,
"error": "Error message"
}
For endpoints that process multiple items (generate-tags, scrape-social), errors array may be included:
{
"success": false,
"message": "Processed 3 items, 2 failed",
"successCount": 3,
"failureCount": 2,
"errors": [
"Item 1: Error message",
"Item 2: Error message"
]
}
Currently, no rate limiting is implemented. Consider adding rate limiting for:
The search endpoint should allow CORS for frontend access. Internal endpoints should not allow CORS (only called by cron or internal services).
Monitor these endpoints for:
curl -X GET https://app.zooly.ai/api/indexing/process-queue \
-H "Authorization: Bearer $CRON_SECRET"
curl -X POST https://app.zooly.ai/api/indexing/search \
-H "Content-Type: application/json" \
-d '{
"filters": {
"category": "MODELS",
"gender": "FEMALE",
"minFollowers": 100000
},
"limit": 20
}'
curl "https://app.zooly.ai/api/indexing/search?q=john&limit=10"
curl -X POST https://app.zooly.ai/api/indexing/search \
-H "Content-Type: application/json" \
-d '{
"filters": {
"hasVoiceSample": true,
"accent": "BRITISH_RP",
"voiceTone": "WARM"
}
}'On This Page
OverviewBase URLAuthenticationInternal EndpointsPublic EndpointUser-Facing EndpointsEndpoints1. Process Queue (Cron)2. Generate Tags3. Scrape Social4. Add Image AssetsFile Upload (Multipart Form Data)URL-Based Upload (JSON)5. Add Voice AssetsFile Upload (Multipart Form Data)URL-Based Upload (JSON)6. SearchGET RequestPOST RequestService FunctionsAsset Upload FunctionsIndexing FunctionsSearch FunctionsAI FunctionsUtility FunctionsError ResponsesRate LimitingCORSMonitoringUsage ExamplesExample 1: Trigger IndexingExample 2: Search for ModelsExample 3: Search by NameExample 4: Voice Search