VantageDash

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 payment
  • customer.subscription.updated — Syncs plan changes (upgrades/downgrades)
  • customer.subscription.deleted — Reverts to Free plan on cancellation
  • invoice.payment_failed — Marks subscription as past_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 finally block guarantees sessions are marked completed or failed (never stuck on running)
  • 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.

On this page

HealthGET /api/healthScrapingPOST /api/scrape → 202GET /api/scrape/status/{session_id}Shopify SyncPOST /api/sync → 202GET /api/sync/status/{session_id}Product MatchingPOST /api/match → 202GET /api/match/status/{session_id}POST /api/match/backfill → 202POST /api/match/embed → 202POST /api/match/validate → 202GET /api/match/validate/{session_id}Industry ProfileGET /api/profilePUT /api/profileDELETE /api/profileIndustry TemplatesGET /api/profile/templatesGET /api/profile/templates/{slug}POST /api/profile/templates/{slug}/applyTenant CredentialsGET /api/tenant/credentialsPUT /api/tenant/credentialsDELETE /api/tenant/credentialsData LifecycleGET /api/tenant/data/exportDELETE /api/tenant/dataAlertsGET /api/alerts/configGET /api/alerts/historyPOST /api/alerts/evaluateSchedulingGET /api/schedulePUT /api/scheduleDELETE /api/scheduleTeam ManagementGET /api/team/meGET /api/team/membersPOST /api/team/invite (owner, admin)GET /api/team/invitations (owner, admin)POST /api/team/invitations/{id}/resend (owner, admin)DELETE /api/team/invitations/{id} (owner, admin)PATCH /api/team/members/{user_id}/role (owner, admin)DELETE /api/team/members/{user_id} (owner, admin)BillingGET /api/billing/subscriptionGET /api/billing/plansGET /api/billing/usagePOST /api/billing/checkout (owner, admin)POST /api/billing/portalPOST /api/billing/webhookCommon Response PatternsTaskResponse (202)Error (4xx/5xx)Rate Limit (429)Frontend API ClientPOST /api/match/recalculate-ppuAdmin Endpoints (Session 37)GET /api/admin/statsGET /api/admin/tenants?page=1&limit=20GET /api/admin/tenants/{tenant_id}GET /api/admin/activity?limit=50GET /api/admin/errors?limit=50GET /api/admin/billingAPI Updates (Session 42)Scrape Endpoint ChangesAPI Updates (Session 48)Single-Competitor Scrape Bug FixSingle-Competitor Scrape Logging