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
| Method | Path | Auth | Rate Limit |
|---|---|---|---|
| POST | /api/auth/login | Public | AUTH |
| POST | /api/auth/refresh | Public | AUTH |
| GET | /api/auth/me | Bearer JWT | - |
| GET | /api/auth/plans | Public | - |
| POST | /api/auth/checkout | Bearer JWT | - |
| POST | /api/auth/billing-portal | Bearer JWT | - |
| POST | /api/auth/webhook | Stripe 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:
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | User email address | |
| password | string | Yes | User 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:
| Status | Error Message | Condition |
|---|---|---|
| 400 | Email and password are required | Missing email or password field |
| 401 | Invalid email or password | Keycloak rejects credentials |
| 401 | Account is disabled | Keycloak account disabled |
| 500 | Failed to verify issued token | Token verification failed |
| 500 | Internal server error | Unexpected 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:
| Field | Type | Required | Description |
|---|---|---|---|
| refreshToken | string | Yes | Keycloak refresh token |
Response 200:
{
"success": true,
"data": {
"token": "eyJhbGciOiJSUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 300
}
}
Error Responses:
| Status | Error Message | Condition |
|---|---|---|
| 400 | Refresh token is required | Missing refreshToken field |
| 401 | Token refresh failed | Invalid 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:
| Header | Value |
|---|---|
| Authorization | Bearer <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:
| Status | Error Message | Condition |
|---|---|---|
| 401 | Unauthorized | Missing or invalid JWT |
| 404 | User not found | No matching user in database |
| 500 | Internal server error | Unexpected 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
-1means 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:
| Header | Value |
|---|---|
| Authorization | Bearer <access_token> |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| planId | string | Yes | Target plan: "pro" or "premium" |
Response 200:
{
"success": true,
"data": {
"url": "https://checkout.stripe.com/c/pay/cs_test_..."
}
}
Error Responses:
| Status | Error Message | Condition |
|---|---|---|
| 400 | Invalid plan | planId missing or not pro/premium |
| 401 | Unauthorized | Missing or invalid JWT |
| 404 | User not found | No matching user in database |
| 500 | Internal server error | Stripe 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:
| Header | Value |
|---|---|
| Authorization | Bearer <access_token> |
Response 200:
{
"success": true,
"data": {
"url": "https://billing.stripe.com/p/session/..."
}
}
Error Responses:
| Status | Error Message | Condition |
|---|---|---|
| 401 | Unauthorized | Missing or invalid JWT |
| 404 | User not found | No matching user in database |
| 500 | Internal server error | No 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 Type | Action |
|---|---|
checkout.session.completed | Upgrade user plan and store stripe_subscription_id |
customer.subscription.deleted | Downgrade user to free plan, clear subscription ID |
invoice.payment_failed | Log warning (no plan change) |
Request Headers:
| Header | Value |
|---|---|
| stripe-signature | Stripe webhook signature string |
| Content-Type | application/json (raw body) |
Response 200:
{
"success": true,
"received": true
}
Response 200 (duplicate event):
{
"success": true,
"received": true,
"duplicate": true
}
Error Responses:
| Status | Error Message | Condition |
|---|---|---|
| 400 | Missing signature | No stripe-signature header or no webhook secret configured |
| 400 | Webhook error | Signature verification failed or processing error |
| 500 | Payment system not configured | STRIPE_SECRET_KEY not set |
Plan Details
| Plan | Price | Contents/Month | AI Generations/Month | Storage |
|---|---|---|---|---|
| free | $0 | 3 | 5 | 100 MB |
| pro | $4/mo | 30 | 100 | 5 GB |
| premium | $8/mo | Unlimited | Unlimited | 50 GB |
User Object Schema
The user object returned by /login and /me endpoints:
| Field | Type | Description |
|---|---|---|
| id | string | UUID, primary key in creatiq_users |
| string | User email (lowercased) | |
| name | string | Display name |
| plan | string | Current 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
| Variable | Required | Description |
|---|---|---|
STRIPE_SECRET_KEY | No* | Stripe API secret key (* payment features disabled without it) |
STRIPE_WEBHOOK_SECRET | No* | Stripe webhook endpoint signing secret |
STRIPE_PRO_PRICE_ID | No* | Stripe Price ID for the Pro plan |
STRIPE_PREMIUM_PRICE_ID | No* | Stripe Price ID for the Premium plan |
JWT_SECRET | Yes | Secret for signing internal JWT tokens |
NEXT_PUBLIC_APP_URL | No | App URL for Stripe redirect URLs (default: http://localhost:17000) |
Notes
- JIT Provisioning: Users are created in
creatiq_userson first login. The plan is derived from the Keycloak role claim (admin-> premium,teacher-> pro, others -> free). - Monthly Counters:
contentsCreatedThisMonthandaiGenerationsThisMonthare automatically reset when the calendar month changes (checked on login). - External Identity: The Keycloak
subclaim is stored asexternalUserIdand thetenant_idclaim astenantId, enabling cross-system user correlation. - Token Lifetime: Keycloak access tokens have a server-configured expiry (returned as
expiresInin seconds). Internal JWTs generated bysubscriptionService.generateToken()expire in 7 days. - Webhook Idempotency: Stripe events are deduplicated using Redis keys with a 24-hour TTL (
webhook:<event_id>).