Base URL: https://api.frameiq.app (production) | http://localhost:8000 (local)
All endpoints require Authorization: Bearer <clerk_jwt> unless marked public.
Interactive docs available at /docs (Swagger UI) and /redoc.
POST /videos
Body
{
"youtube_url": "https://www.youtube.com/watch?v=abc123"
}Response 202 Accepted
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"youtube_url": "https://www.youtube.com/watch?v=abc123",
"status": "queued",
"created_at": "2026-06-07T10:00:00Z"
}Errors
| Code | Reason |
|---|---|
| 400 | Invalid YouTube URL |
| 402 | Plan limit reached (too many videos) |
| 409 | Video already indexed for this user |
GET /videos?page=1&limit=20&status=ready
Query params
| Param | Type | Default | Description |
|---|---|---|---|
page |
int | 1 | Page number |
limit |
int | 20 | Results per page (max 100) |
status |
string | — | Filter by status |
Response 200 OK
{
"items": [
{
"id": "550e8400-...",
"title": "ML Lecture 12: Linear Regression",
"youtube_id": "abc123",
"status": "ready",
"frame_count": 20,
"duration_secs": 600,
"created_at": "2026-06-07T10:00:00Z",
"indexed_at": "2026-06-07T10:05:00Z"
}
],
"total": 3,
"page": 1,
"limit": 20
}GET /videos/{video_id}
Response 200 OK
{
"id": "550e8400-...",
"title": "ML Lecture 12",
"status": "indexing",
"frame_count": null,
"duration_secs": 600,
"job": {
"stage": "describe",
"progress": 45
}
}DELETE /videos/{video_id}
Deletes: Qdrant collection, R2 objects, PostgreSQL rows.
Response 204 No Content
POST /videos/{video_id}/retry
Re-enqueues ingestion from the failed stage.
Response 202 Accepted
{ "status": "queued" }GET /videos/{video_id}/jobs
Poll this endpoint to track ingestion progress.
Response 200 OK
{
"video_id": "550e8400-...",
"status": "indexing",
"stages": [
{ "stage": "download", "progress": 100, "started_at": "...", "finished_at": "..." },
{ "stage": "extract", "progress": 100, "started_at": "...", "finished_at": "..." },
{ "stage": "describe", "progress": 60, "started_at": "...", "finished_at": null }
],
"overall_progress": 73
}POST /chat/{video_id}
Content-Type: application/json
Accept: text/event-stream
Body
{
"question": "What was written on the whiteboard at the start?",
"session_id": "optional-existing-session-uuid"
}Response 200 OK — Server-Sent Events stream
data: {"type": "session_id", "value": "abc-def-..."}
data: {"type": "frames_used", "value": [
{"timestamp": "00:30", "r2_key": "videos/.../frames/frame_000001.jpg"},
{"timestamp": "01:00", "r2_key": "videos/.../frames/frame_000002.jpg"}
]}
data: {"type": "token", "value": "At the 30-second mark,"}
data: {"type": "token", "value": " the whiteboard showed"}
data: {"type": "token", "value": " y = mx + b..."}
data: {"type": "done"}
Errors
| Code | Reason |
|---|---|
| 402 | Monthly query limit reached |
| 404 | Video not found or not owned by user |
| 409 | Video status is not ready |
GET /chat/{video_id}/sessions
Response 200 OK
{
"sessions": [
{
"id": "session-uuid",
"created_at": "2026-06-07T10:10:00Z",
"message_count": 4
}
]
}GET /chat/sessions/{session_id}
Response 200 OK
{
"session_id": "...",
"video_id": "...",
"messages": [
{
"role": "user",
"content": "What is on the whiteboard?",
"created_at": "2026-06-07T10:10:00Z"
},
{
"role": "assistant",
"content": "At the 30-second mark...",
"frame_timestamps": ["00:30", "01:00"],
"created_at": "2026-06-07T10:10:05Z"
}
]
}GET /usage
Response 200 OK
{
"plan": "free",
"videos": {
"used": 2,
"limit": 3
},
"queries": {
"used": 23,
"limit": 50,
"resets_at": "2026-07-01T00:00:00Z"
}
}POST /billing/checkout
Body
{ "plan": "pro" }Response 200 OK
{ "checkout_url": "https://checkout.stripe.com/..." }POST /billing/portal
Returns a Stripe Customer Portal URL for managing subscription, invoices, and payment methods.
Response 200 OK
{ "portal_url": "https://billing.stripe.com/..." }POST /webhooks/clerk
Handles: user.created, user.updated, user.deleted
POST /webhooks/stripe
Handles: customer.subscription.created, customer.subscription.updated,
customer.subscription.deleted
All errors follow RFC 9457 Problem Details:
{
"type": "https://frameiq.app/errors/plan-limit-exceeded",
"title": "Plan limit exceeded",
"status": 402,
"detail": "Your free plan allows 50 queries per month. Upgrade to Pro for 1,000 queries.",
"instance": "/chat/550e8400-..."
}| Tier | Limit |
|---|---|
| All users | 60 requests/minute per IP |
| Free plan | 50 queries/month |
| Pro plan | 1,000 queries/month |
| Enterprise | Unlimited |
Rate limit headers on every response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1717754400
GET /health
Public endpoint — no auth required. Used by Railway health probes and smoke tests.
Response 200 OK
{
"status": "ok",
"database": "connected",
"redis": "connected",
"qdrant": "connected",
"version": "1.0.0"
}Returns 503 Service Unavailable with the failing dependency name if any check fails.
GET /health/worker
Public endpoint. Reports Celery queue depth.
Response 200 OK
{
"status": "ok",
"queue_depth": 3
}