Skip to main content
DocsAPI ReferenceAuthentication API
Back to docs

Authentication API

Login, refresh, me, plans, checkout, Stripe webhooks

api
authentication

Authentication API Reference

Base path: /api/auth

All authentication endpoints use Keycloak Direct Access Grants for credential verification. Users are JIT-provisioned into the local creatiq_users table on first login. Subscription billing is handled via Stripe.


Endpoints

MethodPathAuthRate Limit
POST/api/auth/loginPublicAUTH
POST/api/auth/refreshPublicAUTH
GET/api/auth/meBearer JWT-
GET/api/auth/plansPublic-
POST/api/auth/checkoutBearer JWT-
POST/api/auth/billing-portalBearer JWT-
POST/api/auth/webhookStripe Signature-

POST /api/auth/login

Auth: Public Rate Limit: AUTH (10 requests per 15 minutes)

Description: Authenticate a user via Keycloak Direct Access Grants using email and password. On success, the user is JIT-provisioned (created or updated) in the local creatiq_users table. The assigned plan is derived from the Keycloak role: admin maps to premium, teacher maps to pro, all others default to free.

Request Body:

FieldTypeRequiredDescription
emailstringYesUser email address
passwordstringYesUser password

Response 200:

{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJSUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "expiresIn": 300,
    "user": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "email": "teacher@school.edu",
      "name": "Jane Doe",
      "plan": "pro"
    }
  }
}

Error Responses:

StatusError MessageCondition
400Email and password are requiredMissing email or password field
401Invalid email or passwordKeycloak rejects credentials
401Account is disabledKeycloak account disabled
500Failed to verify issued tokenToken verification failed
500Internal server errorUnexpected server error

POST /api/auth/refresh

Auth: Public Rate Limit: AUTH (10 requests per 15 minutes)

Description: Refresh an expired access token using a valid Keycloak refresh token. Returns a new access token and refresh token pair.

Request Body:

FieldTypeRequiredDescription
refreshTokenstringYesKeycloak refresh token

Response 200:

{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJSUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "expiresIn": 300
  }
}

Error Responses:

StatusError MessageCondition
400Refresh token is requiredMissing refreshToken field
401Token refresh failedInvalid or expired refresh token

GET /api/auth/me

Auth: Bearer JWT (via Authorization: Bearer <token> header) Rate Limit: None

Description: Retrieve the current authenticated user's profile, subscription usage statistics, and full plan details. The user is looked up by the email claim in the JWT.

Request Headers:

HeaderValue
AuthorizationBearer <access_token>

Response 200:

{
  "success": true,
  "data": {
    "user": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "email": "teacher@school.edu",
      "name": "Jane Doe",
      "plan": "pro"
    },
    "usage": {
      "plan": "pro",
      "contentsUsed": 12,
      "contentsLimit": 30,
      "aiUsed": 45,
      "aiLimit": 100,
      "storageUsed": 524288000,
      "storageLimit": 5368709120
    },
    "planDetails": {
      "id": "pro",
      "name": "Pro",
      "price": 4,
      "stripePriceId": "price_xxx",
      "limits": {
        "contentsPerMonth": 30,
        "aiGenerationsPerMonth": 100,
        "storageBytes": 5368709120
      }
    }
  }
}

Error Responses:

StatusError MessageCondition
401UnauthorizedMissing or invalid JWT
404User not foundNo matching user in database
500Internal server errorUnexpected server error

GET /api/auth/plans

Auth: Public Rate Limit: None

Description: List all available subscription plans with their pricing and limits. Returns the three tiers: Free, Pro, and Premium.

Response 200:

{
  "success": true,
  "data": [
    {
      "id": "free",
      "name": "Free",
      "price": 0,
      "limits": {
        "contentsPerMonth": 3,
        "aiGenerationsPerMonth": 5,
        "storageBytes": 104857600
      }
    },
    {
      "id": "pro",
      "name": "Pro",
      "price": 4,
      "limits": {
        "contentsPerMonth": 30,
        "aiGenerationsPerMonth": 100,
        "storageBytes": 5368709120
      }
    },
    {
      "id": "premium",
      "name": "Premium",
      "price": 8,
      "limits": {
        "contentsPerMonth": -1,
        "aiGenerationsPerMonth": -1,
        "storageBytes": 53687091200
      }
    }
  ]
}

Note: A limit value of -1 means unlimited.


POST /api/auth/checkout

Auth: Bearer JWT Rate Limit: None

