API Reference
All backend API endpoints with request/response examples
All endpoints (except /api/health) require a valid Supabase JWT in the Authorization: Bearer <token> header. The backend automatically scopes all operations to the authenticated user's tenant.
Base URLs:
- Production:
https://vantagedash-production.up.railway.app - Local:
http://localhost:8000
Health
GET /api/health
No auth required. Returns system status.
Response (200):
{
"status": "ok",
"supabase_connected": true,
"version": "1.2.0",
"timestamp": "2026-03-16T12:00:00Z"
}Degraded mode (settings failed to load):
{
"status": "degraded",
"supabase_connected": false,
"config_error": "supabase_url field required",
"env_hint": "SUPABASE_URL"
}Scraping
POST /api/scrape → 202
Start a competitor scraping session. Runs in background.
Request body (optional):
{
"competitor_id": "uuid" // scrape specific competitor, or all if omitted
}Response:
{
"session_id": "uuid",
"status": "running",
"message": "Scraping started"
}GET /api/scrape/status/{session_id}
Poll scraping progress.
Response:
{
"id": "uuid",
"status": "running|completed|failed",
"total_targets": 3,
"completed_targets": 1,
"successful_scrapes": 1,
"failed_scrapes": 0,
"total_products": 1250,
"progress_percent": 33.33
}Shopify Sync
POST /api/sync → 202
Start Shopify product sync. Runs in background.
Request body (optional):
{
"shop_url": "mybrand.myshopify.com",
"access_token": "shpat_..."
}If omitted, uses per-tenant encrypted credentials from DB, then env var fallback.
Response: Same TaskResponse format as scrape.
GET /api/sync/status/{session_id}
Poll sync progress. Returns sync_sessions row.
Product Matching
POST /api/match → 202
Start product matching. Runs in background.
Request body (optional):
{
"method": "ai" // "ai" | "fuzzy" | "hybrid"
}ai— GPT-4o-mini semantic matching (most accurate, costs ~$0.01/product)fuzzy— RapidFuzz string similarity (fast, free)hybrid— Fuzzy first, AI verification on uncertain matches (40-79%)
Response: Same TaskResponse format.
GET /api/match/status/{session_id}
Poll matching progress. Returns matching_sessions row.
POST /api/match/backfill → 202
Backfill AI-extracted attributes for products missing them.
Request body (optional):
{
"limit": 500
}POST /api/match/embed → 202
Generate vector embeddings for products.
Request body (optional):
{
"table": "product_tracking", // or "brand_products"
"limit": 500
}POST /api/match/validate → 202
Run parallel validation (old vs new matcher side-by-side).
Request body (optional):
{
"sample_size": 20 // 1-200
}GET /api/match/validate/{session_id}
Get validation results. When complete, returns a validation_report object:
{
"status": "completed",
"validation_report": {
"summary": {
"total": 20,
"both_match_same": 12,
"both_match_different": 3,
"old_only": 2,
"new_only": 1,
"both_nomatch": 2
},
"products": [
{
"brand_product": "Mylar Bag 4x5",
"verdict": "both_match_same",
"old_match": {"name": "Stand Up Pouch 4x5", "confidence": 72},
"new_match": {"name": "Stand Up Pouch 4x5", "confidence": 78}
}
]
}
}Industry Profile
GET /api/profile
Get the current tenant's industry profile.
Response: Full industry_profiles row or 404 if not configured.
PUT /api/profile
Create or update the tenant's industry profile.
Request body: Any combination of allowed fields:
{
"industry_slug": "packaging",
"industry_name": "Packaging & Containers",
"categories": [...],
"scoring_config": {...},
"search_terms": {...},
"size_equivalences": {...},
"hard_block_rules": [...],
"specialty_brands": {...},
"unit_types": [...],
"ai_extraction_categories": [...],
"ai_matching_rules": [...],
"use_new_matcher": false
}Field injection is prevented — only the 12 allowed fields are accepted. Automatically clears the profile cache on save.
DELETE /api/profile
Delete the tenant's industry profile.
Industry Templates
GET /api/profile/templates
List all available industry templates (metadata only).
Response:
[
{
"slug": "packaging",
"name": "Packaging & Containers",
"description": "Bags, jars, bottles, tubes, boxes...",
"example_products": ["Mylar bags 4x5", "Glass jars 2oz"]
}
]GET /api/profile/templates/{slug}
Get full template with all config fields.
POST /api/profile/templates/{slug}/apply
Apply a template to the tenant's profile (creates or replaces).
Tenant Credentials
GET /api/tenant/credentials
Check if Shopify credentials are configured. Never leaks secrets.
Response:
{
"has_shopify_url": true,
"has_shopify_token": true,
"shopify_url_preview": "mybrand.mysh..."
}PUT /api/tenant/credentials
Store encrypted Shopify credentials.
Request body:
{
"shopify_url": "mybrand.myshopify.com",
"shopify_token": "shpat_..."
}DELETE /api/tenant/credentials
Remove all stored credentials.
Data Lifecycle
GET /api/tenant/data/export
Export all tenant data (13 tables) as structured JSON. For GDPR data portability.
DELETE /api/tenant/data
Delete all tenant data. FK-ordered deletion across all tables. Clears credentials. Preserves the tenant row itself.
Alerts
GET /api/alerts/config
Get alert configuration for the tenant.
GET /api/alerts/history
Get alert history (price change notifications).
POST /api/alerts/evaluate
Manually trigger alert evaluation (auto-fires after scrape).
Scheduling
GET /api/schedule
Get scrape schedule for the tenant.
PUT /api/schedule
Create or update scrape schedule.
Request body:
{
"enabled": true,
"interval_hours": 24
}DELETE /api/schedule
Delete the scrape schedule.
Team Management
Role hierarchy: owner > admin > member > viewer.
All team endpoints require authentication. Write operations (invite, role change, remove) require owner or admin role via the require_role() dependency guard.
GET /api/team/me
Get current user's role and tenant info.
Response:
{
"user_id": "uuid",
"email": "user@example.com",
"tenant_id": "my-tenant-id",
"role": "owner"
}GET /api/team/members
List all team members. Enriches with email from Supabase Auth admin API.
Response:
[
{
"user_id": "uuid",
"email": "owner@company.com",
"role": "owner",
"created_at": "2026-01-01T00:00:00Z"
}
]POST /api/team/invite (owner, admin)
Send an invitation to join the tenant. Creates a team_invitations row and sends a Supabase invite email.
Request body:
{
"email": "colleague@company.com",
"role": "member"
}Validation rules:
- Cannot invite yourself
- Admins cannot invite other admins (only owner can)
- Rejects duplicate pending invitations
- Rejects if email is already a tenant member
Response:
{
"status": "created",
"email_sent": true,
"invitation": { "id": "uuid", "email": "...", "role": "member", ... }
}GET /api/team/invitations (owner, admin)
List all invitations for this tenant.
POST /api/team/invitations/{id}/resend (owner, admin)
Resend invitation email and refresh 7-day expiry.
DELETE /api/team/invitations/{id} (owner, admin)
Revoke a pending invitation (sets status to "revoked").
PATCH /api/team/members/{user_id}/role (owner, admin)
Change a member's role.
Request body:
{
"role": "viewer"
}Validation rules:
- Cannot change own role
- Cannot change owner's role
- Only owner can promote to/demote from admin
DELETE /api/team/members/{user_id} (owner, admin)
Remove a member from the tenant. Deletes their user_tenants row.
Validation rules:
- Cannot remove yourself
- Cannot remove the owner
- Only owner can remove admins
Billing
Stripe-powered subscription management. 3 tiers: Free ($0), Pro ($49/mo), Enterprise ($199/mo). The webhook endpoint bypasses rate limiting and JWT auth (uses Stripe signature verification instead).
GET /api/billing/subscription
Get current subscription info for the tenant.
Response:
{
"tenant_id": "my-tenant",
"plan": "free",
"status": "active",
"competitors_limit": 2,
"competitors_used": 1,
"features": {
"ai_matching": false,
"auto_scrape": false,
"embeddings": false,
"min_scrape_interval_hours": null
}
}GET /api/billing/plans
List available plans with features and pricing.
Response:
[
{
"id": "free",
"name": "Free",
"price": 0,
"competitors_limit": 2,
"features": ["Manual scraping", "Fuzzy matching", "CSV export"]
},
{
"id": "pro",
"name": "Pro",
"price": 4900,
"competitors_limit": 10,
"features": ["AI matching", "24h auto-scrape", "PDF export", "Webhooks"]
},
{
"id": "enterprise",
"name": "Enterprise",
"price": 19900,
"competitors_limit": -1,
"features": ["Unlimited competitors", "Vector embeddings", "1h auto-scrape", "Priority support"]
}
]GET /api/billing/usage
Current usage vs plan limits.
Response:
{
"competitors": {"used": 3, "limit": 10, "percent": 30.0},
"plan": "pro"
}POST /api/billing/checkout (owner, admin)
Create a Stripe Checkout session. Redirects user to Stripe-hosted payment page.
Request body:
{
"plan": "pro"
}Response:
{
"checkout_url": "https://checkout.stripe.com/c/pay/..."
}POST /api/billing/portal
Create a Stripe Billing Portal session for managing subscription, payment methods, and invoices.
Response:
{
"portal_url": "https://billing.stripe.com/p/session/..."
}POST /api/billing/webhook
Stripe webhook endpoint. No JWT auth required — authenticated via Stripe signature (Stripe-Signature header) verified against STRIPE_WEBHOOK_SECRET.
Events handled:
checkout.session.completed— Activates subscription after successful paymentcustomer.subscription.updated— Syncs plan changes (upgrades/downgrades)customer.subscription.deleted— Reverts to Free plan on cancellationinvoice.payment_failed— Marks subscription aspast_due
Response: 200 with {"status": "ok"} or 400 on signature verification failure.
Common Response Patterns
TaskResponse (202)
{
"session_id": "uuid",
"status": "running",
"message": "Task started"
}Error (4xx/5xx)
{
"detail": "Error description"
}Rate Limit (429)
{
"detail": "Rate limit exceeded"
}Headers: Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining
Frontend API Client
frontend/src/lib/api/backend.ts provides typed helpers:
startScrape(token, competitorId?)
getScrapeStatus(token, sessionId)
startSync(token, shopUrl?, accessToken?)
getSyncStatus(token, sessionId)
startMatch(token, method?)
getMatchStatus(token, sessionId)
startEmbed(token, table?, limit?)
getProfile(token)
saveProfile(token, data)
deleteProfile(token)
getMyRole(token)
getTeamMembers(token)
getTeamInvitations(token)
sendTeamInvite(token, email, role)
resendInvite(token, invitationId)
revokeInvite(token, invitationId)
updateMemberRole(token, userId, role)
removeMember(token, userId)
getBillingSubscription(token)
getBillingPlans(token)
getBillingUsage(token)
createCheckoutSession(token, plan)
createPortalSession(token)POST /api/match/recalculate-ppu
Recalculates pack_quantity and ppu for all brand_products and product_tracking rows belonging to the authenticated tenant.
Auth: JWT required Response: {"status": "ok", "updated_brand_products": 28, "updated_product_tracking": 150}
Uses variant titles (brand_products) and product names (product_tracking) to extract pack quantities, then computes price-per-unit. Products without quantity indicators default to pack_quantity=1.
Admin Endpoints (Session 37)
All admin endpoints require Authorization: Bearer <jwt> where the JWT belongs to a user in the super_admins table.
GET /api/admin/stats
Platform-wide KPIs. Returns total_tenants, active_tenants, total_users, scrape_sessions, success_rate, products_tracked, MRR, subscribers_by_plan.
GET /api/admin/tenants?page=1&limit=20
Paginated tenant list with competitor_count, product_count, member_count, plan_tier, last_scrape_at.
GET /api/admin/tenants/{tenant_id}
Tenant drill-down: full tenant details, members, competitors, recent scrapes, subscription info.
GET /api/admin/activity?limit=50
Cross-tenant activity feed (scrapes, matches, syncs merged and sorted by started_at desc).
GET /api/admin/errors?limit=50
Failed sessions across all tenants with error_message.
GET /api/admin/billing
Billing summary: subscribers_by_plan, total_active, MRR.
API Updates (Session 42)
Scrape Endpoint Changes
**POST /api/scrape/{competitor_id}** now creates a scrape_sessions row, enabling progress polling via GET /api/scrape/status/{session_id}. Previously only batch scrapes had session tracking.
Batch scrape resilience (POST /api/scrape):
- Per-competitor retry with exponential backoff (1 retry, 3s delay)
- Session-level
finallyblock guarantees sessions are markedcompletedorfailed(never stuck onrunning) - Alert evaluation also triggers after single-competitor scrapes
API Updates (Session 48)
Single-Competitor Scrape Bug Fix
Critical fix: POST /api/scrape/{competitor_id} was saving 0 products due to an RLS bypass bug. The single-competitor code path was not injecting the service-role DB client into scrape_and_save_store(), causing RLS to silently block all product_tracking inserts. Now fixed.
Single-Competitor Scrape Logging
POST /api/scrape/{competitor_id} now writes scrape_logs entries (one per competitor scraped), matching the behavior of batch scrapes. This enables debugging failed scrapes via the Logs page.