Frontend-Backend contract for LiteVault Web V1 Generated from frontend implementation analysis
This document defines the REST API contract between the LiteVault frontend and backend. All endpoints are designed to support the current UI behaviors implemented in the frontend.
- V1 MVP endpoints only
- Supports: Items, Library, Search, Tags, Auth
- Single-user context (multi-tenancy out of scope)
Development: http://localhost:8080/api/v1
Production: https://api.litevault.app/api/v1
Authentication is handled by Clerk. Protected endpoints require a valid Clerk session token.
Request Header:
Authorization: Bearer <Clerk session JWT>
Token Validation:
- Backend verifies JWT signature using Clerk JWKS
- Validates:
exp,nbf,iss,audclaims - Extracts
subclaim asclerk_user_id - User record created/updated just-in-time on first request
Supported Auth Methods (via Clerk):
- Email/password
- OAuth: Google, GitHub
For local development without Clerk, use header-based auth:
X-Dev-User-Id: user-123
Precedence: If Authorization: Bearer header is present and valid, Clerk auth is used. Otherwise, X-Dev-User-Id is checked (only if AUTH_MODE=mixed|dev).
⚠️ DEPRECATED: The following endpoints from initial planning are NOT implemented. Use Clerk's hosted auth UI instead.
POST /auth/signup— Use Clerk SignUp componentPOST /auth/login— Use Clerk SignIn componentPOST /auth/logout— Use Clerk signOut()
| Endpoint | Status | Notes |
|---|---|---|
GET /auth/me |
✅ | Get current authenticated user with profile + preferences |
PATCH /auth/me/profile |
✅ | Update profile fields (nickname, avatarUrl, bio) |
PATCH /auth/me/preferences |
✅ | Update preferences (language, timezone, AI toggle) |
Returns the currently authenticated user's profile with preferences.
Response 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"clerkUserId": "user_2abc123...",
"email": "user@example.com",
"displayName": "John Doe",
"nickname": "Johnny",
"avatarUrl": "https://...",
"bio": "A knowledge enthusiast",
"preferences": {
"defaultLanguage": "en",
"timezone": "America/New_York",
"aiSuggestionsEnabled": true
},
"plan": "free",
"createdAt": "2025-12-27T13:00:00.000Z",
"updatedAt": "2025-12-28T10:00:00.000Z"
}Error Cases
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
No valid auth token |
Update app-owned profile fields (nickname, avatarUrl, bio).
Request Body
{
"nickname": "Johnny",
"avatarUrl": "https://example.com/avatar.png",
"bio": "A knowledge enthusiast"
}All fields are optional. Send only fields to update.
Validation Rules
| Field | Constraint |
|---|---|
nickname |
1-40 characters, trimmed |
avatarUrl |
Must be http:// or https:// URL |
bio |
Max 200 characters |
Response 200 OK — Same as GET /auth/me
Error Cases
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
No valid auth token |
| 422 | VALIDATION_ERROR |
Invalid field values |
Update user preferences.
Request Body
{
"defaultLanguage": "zh",
"timezone": "Asia/Shanghai",
"aiSuggestionsEnabled": false
}All fields are optional. Send only fields to update.
Validation Rules
| Field | Constraint |
|---|---|
defaultLanguage |
en or zh |
timezone |
IANA timezone string |
aiSuggestionsEnabled |
Boolean |
Response 200 OK — Same as GET /auth/me
Error Cases
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
No valid auth token |
| 422 | VALIDATION_ERROR |
Invalid field values |
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET |
/auth/me |
Get current user with profile + preferences | ✓ |
PATCH |
/auth/me/profile |
Update profile (nickname, avatarUrl, bio) | ✓ |
PATCH |
/auth/me/preferences |
Update preferences (language, timezone, AI) | ✓ |
POST |
/items |
Create new item (triggers enrichment) | ✓ |
GET |
/items/pending |
List pending items | ✓ |
GET |
/items/:id |
Get single item | ✓ |
PATCH |
/items/:id |
Update item (edit/confirm/discard) | ✓ |
POST |
/items/:id/retry |
Retry failed enrichment | ✓ |
GET |
/items/:id/attachments |
List attachments for item | ✓ |
GET |
/library |
List archived items (timeline) | ✓ |
GET |
/search |
Search library (lexical, V1) | ✓ |
GET |
/tags |
List all tags | ✓ |
POST |
/tags |
Create new tag | ✓ |
PATCH |
/tags/:id |
Rename tag | ✓ |
DELETE |
/tags/:id |
Delete tag | ✓ |
POST |
/tags/merge |
Merge tags | ✓ |
POST |
/uploads/initiate |
Get presigned PUT URL for upload | ✓ |
POST |
/uploads/complete |
Complete upload and create attachment | ✓ |
GET |
/uploads/:id |
Get upload status | ✓ |
DELETE |
/uploads/:id |
Delete upload | ✓ |
GET |
/attachments/:id/download_url |
Get presigned download URL | ✓ |
DELETE |
/attachments/:id |
Delete attachment | ✓ |
- Format: UUID v4 (e.g.,
550e8400-e29b-41d4-a716-446655440000) - Generated server-side
- Format: ISO-8601 UTC (e.g.,
2025-12-27T13:00:00.000Z) - Fields:
createdAt,updatedAt,confirmedAt
- Default: descending by
createdAt - Library: descending by
confirmedAt
- Trim whitespace
- Case-preserving storage, case-insensitive matching
- Max length: 50 characters
- Allowed characters: alphanumeric, spaces, hyphens, underscores
{
"cursor": "string | null",
"limit": 20,
"hasMore": true
}- Cursor-based pagination for Library and Tags
- Default limit: 20, max: 100
Creates a new item. Optionally triggers async AI enrichment.
Idempotency: Use Idempotency-Key header to prevent duplicates.
Request
{
"rawText": "string (required, max 10000 chars)",
"enrich": true,
"tagIds": ["uuid", "uuid"]
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
rawText |
string | Yes | - | Captured text content |
enrich |
boolean | No | true |
If true, triggers AI enrichment (ENRICHING). If false, saves directly to ARCHIVED. |
tagIds |
string[] | No | [] |
Tag UUIDs to associate with item. Validated: must belong to user and not be deleted. |
Headers
Idempotency-Key: <uuid> (recommended)
Response 201 Created
When enrich=true (default):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"rawText": "Meeting notes from the product review...",
"title": null,
"summary": null,
"tags": [],
"status": "ENRICHING",
"sourceType": null,
"enrichmentMode": "AI",
"createdAt": "2025-12-27T13:00:00.000Z",
"updatedAt": "2025-12-27T13:00:00.000Z",
"confirmedAt": null
}When enrich=false (Direct Save to Library):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"rawText": "Meeting notes from the product review...",
"title": "Meeting notes from the product review",
"summary": null,
"tags": [{"id": "tag-uuid", "name": "Work", "color": "#3B82F6"}],
"status": "ARCHIVED",
"sourceType": "NOTE",
"enrichmentMode": "MANUAL",
"createdAt": "2025-12-27T13:00:00.000Z",
"updatedAt": "2025-12-27T13:00:00.000Z",
"confirmedAt": "2025-12-27T13:00:00.000Z"
}Note: When
enrich=false:
- Item is created directly as ARCHIVED (no pending review step)
- Title is auto-generated from the first 60 characters of the first non-empty line
confirmedAtis set to creation time- Any provided
tagIdsare validated and associated
Error Cases
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR |
rawText empty or too long, or invalid tagIds |
| 401 | UNAUTHORIZED |
Missing/invalid auth |
| 409 | DUPLICATE_REQUEST |
Idempotency key already used |
Returns items with status ENRICHING, READY_TO_CONFIRM, or FAILED.
Response 200 OK
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"rawText": "Meeting notes...",
"title": "Product Review Meeting Notes",
"summary": "Product review discussing Q1 roadmap...",
"tags": ["Meetings", "Product"],
"status": "READY_TO_CONFIRM",
"sourceType": "NOTE",
"createdAt": "2025-12-27T13:00:00.000Z",
"updatedAt": "2025-12-27T13:02:00.000Z",
"confirmedAt": null
}
],
"total": 1
}Response 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"rawText": "Meeting notes...",
"title": "Product Review Meeting Notes",
"summary": "Product review discussing Q1 roadmap...",
"tags": [{"id": "tag-1", "name": "Meetings", "color": "#6B7280"}],
"suggestedTags": [
{"id": "sug-1", "name": "Q1 Planning", "status": "PENDING", "confidence": 0.92},
{"id": "sug-2", "name": "Product", "status": "PENDING", "confidence": 0.87}
],
"status": "READY_TO_CONFIRM",
"sourceType": "NOTE",
"createdAt": "2025-12-27T13:00:00.000Z",
"updatedAt": "2025-12-27T13:05:00.000Z",
"confirmedAt": null,
"attachmentCount": 2,
"attachments": [
{
"id": "att-uuid-1",
"uploadId": "upload-uuid-1",
"displayName": "screenshot.png",
"mimeType": "image/png",
"sizeBytes": 524288,
"kind": "image",
"createdAt": "2025-12-27T13:03:00.000Z"
}
]
}Note:
suggestedTagscontains AI-generated tag suggestions stored initem_tag_suggestionstable. For ARCHIVED items,suggestedTagsshows historical accepted/rejected decisions. For READY_TO_CONFIRM items, it shows PENDING suggestions requiring user review.
Error Cases
| Status | Code | Description |
|---|---|---|
| 404 | NOT_FOUND |
Item does not exist |
Used for: editing fields, confirming, or discarding.
Request — Confirm (from READY_TO_CONFIRM)
{
"action": "confirm",
"acceptedSuggestionIds": ["sug-1"],
"rejectedSuggestionIds": ["sug-2"],
"addedTagIds": ["existing-tag-uuid"]
}| Field | Type | Required | Description |
|---|---|---|---|
acceptedSuggestionIds |
string[] | No | Suggestion IDs to accept (creates/revives tags) |
rejectedSuggestionIds |
string[] | No | Suggestion IDs to reject |
addedTagIds |
string[] | No | Existing tag IDs to associate with item |
title |
string | No | Override AI-generated title |
summary |
string | No | Override AI-generated summary |
originalText |
string | No | Edit original text payload |
Request — Discard (from READY_TO_CONFIRM, FAILED, or ARCHIVED)
{
"action": "discard"
}Note: Discard from ARCHIVED status is used for "Library Discard" — removing an item from the library. The item transitions to DISCARDED status and is hidden from all queries.
Request — Edit (ARCHIVED items only)
{
"title": "Updated Title",
"summary": "Updated summary",
"originalText": "Updated original text content...",
"addedTagIds": ["tag-uuid"],
"removedTagIds": ["tag-uuid-2"]
}Note: Editing
originalTextdoes NOT trigger AI regeneration. Title, summary, and tags remain unchanged unless explicitly modified.
Response 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "ARCHIVED",
"confirmedAt": "2025-12-27T13:05:00.000Z",
"updatedAt": "2025-12-27T13:05:00.000Z"
}Error Cases
| Status | Code | Description |
|---|---|---|
| 409 | INVALID_STATE_TRANSITION |
Cannot confirm/discard item in ENRICHING state |
| 404 | NOT_FOUND |
Item does not exist |
Retries AI enrichment for a FAILED item.
Request: Empty body
Response 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "ENRICHING",
"updatedAt": "2025-12-27T13:06:00.000Z"
}Error Cases
| Status | Code | Description |
|---|---|---|
| 400 | INVALID_STATE |
Item is not in FAILED state |
Returns items with status ARCHIVED, sorted by confirmedAt descending.
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
cursor |
string | null | Pagination cursor |
limit |
number | 20 | Items per page (max 100) |
q |
string | null | Search filter (title/rawText) |
tag |
string | null | Filter by tag name |
Response 200 OK
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"rawText": "Meeting notes...",
"title": "Product Review Meeting Notes",
"summary": "Product review discussing Q1 roadmap...",
"tags": [{"id": "tag-1", "name": "Meetings", "color": "#6B7280"}],
"status": "ARCHIVED",
"sourceType": "NOTE",
"createdAt": "2025-12-27T13:00:00.000Z",
"confirmedAt": "2025-12-27T13:05:00.000Z",
"attachmentCount": 2
}
],
"pagination": {
"cursor": "eyJjb25maXJtZWRBdCI6IjIwMjUtMTItMjdUMTM6MDU6MDAuMDAwWiJ9",
"hasMore": true
}
}Searches archived items using lexical matching. Supports two modes:
- Tag-only mode: Query starts with
#→ matches tag names only - Combined mode: Otherwise → matches title/summary/rawText OR tags
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
q |
string | required | Search query (non-empty) |
cursor |
string | null | Pagination cursor |
limit |
number | 20 | Items per page (max 100) |
Query Parsing Rules
| Query | Mode | Behavior |
|---|---|---|
#work |
tag_only | Match items with tags containing "work" |
meeting notes |
combined | Match items where text OR tags contain "meeting notes" |
Response 200 OK
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Product Review Meeting Notes",
"summary": "Product review discussing Q1 roadmap...",
"tags": [{"id": "tag-1", "name": "Meetings", "color": "#6B7280"}],
"sourceType": "NOTE",
"createdAt": "2025-12-27T13:00:00.000Z",
"confirmedAt": "2025-12-27T13:05:00.000Z",
"attachmentCount": 2
}
],
"mode": "combined",
"pagination": {
"cursor": "eyJjb25maXJtZWRBdCI6IjIwMjUtMTItMjdUMTM6MDU6MDAuMDAwWiJ9",
"hasMore": true
},
"total": 42
}Ordering: Results ordered by confirmed_at DESC, id DESC (most recent first).
Error Cases
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR |
Query empty after trimming |
| 400 | INVALID_CURSOR |
Malformed pagination cursor |
| 401 | UNAUTHORIZED |
Missing/invalid auth |
⚠️ NOT IMPLEMENTED: Reserved for future semantic search with LLM answer synthesis.
Future endpoint for AI-powered search with synthesized answers and evidence.
Request (V2 Future)
{
"query": "How do I organize my design references?",
"mode": "semantic"
}Response (V2 Future)
{
"answer": "Based on your notes, here are the key insights...",
"evidence": [
{
"itemId": "550e8400-e29b-41d4-a716-446655440000",
"title": "Dashboard Design References",
"snippet": "Design references for the new dashboard...",
"score": 0.95,
"type": "NOTE",
"tags": ["Design", "Research"]
}
],
"totalSources": 3
}Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
q |
string | null | Search by name |
sort |
string | name |
Sort by: name, usage, lastUsed |
unused |
boolean | false | Show only unused tags |
cursor |
string | null | Pagination cursor |
limit |
number | 50 | Items per page |
Response 200 OK
{
"tags": [
{
"id": "tag-uuid-1",
"name": "Meetings",
"usageCount": 12,
"lastUsed": "2025-12-27T13:00:00.000Z",
"createdAt": "2025-12-01T00:00:00.000Z",
"color": "#6B7280"
}
],
"total": 8
}Creates a new tag or returns existing tag if name already exists (case-insensitive match).
Revive Behavior: If a soft-deleted tag with the same name exists, it is "revived":
deleted_atis set to NULL- The same tag ID is returned
- Optionally updates
colorif provided
Request
{
"name": "Design",
"color": "#3B82F6"
}Response 201 Created (new tag or revived)
{
"id": "tag-uuid-new",
"name": "Design",
"usageCount": 0,
"lastUsed": null,
"createdAt": "2025-12-27T13:10:00.000Z",
"color": "#3B82F6"
}Response 200 OK (existing active tag returned)
{
"id": "tag-uuid-existing",
"name": "Design",
"usageCount": 5,
"lastUsed": "2025-12-26T10:00:00.000Z",
"createdAt": "2025-12-01T00:00:00.000Z",
"color": "#6B7280"
}Note: Tag matching is case-insensitive. Posting "design" when "Design" exists returns the existing tag (or revives a deleted "Design").
Error Cases
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR |
Name empty or too long |
Updates a tag's name and/or color. Both fields are optional.
Request
{
"name": "Meetings & Notes",
"color": "#3B82F6"
}Response 200 OK
{
"id": "tag-uuid-1",
"name": "Meetings & Notes",
"usageCount": 12,
"lastUsed": "2025-12-27T13:00:00.000Z",
"createdAt": "2025-12-01T00:00:00.000Z",
"color": "#3B82F6"
}Error Cases
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR |
Invalid color format (must be #RRGGBB) |
| 404 | NOT_FOUND |
Tag does not exist |
| 409 | TAG_EXISTS |
New name conflicts with existing tag |
Soft-deletes a tag by setting deleted_at=now(). The tag is hidden from all queries but preserved in the database. Items that referenced the tag will no longer display it.
Behavior:
- Sets
deleted_at=now()(does NOT physically remove the row) - Deleted tags are excluded from GET /tags and all item displays
- Idempotent: Deleting an already-deleted tag returns success
Response 204 No Content
Error Cases
| Status | Code | Description |
|---|---|---|
| 404 | TAG_NOT_FOUND |
Tag does not exist (never existed) |
Note: Re-creating a tag with the same name after deletion will "revive" the original tag, returning the same ID. See POST /tags.
⚠️ NOT IMPLEMENTED: This endpoint is planned but not yet implemented.
Merges source tags into target tag.
Request
{
"sourceTagIds": ["tag-uuid-2", "tag-uuid-3"],
"targetTagId": "tag-uuid-1"
}Response 200 OK
{
"targetTag": {
"id": "tag-uuid-1",
"name": "Meetings",
"usageCount": 26,
"lastUsed": "2025-12-27T13:00:00.000Z"
},
"mergedCount": 2
}File uploads use a presigned URL workflow for direct client-to-storage uploads:
- Client calls
POST /uploads/initiatewith file metadata - Backend returns presigned PUT URL for object storage
- Client uploads file directly to storage using presigned URL
- Client calls
POST /uploads/completeto finalize - Backend verifies upload and creates attachment record
Returns a presigned PUT URL for direct upload to object storage (MinIO/S3).
Request
{
"filename": "document.pdf",
"mimeType": "application/pdf",
"sizeBytes": 1048576,
"kind": "file",
"itemId": "550e8400-e29b-41d4-a716-446655440000",
"idempotencyKey": "abc123"
}| Field | Type | Required | Description |
|---|---|---|---|
filename |
string | Yes | Original filename (max 255 chars) |
mimeType |
string | Yes | MIME type of file |
sizeBytes |
number | Yes | File size in bytes |
kind |
string | Yes | image or file |
itemId |
string | No | Item to attach to (can be set on complete) |
idempotencyKey |
string | No | Unique key to prevent duplicates |
Response 201 Created
{
"uploadId": "upload-uuid",
"objectKey": "user-id/upload-id/document.pdf",
"presignedPutUrl": "https://minio.example.com/bucket/...",
"headersToInclude": {
"Content-Type": "application/pdf",
"Content-Length": "1048576"
},
"expiresAt": "2025-12-27T14:00:00.000Z",
"status": "INITIATED"
}Error Cases
| Status | Code | Description |
|---|---|---|
| 413 | FILE_TOO_LARGE |
File exceeds size limit |
| 415 | INVALID_FILE_TYPE |
MIME type not allowed |
| 409 | DUPLICATE_REQUEST |
Idempotency key already used |
Verifies the file exists in storage and creates an attachment record.
Request
{
"uploadId": "upload-uuid",
"itemId": "550e8400-e29b-41d4-a716-446655440000",
"etag": "\"abc123\""
}| Field | Type | Required | Description |
|---|---|---|---|
uploadId |
string | Yes | Upload ID from initiate |
itemId |
string | Yes | Item to attach to |
etag |
string | No | ETag from S3 PUT response |
Response 200 OK
{
"upload": {
"id": "upload-uuid",
"objectKey": "user-id/upload-id/document.pdf",
"filename": "document.pdf",
"mimeType": "application/pdf",
"sizeBytes": 1048576,
"kind": "file",
"status": "COMPLETED",
"createdAt": "2025-12-27T13:00:00.000Z",
"completedAt": "2025-12-27T13:01:00.000Z"
},
"attachment": {
"id": "attachment-uuid",
"uploadId": "upload-uuid",
"itemId": "item-uuid",
"displayName": "document.pdf",
"kind": "file",
"createdAt": "2025-12-27T13:01:00.000Z"
}
}Error Cases
| Status | Code | Description |
|---|---|---|
| 404 | UPLOAD_NOT_FOUND |
Upload does not exist |
| 410 | UPLOAD_EXPIRED |
Presigned URL has expired |
| 409 | INVALID_UPLOAD_STATE |
Upload not in INITIATED state |
| 400 | UPLOAD_VERIFICATION_FAILED |
File not found in storage |
Response 200 OK
{
"id": "upload-uuid",
"objectKey": "user-id/upload-id/document.pdf",
"filename": "document.pdf",
"mimeType": "application/pdf",
"sizeBytes": 1048576,
"kind": "file",
"status": "COMPLETED",
"createdAt": "2025-12-27T13:00:00.000Z",
"completedAt": "2025-12-27T13:01:00.000Z",
"expiresAt": "2025-12-27T14:00:00.000Z"
}Soft-deletes an upload record.
Response 204 No Content
Returns all attachments for an item.
Response 200 OK
{
"attachments": [
{
"id": "attachment-uuid",
"uploadId": "upload-uuid",
"displayName": "document.pdf",
"mimeType": "application/pdf",
"sizeBytes": 1048576,
"kind": "file",
"createdAt": "2025-12-27T13:01:00.000Z"
},
{
"id": "attachment-uuid-2",
"uploadId": "upload-uuid-2",
"displayName": "screenshot.png",
"mimeType": "image/png",
"sizeBytes": 524288,
"kind": "image",
"createdAt": "2025-12-27T13:02:00.000Z"
}
],
"total": 2
}Returns a presigned download URL for an attachment.
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
preview |
boolean | false | If true, returns inline URL for in-browser viewing (PDF preview) |
Response 200 OK
{
"downloadUrl": "https://minio.example.com/bucket/...",
"expiresAt": "2025-12-27T14:00:00.000Z",
"filename": "document.pdf",
"mimeType": "application/pdf",
"sizeBytes": 1048576
}Note: When
preview=true, the URL returnsContent-Disposition: inlinefor in-browser viewing. Whenpreview=false(default), returnsContent-Disposition: attachmentfor download.
Soft-deletes an attachment record.
Response 204 No Content
| Value | Description |
|---|---|
ENRICHING |
AI enrichment in progress |
READY_TO_CONFIRM |
Enrichment complete, awaiting user confirmation |
ARCHIVED |
Confirmed and saved to library |
DISCARDED |
User discarded the item |
FAILED |
Enrichment failed, can retry |
| Value | Description |
|---|---|
NOTE |
User-captured note |
ARTICLE |
Extracted article content |
When an item is created:
- Backend returns immediately with
status: ENRICHING - Async job processes enrichment (title, summary, tags, sourceType)
- On success: status →
READY_TO_CONFIRM - On failure: status →
FAILED
Frontend polls GET /items/pending every 3 seconds while items are in ENRICHING state.
Backend should emit events for real-time updates:
item.enrichment.completeitem.enrichment.failed
| Endpoint | Limit | Window |
|---|---|---|
POST /items |
10 | 1 minute |
POST /search |
20 | 1 minute |
* (default) |
100 | 1 minute |
Response Headers:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
X-RateLimit-Reset: 1703682400 (Unix timestamp)
Limits on successful AI enrichment operations per user per UTC day.
| Plan | Daily Quota | Concurrency Limit |
|---|---|---|
| Free | 2 | 1 |
| Pro | 10 | 3 |
Response Headers (on AI-related endpoints):
X-AI-Quota-Limit: 50
X-AI-Quota-Remaining: 49
X-AI-Quota-Reset: 1703721600 (Seconds until UTC midnight)
Error Response (Quota Exceeded):
{
"error": {
"code": "DAILY_QUOTA_EXCEEDED",
"message": "You have reached your daily AI enrichment limit (50/day).",
"requestId": "req-...",
"details": {
"limit": 50,
"resetAt": "2025-12-31T00:00:00Z"
}
}
}Error Response (Concurrency Limit):
{
"error": {
"code": "CONCURRENCY_LIMIT_EXCEEDED",
"message": "Too many items are being processed simultaneously.",
"requestId": "req-...",
"details": {
"activeJobs": 3,
"limit": 3
}
}
}