Description: Create a Stripe Checkout session for upgrading to a paid plan. Returns a URL to redirect the user to the Stripe-hosted payment page. Only pro and premium plan IDs are accepted.

Request Headers:

HeaderValue
AuthorizationBearer <access_token>

Request Body:

FieldTypeRequiredDescription
planIdstringYesTarget plan: "pro" or "premium"

Response 200:

{
  "success": true,
  "data": {
    "url": "https://checkout.stripe.com/c/pay/cs_test_..."
  }
}

Error Responses:

StatusError MessageCondition
400Invalid planplanId missing or not pro/premium
401UnauthorizedMissing or invalid JWT
404User not foundNo matching user in database
500Internal server errorStripe not configured or unexpected error

POST /api/auth/billing-portal

Auth: Bearer JWT Rate Limit: None

Description: Create a Stripe Customer Portal session for the authenticated user. The portal allows users to manage their subscription, update payment methods, and view invoices. Requires the user to have an existing Stripe customer ID (i.e., have previously completed a checkout).

Request Headers:

HeaderValue
AuthorizationBearer <access_token>

Response 200:

{
  "success": true,
  "data": {
    "url": "https://billing.stripe.com/p/session/..."
  }
}

Error Responses:

StatusError MessageCondition
401UnauthorizedMissing or invalid JWT
404User not foundNo matching user in database
500Internal server errorNo Stripe customer ID or Stripe API error

POST /api/auth/webhook

Auth: Stripe Signature (stripe-signature header verified against STRIPE_WEBHOOK_SECRET) Rate Limit: None

Description: Handle Stripe webhook events. The request body must be raw (not JSON-parsed) for signature verification. Events are deduplicated via Redis with a 24-hour TTL to ensure idempotent processing.

Handled Event Types:

Event TypeAction
checkout.session.completedUpgrade user plan and store stripe_subscription_id
customer.subscription.deletedDowngrade user to free plan, clear subscription ID
invoice.payment_failedLog warning (no plan change)

Request Headers:

HeaderValue
stripe-signatureStripe webhook signature string
Content-Typeapplication/json (raw body)

Response 200:

{
  "success": true,
  "received": true
}

Response 200 (duplicate event):

{
  "success": true,
  "received": true,
  "duplicate": true
}

Error Responses:

StatusError MessageCondition
400Missing signatureNo stripe-signature header or no webhook secret configured
400Webhook errorSignature verification failed or processing error
500Payment system not configuredSTRIPE_SECRET_KEY not set

Plan Details

PlanPriceContents/MonthAI Generations/MonthStorage
free$035100 MB
pro$4/mo301005 GB
premium$8/moUnlimitedUnlimited50 GB

User Object Schema

The user object returned by /login and /me endpoints:

FieldTypeDescription
idstringUUID, primary key in creatiq_users
emailstringUser email (lowercased)
namestringDisplay name
planstringCurrent plan: free, pro, or premium

The /me endpoint additionally returns usage (current month counters vs. limits) and planDetails (full plan definition including Stripe price ID).


Environment Variables

VariableRequiredDescription
STRIPE_SECRET_KEYNo*Stripe API secret key (* payment features disabled without it)
STRIPE_WEBHOOK_SECRETNo*Stripe webhook endpoint signing secret
STRIPE_PRO_PRICE_IDNo*Stripe Price ID for the Pro plan
STRIPE_PREMIUM_PRICE_IDNo*Stripe Price ID for the Premium plan
JWT_SECRETYesSecret for signing internal JWT tokens
NEXT_PUBLIC_APP_URLNoApp URL for Stripe redirect URLs (default: http://localhost:17000)

Notes

  • JIT Provisioning: Users are created in creatiq_users on first login. The plan is derived from the Keycloak role claim (admin -> premium, teacher -> pro, others -> free).
  • Monthly Counters: contentsCreatedThisMonth and aiGenerationsThisMonth are automatically reset when the calendar month changes (checked on login).
  • External Identity: The Keycloak sub claim is stored as externalUserId and the tenant_id claim as tenantId, enabling cross-system user correlation.
  • Token Lifetime: Keycloak access tokens have a server-configured expiry (returned as expiresIn in seconds). Internal JWTs generated by subscriptionService.generateToken() expire in 7 days.
  • Webhook Idempotency: Stripe events are deduplicated using Redis keys with a 24-hour TTL (webhook:<event_id>).
Back to docsdocs/product/api/authentication.md