API Reference

API endpoints and usage for Likeness Search

Overview

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.

Base URL

All endpoints are under /api/indexing/:

  • Production: https://app.zooly.ai/api/indexing/
  • Development: http://localhost:3004/api/indexing/

Authentication

Internal Endpoints

The following endpoints require CRON_SECRET bearer token authentication:

  • GET /api/indexing/process-queue
  • POST /api/indexing/generate-tags
  • POST /api/indexing/scrape-social

Header: Authorization: Bearer <CRON_SECRET>

Public Endpoint

The search endpoint requires no authentication:

  • GET /api/indexing/search
  • POST /api/indexing/search

User-Facing Endpoints

The asset upload endpoints require user authentication (via cookie):

  • POST /api/ui-api/user-image-assets/add
  • POST /api/ui-api/user-voice-assets/add

For detailed documentation on both upload endpoints, see Asset Uploads.

Endpoints

1. Process Queue (Cron)

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 - Success
  • 401 - Unauthorized (invalid or missing CRON_SECRET)
  • 500 - Internal server error

Max Duration: 300 seconds (5 minutes)

2. Generate Tags

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 - Unauthorized
  • 404 - Event or account not found
  • 500 - Internal server error

Max Duration: 300 seconds (5 minutes)

Process:

  1. Validates event exists and is IN_PROGRESS
  2. Gets unprocessed assets (searchTags IS NULL, tagAttemptCount < 5)
  3. For each asset:
    • IMAGE: Generates tags using Gemini vision
    • VOICE: Generates tags using Gemini audio + creates voice sample
      • Creates ElevenLabs voice clone (addVoice)
      • Generates AI sample text
      • Creates TTS audio sample
      • Uploads to S3
      • Persists voiceId to eleven_labs table
      • Updates likenessAssets.voiceSampleUrl
  4. Updates likenessAssets.searchTags
  5. Marks event COMPLETED and creates new queue event for re-indexing

3. Scrape Social

Endpoint: 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 - Success
  • 400 - Bad request (missing or invalid eventId)
  • 401 - Unauthorized
  • 404 - Event or account not found
  • 500 - Internal server error

Max Duration: 300 seconds (5 minutes)

Process:

  1. Validates event exists and is IN_PROGRESS
  2. Gets social links from account_social_links table
  3. TODO: Implement actual scraping logic per platform
  4. Updates follower counts in account_social_links
  5. If account missing image and scrape has avatar:
    • Uploads avatar to S3
    • Sets account.imageUrl
    • Creates likenessAssets entry
  6. Marks event COMPLETED and creates new queue event for re-indexing

Note: Actual scraping logic is a placeholder. Platform-specific scrapers need to be implemented.

4. Add Image Assets

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.

File Upload (Multipart Form Data)

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

URL-Based Upload (JSON)

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 error

Process:

  1. Authenticates user and resolves account ID
  2. For file uploads: validates MIME types, uploads to S3 (likeness-image-assets/)
  3. For URL uploads: validates URL strings
  4. Checks for duplicate contentUrl per account
  5. Creates likenessAssets records with type: "IMAGE"
  6. Creates indexing queue event (IMAGE_ASSET)
  7. Returns created asset IDs

5. Add Voice Assets

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.

File Upload (Multipart Form Data)

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,
});

URL-Based Upload (JSON)

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 error

Process:

  1. Authenticates user and resolves account ID
  2. For file uploads:
    • Validates file types (audio/video)
    • Uploads files to S3 (likeness-voice-assets/)
    • Collects S3 URLs
  3. For URL uploads:
    • Validates URLs (rejects video platforms)
    • Checks for duplicates
  4. Creates likenessAssets records with type: "VOICE"
  5. Creates indexing queue event (VOICE_ASSET)
  6. Returns created asset IDs

Validation:

  • Rejects video platform URLs (YouTube, Vimeo, TikTok, etc.)
  • Checks for duplicate contentUrl per account
  • Validates file types (must be audio files)

Note: 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.

GET Request

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, engagementRate
  • orderDirection - Sort direction: asc, desc

Example:

GET /api/indexing/search?q=blonde&limit=10&orderBy=numberOfFollowers&orderDirection=desc

POST Request

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 - Success
  • 400 - Bad request (invalid filters)
  • 500 - Internal server error

Max Duration: 60 seconds (1 minute)

Search Process:

  1. Validates filters
  2. Calls searchLikeness() service function
  3. SQL search runs first
  4. If no results and first page, vector fallback search runs
  5. Results are formatted with account and asset data
  6. Returns formatted results

Service Functions

The API routes use service functions directly from their source packages:

Asset Upload Functions

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.

Indexing Functions

Location: packages/likeness-search/src/
Import from: @zooly/likeness-search

  • indexingDaemon() - Main daemon loop
  • processNextEvent() - Process single queue event
  • processIndexingEvent(event) - Process specific event
  • upsertToIndex(accountId) - Upsert account to indexes
  • removeFromIndex(accountId) - Remove account from indexes

Search Functions

Location: packages/likeness-search/src/
Import from: @zooly/likeness-search

  • searchLikeness(filters, options) - Main search function
  • vectorFallbackSearch(filters, options) - Vector similarity search
  • formatSearchResults(results) - Format and enrich results
  • filtersToDescription(filters) - Convert filters to text

AI Functions

Location: packages/likeness-search/src/
Import from: @zooly/likeness-search

  • generateTagsFromImage(imageUrl) - Extract tags from image
  • generateTagsFromVoice(audioUrl) - Extract tags from voice

Note: 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 sample

Note: generateVoiceSampleText is an internal function used by createVoiceSample.

Utility Functions

Location: packages/likeness-search/src/
Import from: @zooly/likeness-search

  • aggregateAccountTags(accountId) - Aggregate tags from sources
  • normalizeTagsForVector(tags) - Normalize tags for embedding
  • triggerAITagGeneration(eventId) - Trigger AI generation API
  • triggerSocialScraping(accountId, eventId) - Trigger scraping API
  • triggerDataCollection(event, missingData) - Orchestrate triggers
  • handleApiError(error, retryCount) - Error classification and retry
  • classifyError(error) - Classify error type
  • calculateBackoffDelay(retryCount, baseDelay) - Calculate retry delay

Error Responses

All 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"
  ]
}

Rate Limiting

Currently, no rate limiting is implemented. Consider adding rate limiting for:

  • Search endpoint (public, may receive high traffic)
  • Generate tags endpoint (AI API costs)
  • Scrape social endpoint (scraping costs)

CORS

The search endpoint should allow CORS for frontend access. Internal endpoints should not allow CORS (only called by cron or internal services).

Monitoring

Monitor these endpoints for:

  • Response Times: Track p50, p95, p99 latencies
  • Error Rates: Track 4xx and 5xx error rates
  • Success Rates: Track successful vs failed operations
  • Queue Depth: Monitor pending events count
  • Search Patterns: Track common filter combinations

Usage Examples

Example 1: Trigger Indexing

curl -X GET https://app.zooly.ai/api/indexing/process-queue \
  -H "Authorization: Bearer $CRON_SECRET"

Example 2: Search for Models

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
  }'

Example 3: Search by Name

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"
    }
  }'