Kit provides 34 REST API endpoints organized into 11 categories. Every endpoint uses Next.js App Router route handlers — there are no server actions. All requests and responses use JSON unless otherwise noted (file uploads use raw streams).
This page is a lookup reference. For architectural context, see Security Overview. For endpoint-specific guides, follow the links in each section.
Quick Reference
All endpoints at a glance. Click any path to jump to its detailed section.
| Path | Method(s) | Auth | Rate Limit | Description |
|---|---|---|---|---|
/api/user/status | GET | Clerk | api | Current user subscription status |
/api/checkout/create-url | POST | Clerk | payments | Create Lemon Squeezy checkout URL |
/api/payments/create-checkout | POST, GET | Clerk | payments | Create checkout session with options |
/api/payments/cancel-subscription | POST | Clerk | payments | Cancel active subscription |
/api/payments/change-plan | POST | Clerk | payments | Upgrade or downgrade plan |
/api/payments/create-portal-session | POST | Clerk | — | Generate customer portal URL |
/api/payments/customer-portal | POST | Clerk | payments | Customer portal redirect |
/api/payments/subscription | GET | Clerk | — | Current subscription details |
/api/credits/bonus-packages | GET | Clerk | api | Available bonus credit packages |
/api/credits/checkout | POST | Clerk | payments | Create bonus credit checkout URL |
/api/credits/history | GET | Clerk | api | Paginated credit transaction history |
/api/credits/preferences | GET | Clerk | api | Get bonus credit preference |
/api/credits/preferences | PATCH | Clerk | api | Update bonus credit preference |
/api/config/public | GET | — | — | Public pricing configuration |
/api/pricing/config | GET | — | — | Full pricing config with validation |
/api/ai/chat | POST | Clerk | credits | Synchronous AI completion |
/api/ai/stream | POST | Clerk | credits | Streaming AI completion (SSE) |
/api/ai/usage | GET | Clerk | — | AI usage stats and limits |
/api/ai/speech-to-text | POST | Clerk | credits | Audio transcription (Whisper STT) |
/api/ai/image-gen | POST | Clerk | credits | AI image generation (GPT Image) |
/api/ai/generate-content | POST | Clerk | credits | Template-based content generation (SSE) |
/api/ai/rag/ask | POST | Clerk | credits | RAG-powered Q&A |
/api/ai/rag/conversations | GET, DELETE | Clerk | — | List or delete conversations |
/api/ai/rag/conversations/[id] | GET | Clerk | — | Single conversation with messages |
/api/upload | POST | Clerk | upload | Upload file to Vercel Blob |
/api/files/[id] | GET, DELETE | Clerk | — | Get or delete uploaded file |
/api/email/send | POST | Clerk | Send transactional email | |
/api/email/test | POST | Clerk | — | Send test email (dev only) |
/api/webhooks/clerk | POST | Svix | webhooks | Clerk user lifecycle events |
/api/webhooks/lemonsqueezy | POST | HMAC | webhooks | Payment and subscription events |
/api/webhooks/resend | POST | HMAC | webhooks | Email delivery status events |
/api/cron/check-trials | POST, GET | Bearer | — | Expire ended trial subscriptions |
/api/cron/expire-bonus-credits | POST, GET | Bearer | — | Expire bonus credit purchases |
/api/health | GET | — | — | Application health check |
Rate Limit Categories
Every rate-limited endpoint belongs to one of six categories. Rate limits are enforced per-user and/or per-IP using Upstash Redis:
Rate limit configuration per category
const API_LIMITS: Record<
APICategory,
{ user?: RateLimitConfig; ip?: RateLimitConfig }
> = {
upload: {
user: { requests: 10, window: '1 h', identifier: 'user' },
ip: { requests: 20, window: '1 h', identifier: 'ip' },
},
email: {
user: { requests: 5, window: '1 h', identifier: 'user' },
ip: { requests: 10, window: '1 h', identifier: 'ip' },
},
contact: {
ip: { requests: 3, window: '1 h', identifier: 'ip' },
},
payments: {
user: { requests: 20, window: '1 h', identifier: 'user' },
},
webhooks: {
ip: { requests: 100, window: '1 h', identifier: 'ip' },
},
api: {
user: { requests: 100, window: '1 h', identifier: 'user' },
ip: { requests: 200, window: '1 h', identifier: 'ip' },
},
}
If Redis is unavailable, rate limiting is bypassed and requests are allowed through. This prevents infrastructure issues from blocking legitimate users. Monitor for
"Rate limiting disabled" warnings in production logs.AI endpoints use a separate credit-based rate limiting system — see Cost Management for details.
Authentication & User
Get User Status
GET
/api/user/statusReturns the authenticated user's subscription status for personalized UI display (e.g., dynamic CTA buttons on the pricing page).
- Auth: Clerk (required)
- Rate Limit:
api— 100 req/hour per user - Cache: 5 minutes (
private, max-age=300)
Response (200):
json
{
"status": "active_subscriber",
"tier": "pro",
"billingPeriod": "monthly",
"isTrial": false,
"needsAction": false,
"isLocked": false
}
Error Codes:
401 Unauthorized, 429 Rate limited, 500 Server errorPayments
Eight endpoints manage the complete payment lifecycle — from checkout creation through subscription management.
All payment endpoints use the
payments rate limit category (20 requests/hour per user). For payment architecture details, see Payments Overview.Create Checkout URL
POST
/api/checkout/create-urlCreates a Lemon Squeezy checkout URL for a specific plan variant.
- Auth: Clerk (required)
- Rate Limit:
payments
Request Body:
json
{
"variantId": "123456",
"tier": "pro"
}
Response (200):
json
{
"checkoutUrl": "https://nextsaas.lemonsqueezy.com/checkout/..."
}
Error Codes:
400 Invalid variant/tier, 401 Unauthorized, 429 Rate limited, 500 Server errorCreate Checkout Session
POSTGET
/api/payments/create-checkoutCreates a checkout session with extended options including redirect URLs and discount codes.
- Auth: Clerk (required)
- Rate Limit:
payments
Request Body:
json
{
"variantId": "123456",
"productId": "789",
"redirectUrl": "https://yourapp.com/success",
"cancelUrl": "https://yourapp.com/cancel",
"discountCode": "LAUNCH20"
}
Only
variantId is required. All other fields are optional.Response (200):
json
{
"checkoutUrl": "https://nextsaas.lemonsqueezy.com/checkout/..."
}
This endpoint also supports GET requests with query parameters
tier and billing for the registration-first checkout flow:GET /api/payments/create-checkout?tier=pro&billing=annual
→ 302 Redirect to Lemon Squeezy checkout
Error Codes:
400 Validation failed, 401 Unauthorized, 404 User not found, 500 Server errorCancel Subscription
POST
/api/payments/cancel-subscriptionCancels the user's active subscription. The subscription remains active until the current billing period ends.
- Auth: Clerk (required)
- Rate Limit:
payments
Request Body: None required.
Response (200):
json
{
"message": "Subscription cancelled successfully",
"endsAt": "2026-03-14T00:00:00.000Z"
}
Error Codes:
401 Unauthorized, 404 No active subscription, 500 Server errorChange Subscription Plan
POST
/api/payments/change-planUpgrades or downgrades the user's subscription to a different plan variant.
- Auth: Clerk (required)
- Rate Limit:
payments
Request Body:
json
{
"newVariantId": "654321"
}
Response (200):
json
{
"success": true,
"message": "Plan changed successfully",
"newVariantId": "654321",
"subscription": {
"status": "active",
"currentPeriodEnd": "2026-03-14T00:00:00.000Z"
}
}
During a trial period, users can downgrade a maximum of 2 times. The
downgradeCount is tracked on the user record. After 2 downgrades, further downgrade attempts are blocked until the trial ends.Error Codes:
400 Invalid variant or downgrade limit reached, 401 Unauthorized, 404 No subscription, 500 Server errorCreate Portal Session
POST
/api/payments/create-portal-sessionGenerates a Lemon Squeezy customer portal URL where users can manage billing, update payment methods, and view invoices.
- Auth: Clerk (required)
- Rate Limit: —
Response (200):
json
{
"url": "https://nextsaas.lemonsqueezy.com/billing/..."
}
The portal URL is valid for 24 hours.
Error Codes:
401 Unauthorized, 404 No subscription, 500 Server errorCustomer Portal Access
POST
/api/payments/customer-portalAlternative endpoint for customer portal access. Returns the portal URL in a
portalUrl field.- Auth: Clerk (required)
- Rate Limit:
payments
Response (200):
json
{
"portalUrl": "https://nextsaas.lemonsqueezy.com/billing/..."
}
Error Codes:
401 Unauthorized, 404 No subscription, 500 Server errorGet Subscription Details
GET
/api/payments/subscriptionReturns the user's current subscription details, including plan tier, billing period, feature limits, and trial status.
- Auth: Clerk (required)
- Rate Limit: —
- Cache: 60 seconds
Response (200):
json
{
"subscription": {
"id": "sub_123",
"status": "active",
"variantId": "123456",
"currentPeriodEnd": "2026-03-14T00:00:00.000Z"
},
"hasActiveSubscription": true,
"tier": "pro",
"billingPeriod": "monthly",
"planName": "Pro",
"isFreePlan": false,
"user": {
"downgradeCount": 0,
"hasUsedTrial": true
},
"limits": {}
}
Returns
subscription: null and isFreePlan: true for users without an active subscription.Error Codes:
401 Unauthorized, 404 User not found, 500 Server errorDebug Payment State
GET
/api/payments/debugDevelopment-only endpoint that returns detailed debug information about the user's payment state.
- Auth: Clerk (required) or
?key=debugquery parameter in development - Rate Limit: —
This endpoint is intended for development debugging only. It exposes internal subscription state and environment configuration. Never expose it to end users in production.
Error Codes:
401 Unauthorized, 500 Server errorCredits
Four endpoints manage bonus credit packages and preferences — listing available packages, creating checkout sessions, viewing transaction history, and managing per-user bonus credit preferences. These are only available in credit-based pricing mode.
These endpoints require
NEXT_PUBLIC_PRICING_MODEL=credit_based and NEXT_PUBLIC_BONUS_CREDITS_ENABLED=true. When either condition is false, they return 404. For credit system architecture, see Credit System.List Bonus Packages
GET
/api/credits/bonus-packagesReturns the bonus credit packages available for the authenticated user's subscription tier, along with monthly purchase limits and current usage.
- Auth: Clerk (required)
- Rate Limit:
api— 100 req/hour per user
Response (200):
json
{
"enabled": true,
"packages": [
{
"credits": 500,
"price": 9.99,
"variantId": "334455"
},
{
"credits": 1000,
"price": 19.99,
"variantId": "334456"
}
],
"monthlyLimit": 2000,
"monthlyPurchasedCredits": 500,
"canPurchaseMore": true,
"tier": "pro"
}
| Field | Type | Description |
|---|---|---|
enabled | boolean | Always true when the endpoint returns 200 |
packages | array | Available packages for the user's tier (up to 2 per tier) |
monthlyLimit | number | Maximum bonus credits purchasable per calendar month |
monthlyPurchasedCredits | number | Bonus credits already purchased this month |
canPurchaseMore | boolean | Whether the user is below the monthly purchase limit |
tier | string | User's current subscription tier |
Free-tier users cannot purchase bonus credits — they receive an empty
packages array with monthlyLimit: 0.Error Codes:
401 Unauthorized, 404 Feature disabled or not credit-based, 429 Rate limited, 500 Server errorCreate Credit Checkout
POST
/api/credits/checkoutCreates a Lemon Squeezy checkout URL for purchasing a bonus credit package. Validates the variant ID against the user's tier packages and enforces monthly purchase limits.
- Auth: Clerk (required)
- Rate Limit:
payments— 20 req/hour per user
Request Body (Zod-validated):
json
{
"variantId": "334455"
}
Response (200):
json
{
"checkoutUrl": "https://nextsaas.lemonsqueezy.com/checkout/..."
}
After successful payment, the user is redirected to
/dashboard/credits?purchase=success. The order_created webhook processes the purchase asynchronously.Error Codes:
400 Invalid variant ID or not in tier packages, 401 Unauthorized, 403 Monthly purchase limit reached, 404 Feature disabled or not credit-based, 429 Rate limited, 500 Server errorGet Credit History
GET
/api/credits/historyReturns a paginated list of credit transactions for the authenticated user, filtered by year.
- Auth: Clerk (required)
- Rate Limit:
api— 100 req/hour per user - Cache: 60 seconds (
private, max-age=60)
Query Parameters:
| Parameter | Type | Default | Constraints | Description |
|---|---|---|---|---|
limit | number | 50 | 1–100 | Number of transactions per page |
offset | number | 0 | >= 0 | Pagination offset |
year | number | Current year | 2000–current+1 | Filter by calendar year |
Response (200):
json
{
"transactions": [
{
"id": "tx_abc123",
"amount": -1.5,
"balanceAfter": 848.5,
"type": "usage",
"operation": "chat_streaming",
"metadata": { "model": "gpt-5-nano" },
"createdAt": "2026-02-14T10:30:00.000Z"
}
],
"totalCount": 153,
"availableYears": [2025, 2026]
}
| Field | Type | Description |
|---|---|---|
transactions | array | Credit transaction records, newest first |
totalCount | number | Total transactions matching the year filter |
availableYears | number[] | Years with at least one transaction (ascending) |
Transaction
amount is negative for deductions and positive for credits (purchases, refunds, resets). The type field identifies the category: usage, purchase, refund, monthly_reset, or adjustment.Error Codes:
401 Unauthorized, 429 Rate limited, 500 Server errorGet Credit Preferences
GET
/api/credits/preferencesReturns the user's bonus credit auto-use preference and the feature toggle status. Only available when credit-based pricing is active.
- Auth: Clerk (required)
- Rate Limit:
api— 100 req/hour per user - Guard: Returns
404if not in credit-based pricing mode
Response (200):
json
{
"bonusCreditsAutoUse": false,
"userToggleEnabled": true
}
| Field | Type | Description |
|---|---|---|
bonusCreditsAutoUse | boolean | Whether the user has opted into automatic bonus credit consumption |
userToggleEnabled | boolean | Whether the per-user toggle feature is enabled system-wide |
Error Codes:
401 Unauthorized, 404 Not credit-based, 429 Rate limited, 500 Server errorUpdate Credit Preferences
PATCH
/api/credits/preferencesUpdates the user's bonus credit auto-use preference. Only available when both credit-based pricing and the user toggle feature are enabled.
- Auth: Clerk (required)
- Rate Limit:
api— 100 req/hour per user - Guard: Returns
404if not credit-based,403if user toggle feature is disabled - Validation:
bonusPreferencesSchema
Request Body:
json
{
"bonusCreditsAutoUse": true
}
Response (200):
json
{
"success": true,
"bonusCreditsAutoUse": true
}
Error Codes:
400 Invalid request body, 401 Unauthorized, 403 User toggle not enabled, 404 Not credit-based, 429 Rate limited, 500 Server errorConfiguration
Get Public Config
GET
/api/config/publicReturns the public pricing configuration. Used by the pricing page and checkout flows to display current plans, prices, and features.
- Auth: None
- Rate Limit: None
- Cache: 1 hour (
public, max-age=3600) - Runtime: Edge
Response (200):
json
{
"model": "credit_based",
"currency": "EUR",
"tiers": [
{ "key": "basic", "name": "Basic", "monthlyPrice": 9.90 },
{ "key": "pro", "name": "Pro", "monthlyPrice": 19.90 },
{ "key": "enterprise", "name": "Enterprise", "monthlyPrice": 39.90 }
],
"features": { "freeTierEnabled": true },
"tierVisibility": {}
}
The response shape varies based on the active pricing model (
credit_based or classic_saas). Credit-based responses include creditBased fields; classic SaaS responses include classicSaas fields.Get Pricing Config
GET
/api/pricing/configReturns the complete pricing configuration with server-side validation. Ensures all required environment variables are set before returning.
- Auth: None
- Rate Limit: None
- Cache: 60 seconds (
must-revalidate)
Error Codes:
500 Incomplete configuration (missing environment variables)AI Chat
Six endpoints power the standard (non-RAG) AI system: synchronous completions, streaming, speech-to-text, usage tracking, image generation, and content generation.
AI Chat Completion
POST
/api/ai/chatGenerates AI completions synchronously. Supports single requests and batch mode (up to 10 concurrent requests).
- Auth: Clerk (required)
- Rate Limit: Credit-based (15 credits per request)
- Feature Guard: Returns
404ifNEXT_PUBLIC_AI_LLM_CHAT_ENABLEDisfalse
Request Body (Zod-validated):
ChatRequestSchema — request validation
const ChatRequestSchema = z.object({
messages: z.array(
z.object({
role: z.enum(['system', 'user', 'assistant', 'function', 'tool']),
content: z.string(),
name: z.string().optional(),
})
),
model: z.string().optional(),
temperature: z.number().min(0).max(2).optional(),
maxTokens: z.number().positive().optional(),
systemPrompt: z.string().optional(),
context: z.string().optional(),
// Optional caching control
cache: z.boolean().optional(),
cacheTTL: z.number().positive().optional(),
})
Response (200):
json
{
"text": "The AI response content...",
"model": "gpt-5-nano",
"usage": { "promptTokens": 150, "completionTokens": 80 },
"cost": 15
}
Error Codes:
400 Validation failed, 401 Unauthorized, 402 Insufficient credits, 429 Rate limited, 500 Server errorAI Streaming Completion
POST
/api/ai/streamGenerates AI completions with Server-Sent Events (SSE) streaming. Supports text-only and multimodal (vision) requests via
ContentPart[] message format.- Auth: Clerk (required)
- Rate Limit: Credit-based (20 credits for text, 30 credits for image analysis — auto-detected)
- Response Type:
text/event-stream
Request Body (text-only):
json
{
"messages": [{ "role": "user", "content": "Hello" }],
"model": "gpt-5-nano",
"temperature": 0.7,
"maxTokens": 1000,
"systemPrompt": "You are a helpful assistant."
}
Request Body (multimodal with images):
json
{
"messages": [{
"role": "user",
"content": [
{ "type": "image", "image": "data:image/png;base64,..." },
{ "type": "text", "text": "Describe this image" }
]
}]
}
Response: SSE stream with
X-RateLimit-* headers.Error Codes:
400 Validation failed, 401 Unauthorized, 402 Insufficient credits, 429 Rate limited, 500 Server errorGet AI Usage Stats
GET
/api/ai/usageReturns AI usage statistics and remaining limits for the authenticated user.
- Auth: Clerk (required)
- Rate Limit: —
Response (200) — Credit system enabled:
json
{
"unlimited": false,
"tier": "pro",
"monthlyLimit": 5000,
"remaining": 4847,
"bonusCredits": 50,
"used": 153,
"resetAt": "2026-02-01T00:00:00.000Z"
}
Response (200) — Credit system disabled (classic SaaS):
json
{
"unlimited": true,
"tier": "unlimited",
"monthlyLimit": 999999,
"remaining": 999999,
"bonusCredits": 0,
"used": 0
}
| Field | Type | Description |
|---|---|---|
unlimited | boolean | true when credit system is disabled (classic SaaS mode) |
tier | string | Subscription tier (free, basic, pro, enterprise) or "unlimited" |
monthlyLimit | number | Monthly credit allocation for the user's tier |
remaining | number | Available subscription credits this billing cycle |
bonusCredits | number | Purchased bonus credits (separate from subscription allocation) |
used | number | Credits consumed this billing cycle (monthlyLimit - remaining) |
resetAt | string? | ISO 8601 timestamp of last credit reset (absent when credit system disabled) |
Error Codes:
401 Unauthorized, 404 User not found, 500 Server errorSpeech-to-Text
POST
/api/ai/speech-to-textTranscribes audio recordings using the OpenAI Whisper API. Used by the Audio Input feature in LLM Chat.
- Auth: Clerk (required)
- Rate Limit: Credit-based (20 credits per transcription)
- Content Type:
multipart/form-data - Feature Guard: Returns
404ifNEXT_PUBLIC_AI_AUDIO_INPUT_ENABLEDisfalse
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
audio | File | Yes | Audio file (WebM, WAV, MP3, M4A, OGG, FLAC — max 25 MB) |
Response (200):
json
{
"text": "The transcribed text content...",
"language": "en",
"duration": 5.2
}
| Field | Type | Description |
|---|---|---|
text | string | Transcribed text from the audio |
language | string | Detected language code (ISO 639-1) |
duration | number | Audio duration in seconds |
Error Codes:
400 No audio file provided or invalid format, 401 Unauthorized, 402 Insufficient credits, 404 Feature disabled, 413 File too large (>25 MB), 500 Server errorAI Image Generation
POST
/api/ai/image-genGenerates images using OpenAI's GPT Image API. Supports multiple models, sizes, quality levels, and output formats.
- Auth: Clerk (required)
- Rate Limit: Credit-based (80 credits per generation)
- Feature Guard: Returns
404ifNEXT_PUBLIC_AI_IMAGE_GEN_ENABLEDisfalse - Max Duration: 60 seconds
Request Body (Zod-validated):
json
{
"prompt": "A mountain landscape at sunset",
"model": "gpt-image-1",
"size": "1024x1024",
"quality": "auto",
"output_format": "png",
"background": "opaque"
}
| Field | Type | Required | Default | Options |
|---|---|---|---|---|
prompt | string | Yes | — | 1-4000 characters |
model | string | No | gpt-image-1 | gpt-image-1, gpt-image-1.5, gpt-image-1-mini |
size | string | No | 1024x1024 | 1024x1024, 1536x1024, 1024x1536, auto |
quality | string | No | auto | low, medium, high, auto |
output_format | string | No | png | png, jpeg, webp |
background | string | No | opaque | opaque, transparent |
Transparent background is only supported with PNG and WebP formats. Requesting
background: "transparent" with output_format: "jpeg" returns a 400 error.Response (200):
json
{
"image": "data:image/png;base64,...",
"model": "gpt-image-1",
"size": "1024x1024",
"quality": "auto",
"format": "png",
"revised_prompt": "A detailed mountain landscape...",
"credits_used": 80,
"credits_remaining": 4920
}
Error Codes:
400 Validation failed or content policy violation, 401 Unauthorized, 402 Insufficient credits, 404 Feature disabled, 429 Rate limited, 500 Server errorAI Content Generation
POST
/api/ai/generate-contentGenerates template-based text content with Server-Sent Events (SSE) streaming. Supports five content templates with configurable tone, language, and length.
- Auth: Clerk (required)
- Rate Limit: Credit-based (25 credits per generation)
- Response Type:
text/event-stream - Feature Guard: Returns
404ifNEXT_PUBLIC_AI_CONTENT_GEN_ENABLEDisfalse - Max Duration: 60 seconds
Request Body (Zod-validated):
json
{
"template": "email",
"fields": {
"recipient": "Customer",
"subject": "Welcome to our platform",
"key_points": "Introduction, features overview"
},
"tone": "professional",
"language": "en",
"length": "medium"
}
| Field | Type | Required | Default | Options |
|---|---|---|---|---|
template | string | Yes | — | email, product_description, blog_outline, social_media, marketing_copy |
fields | object | Yes | — | Template-specific key-value pairs |
tone | string | No | professional | professional, friendly, persuasive, neutral |
language | string | No | en | de, en, fr, es |
length | string | No | medium | short, medium, long |
Response: SSE stream with content chunks, followed by a meta event with credit info, then
[DONE].Error Codes:
400 Validation failed or missing required template fields, 401 Unauthorized, 402 Insufficient credits, 404 Feature disabled, 429 Rate limited, 500 Server errorAI RAG — Authenticated
Three endpoints for the authenticated RAG (Retrieval-Augmented Generation) chat system with conversation persistence.
Ask RAG Question
POST
/api/ai/rag/askSubmits a question to the RAG system. Retrieves relevant document chunks, generates a context-aware response, and persists the conversation.
- Auth: Clerk (required)
- Rate Limit: Credit-based (varies by query complexity)
- Feature Guard: Returns
404ifNEXT_PUBLIC_AI_RAG_CHAT_ENABLEDisfalse
Request Body:
json
{
"question": "How do I configure authentication?",
"conversationId": "conv_abc123",
"stream": true
}
Accepts either
question (string) or messages (Vercel AI SDK format). The conversationId is optional — omit it to start a new conversation.Streaming Response Headers:
X-RAG-Sources— Base64-encoded JSON string of source document referencesX-RAG-Chunks-Used— Number of retrieved chunksX-RAG-Tokens-Estimated— Estimated token count
Error Codes:
400 Missing question, 401 Unauthorized, 402 Insufficient credits, 500 Server errorList RAG Conversations
GETDELETE
/api/ai/rag/conversationsLists the user's RAG conversations with pagination.
- Auth: Clerk (required)
- Rate Limit: —
Query Parameters:
limit— Number of conversations (default:20)offset— Pagination offset (default:0)
Response (200):
json
{
"conversations": [
{
"id": "conv_abc123",
"title": "Authentication Setup",
"messageCount": 5,
"lastMessage": "You can configure...",
"createdAt": "2026-02-14T10:00:00.000Z"
}
],
"pagination": { "total": 12, "limit": 20, "offset": 0, "hasMore": false }
}
DELETE method on the same path deletes a conversation:
DELETE /api/ai/rag/conversations?id=conv_abc123
→ { "success": true, "deleted": 1 }
Error Codes:
400 Missing ID (DELETE), 401 Unauthorized, 404 Not found, 500 Server errorGet RAG Conversation
GET
/api/ai/rag/conversations/[id]Returns a single conversation with all its messages.
- Auth: Clerk (required)
- Rate Limit: —
Response (200):
json
{
"id": "conv_abc123",
"title": "Authentication Setup",
"tier": "pro",
"messages": [
{
"id": "msg_1",
"role": "user",
"content": "How do I configure authentication?",
"tokens": 8,
"model": "gpt-5-nano",
"createdAt": "2026-02-14T10:00:00.000Z"
}
]
}
Error Codes:
401 Unauthorized, 404 Conversation not found, 500 Server errorFile Storage
Upload File
POST
/api/uploadUploads a file to Vercel Blob storage. The file content is streamed directly from the request body — no multipart form data.
- Auth: Clerk (required)
- Rate Limit:
upload— 10 req/hour per user, 20 req/hour per IP
Query Parameters (required):
filename— Original file namefilesize— File size in bytesfiletype— MIME type (e.g.,image/png)
Request Body: Raw file content as
ReadableStream.Response (200):
json
{
"id": "file_abc123",
"url": "https://blob.vercel-storage.com/...",
"originalName": "document.pdf",
"size": 245760,
"contentType": "application/pdf",
"createdAt": "2026-02-14T10:00:00.000Z"
}
Error Codes:
400 Missing params or invalid file type, 401 Unauthorized, 409 Duplicate file, 503 Database error, 500 Server errorGet or Delete File
GETDELETE
/api/files/[id]Returns metadata for a specific uploaded file. Only the file owner can access it.
- Auth: Clerk (required)
- Rate Limit: —
Response (200):
json
{
"id": "file_abc123",
"url": "https://blob.vercel-storage.com/...",
"originalName": "document.pdf",
"size": 245760,
"contentType": "application/pdf",
"createdAt": "2026-02-14T10:00:00.000Z"
}
DELETE method on the same path removes the file from both Vercel Blob and the database:
DELETE /api/files/file_abc123
→ { "success": true, "message": "File deleted successfully" }
Error Codes:
401 Unauthorized, 404 File not found, 500 Server errorSend Email
POST
/api/email/sendSends a transactional email through Resend. Requires either HTML or plain text content.
- Auth: Clerk (required)
- Rate Limit:
email— 5 req/hour per user, 10 req/hour per IP
Request Body:
json
{
"to": "user@example.com",
"subject": "Welcome to our platform",
"html": "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
"type": "welcome"
}
Either
html or text must be provided.Response (200):
json
{
"success": true,
"messageId": "msg_resend_123",
"emailLogId": "log_abc123"
}
Error Codes:
400 Validation failed, 401 Unauthorized, 404 User not found, 500 Server errorSend Test Email
POST
/api/email/testSends a test email for development and debugging. Only available in non-production environments.
- Auth: Clerk (optional)
- Rate Limit: —
Request Body:
json
{
"email": "dev@example.com"
}
Error Codes:
400 Missing email, 403 Not in development, 500 Server errorWebhooks
All three webhook endpoints verify incoming signatures before processing events. They use the
webhooks rate limit category (100 req/hour per IP).Webhook secrets (
CLERK_WEBHOOK_SECRET, LEMONSQUEEZY_WEBHOOK_SECRET, RESEND_WEBHOOK_SECRET) must be server-side only. Never prefix them with NEXT_PUBLIC_.Clerk Webhook
POST
/api/webhooks/clerkProcesses Clerk user lifecycle events. Creates, updates, or deletes users in the database.
- Auth: Svix signature verification (
CLERK_WEBHOOK_SECRET) - Rate Limit:
webhooks
Handled Events:
| Event | Action |
|---|---|
user.created | Creates user with Free Tier credits |
user.updated | Updates email and name |
user.deleted | Deletes user from database |
Response:
200 on success, 400 if Svix headers are missing, 401 if signature verification fails.Lemon Squeezy Webhook
POST
/api/webhooks/lemonsqueezyProcesses Lemon Squeezy payment and subscription events. This is the most complex webhook handler, supporting 11 event types.
- Auth: HMAC signature verification (
LEMONSQUEEZY_WEBHOOK_SECRET) - Rate Limit:
webhooks
Handled Events:
| Event | Action |
|---|---|
subscription_created | Create subscription record |
subscription_updated | Update plan/status |
subscription_cancelled | Mark for end-of-period cancellation |
subscription_resumed | Reactivate cancelled subscription |
subscription_expired | Downgrade to free tier |
subscription_paused | Pause subscription |
subscription_unpaused | Resume paused subscription |
subscription_payment_failed | Log failure, notify user |
subscription_payment_success | Confirm payment |
subscription_payment_recovered | Clear failed state |
order_created | Process one-time purchase (bonus credits) |
The Lemon Squeezy webhook handler returns
200 even when an individual event handler fails. This prevents Lemon Squeezy from retrying events that would fail again. Handler errors are logged for debugging.Resend Webhook
POST
/api/webhooks/resendProcesses Resend email delivery status events (delivered, bounced, complained).
- Auth: HMAC signature verification (
RESEND_WEBHOOK_SECRET) - Rate Limit:
webhooks
Response:
{ "success": true, "processed": "email.delivered" }Error Codes:
400 Invalid payload, 401 Signature verification failed, 500 Server errorCron Jobs
Automated maintenance tasks triggered by Vercel Cron. Both endpoints require a
CRON_SECRET bearer token in production.Each cron endpoint also supports a GET request that returns its configuration status (active/inactive, schedule, model) without requiring authentication. Useful for monitoring dashboards.
Check Trial Expirations
POSTGET
/api/cron/check-trialsExpires trial subscriptions that have reached their end date. Runs daily at 2:00 AM UTC.
- Auth: Bearer token (
CRON_SECRET) in production - Schedule:
0 2 * * *(daily) - Pricing Model: Classic SaaS only (no-op for credit-based)
Response (200):
json
{
"success": true,
"message": "Trial check completed",
"expiredCount": 3,
"timestamp": "2026-02-14T02:00:00.000Z"
}
Expire Bonus Credits
POSTGET
/api/cron/expire-bonus-creditsExpires bonus credit purchases that have passed their expiry date. Runs daily at 3:00 AM UTC.
- Auth: Bearer token (
CRON_SECRET) in production - Schedule:
0 3 * * *(daily) - Pricing Model: Credit-based only
Response (200):
json
{
"success": true,
"message": "Bonus credit expiry check completed",
"expiredCount": 1,
"timestamp": "2026-02-14T03:00:00.000Z"
}
Health
Health Check
GET
/api/healthReturns application health status including database connectivity and memory usage.
- Auth: None
- Rate Limit: None
- Cache: None (
no-cache, no-store, must-revalidate)
Response (200 healthy / 503 unhealthy):
json
{
"status": "healthy",
"timestamp": "2026-02-14T10:00:00.000Z",
"version": "1.0.0",
"environment": "production",
"checks": {
"database": "connected",
"memory": "ok"
},
"responseTime": "12ms"
}
Use this endpoint for uptime monitoring services and load balancer health checks.
Error Codes
All endpoints follow a consistent error response format:
json
{
"error": "Human-readable error message"
}
| Code | Meaning | Common Cause |
|---|---|---|
400 | Bad Request | Validation failed — check request body against the Zod schema |
401 | Unauthorized | Missing or invalid Clerk session / webhook signature |
402 | Payment Required | Insufficient AI credits for the requested operation |
403 | Forbidden | Development-only endpoint accessed in production |
404 | Not Found | Resource doesn't exist or feature is disabled via feature flag |
405 | Method Not Allowed | Wrong HTTP method for the endpoint |
409 | Conflict | Duplicate resource (e.g., re-uploading same file) |
429 | Too Many Requests | Rate limit exceeded — check X-RateLimit-* headers for reset time |
500 | Server Error | Internal error — check server logs |
503 | Service Unavailable | Database or health check failed |
When rate limiting is active, responses include three headers:
X-RateLimit-Limit (max requests), X-RateLimit-Remaining (remaining in window), and X-RateLimit-Reset (Unix timestamp when the window resets).