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
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"
}
| Field | Type | Description |
|---|
id | UUID | User ID (matches Supabase auth.users) |
email | string | User email |
name | string | Display name |
organization | string | Company/organization |
role | enum | admin, engineer, manager, or viewer |
subscription_status | string | active or inactive |
subscription_plan | string | free, pro, consultant, or null |
Sync User Profile
Called by the frontend on every session load. Handles two scenarios:
- Pending subscription: If the user paid via Stripe before signing up, migrates the
pending_subscription into their profile.
- 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.