Skip to main content

Overview

Conformly.ai uses Supabase JWT tokens for authentication. All API requests (except payment endpoints) require a valid token in the Authorization header.

Base URL

Production:
https://beta-api.conformly.ai/api/v1
Local Development:
http://localhost:8000/api/v1

Authentication Header

Authorization: Bearer <supabase_jwt_token>

Endpoints

Get Current User Profile

Returns the authenticated user’s profile including subscription status.
curl -X GET "http://localhost:8000/api/v1/auth/me" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"
Response:
{
  "id": "f2427d3d-96c6-41a3-8a5d-9e0bd3fe4031",
  "email": "user@example.com",
  "name": "John Doe",
  "organization": "Acme Corp",
  "role": "engineer",
  "created_at": "2026-01-15T10:30:00Z",
  "last_login": "2026-03-03T14:00:00Z",
  "subscription_status": "active",
  "subscription_plan": "free"
}
FieldTypeDescription
idUUIDUser ID (matches Supabase auth.users)
emailstringUser email
namestringDisplay name
organizationstringCompany/organization
roleenumadmin, engineer, manager, or viewer
subscription_statusstringactive or inactive
subscription_planstringfree, pro, consultant, or null

Sync User Profile

Called by the frontend on every session load. Handles two scenarios:
  1. Pending subscription: If the user paid via Stripe before signing up, migrates the pending_subscription into their profile.
  2. No subscription: Activates the free plan automatically (sets subscription_status = 'active', subscription_plan = 'free').
curl -X POST "http://localhost:8000/api/v1/auth/sync" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id": "user-uuid", "email": "user@example.com", "name": "John Doe"}'
Response:
{
  "success": true,
  "message": "Profile synced successfully",
  "user_id": "f2427d3d-96c6-41a3-8a5d-9e0bd3fe4031",
  "email": "user@example.com",
  "subscription_migrated": false,
  "subscription_plan": "free"
}

Refresh JWT Token

curl -X POST "http://localhost:8000/api/v1/auth/refresh" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"
Response:
{
  "access_token": "new_jwt_token",
  "token_type": "bearer",
  "expires_in": 3600
}

Subscription Flow

Error Responses

401 Unauthorized:
{
  "error": {
    "code": "AUTHENTICATION_ERROR",
    "message": "Could not validate credentials",
    "details": {"reason": "Invalid token or token expired"},
    "timestamp": "2026-01-15T10:30:00Z",
    "request_id": "req_123456789"
  }
}
402 Payment Required (when using check_active_subscription dependency):
{
  "detail": "Active subscription required"
}

Frontend Integration

The ProtectedRoute component handles the full auth + subscription flow:
// 1. On page load, checkSession() runs:
//    - Calls POST /auth/sync (activates free plan if needed)
//    - Calls GET /auth/me (reads subscription_status)
//    - Sets authState to "authenticated" or "no-subscription"

// 2. On explicit sign-in (SIGNED_IN event):
//    - Defers sync + check via setTimeout to avoid Supabase deadlock
//    - Same flow as above

// 3. On sign-out:
//    - Clears session and redirects to /login
Never call supabase.auth.getSession() or refreshSession() from inside an onAuthStateChange callback. The Supabase client holds an internal lock during event dispatch, and nested calls will deadlock.