Authentication
JWT (RS256) + httpOnly refresh cookie. Access token lifetime: 15 min. Refresh token: 7 days with family-based rotation and theft detection.
Creates a new admin account and triggers an OTP email verification.
{ "name": "Aarav Sharma", "username": "aarav", "email": "aarav@kraviona.com", "phone": "9876543210", "password": "securePass#99" }
200{ "success": true, "data": { "userId": "u-001", "email": "aarav@kraviona.com" } }
POST /auth/verify-account to complete signup.Verifies the OTP sent during signup. Returns access token and user profile on success.
{ "email": "aarav@kraviona.com", "otp": "847291" }
200{ "success": true, "data": { "accessToken": "eyJhbGci...", "user": { "id": "u-001", "role": "editor" } } }
Resends a one-time password. Rate-limited to 1 request per 30 s, 5 per hour per email.
{ "email": "aarav@kraviona.com", "purpose": "signup" // or "login" }
200{ "success": true, "data": { "ok": true } }
Authenticates via email + password (Argon2id). Sets httpOnly kra_access (15 min) and kra_refresh (7 d) cookies. Rate-limited 5/min/IP, 10/min/email.
{ "email": "admin@kraviona.com", "password": "securePass#99" }
200{ "success": true, "data": {
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5...",
"user": { "id": "u-001", "name": "Aarav", "email": "...", "role": "admin", "permissions": ["*"] }
} }
kra_access (HttpOnly, SameSite=Strict, Path=/, Max-Age=900) · kra_refresh (HttpOnly, SameSite=Strict, Path=/auth, Max-Age=604800) · XSRF-TOKEN (non-HttpOnly, Max-Age=900)Two-step OTP login. Call with just email to trigger OTP, then again with email + otp to verify. 5 wrong attempts → 15 min lockout.
{ "email": "admin@kraviona.com" }
{ "email": "admin@kraviona.com", "otp": "123456" }
200{ "success": true, "data": { "accessToken": "eyJhbGci...", "user": { ... } } }
Rotates the refresh token. Requires a valid kra_refresh cookie (no request body). Revokes the old token and issues a fresh pair. Presenting a previously-revoked token kills the entire token family and forces re-login.
200{ "success": true, "data": { "accessToken": "eyJhbGci..." } }
Revokes the entire refresh token family and clears cookies. No request body needed.
200{ "success": true, "data": { "ok": true } }
Returns the current authenticated user's profile and resolved permissions list.
200{ "success": true, "data": {
"id": "u-001", "name": "Aarav Sharma",
"email": "aarav@kraviona.com", "role": "admin",
"permissions": ["post.create", "post.publish", "..."]
} }
Allows an authenticated user to update their own profile (name, phone, avatar, locale, timezone). Self-edit only — cannot change role or permissions.
{ "name": "Aarav S.", "phone": "9876543210", "locale": "en-IN", "timezone": "Asia/Kolkata" }
Paginated list of all admin users. Filter by role, status, or free-text search.
Assigns a new role to a user. Cannot elevate a user above the requester's own permission set.
{ "userId": "u-042", "roleSlug": "editor" }
Blocks or unblocks a user. Blocked users receive 403 account_blocked on all requests until unblocked.
{ "isBlocked": true }
Soft-deletes a user (sets status=inactive). The user record and audit history are preserved.
Site Config
Singleton documents keyed by key. Covers Hero, Nav, Footer, About, Analytics, SEO defaults, and all page-level configs. Public reads are cached 5 min.
Returns the merged site-config blob containing all singleton sections. Cached at the CDN for 5 minutes. Used by the public site to bootstrap.
PUT /v1/site-config is called.Full replace or deep-merge of the site config. Accepts Partial<SiteConfig.data>. Busts the public cache on success.
{ "hero": { "headline": "Build. Ship. Grow.", "subtext": "..." } }
Fetch a single singleton section by key. Available keys:
Updates a single config section by key. Body: { "data": { ... } }. Writes an audit log entry with changed field paths.
{ "data": { "ctaLabel": "Get Started", "ctaUrl": "/contact" } }
Services
Core service offerings. Each service can have per-service landing page config embedded in pageConfig.
Fetch all active services for the public-facing site, sorted by display order.
Returns the navbar subset only (_id, name, slug, order). Cached for faster nav renders.
Returns a single service with its merged pageConfig (used by per-service landing pages).
Admin list of all services including inactive/draft ones. Supports standard pagination and filtering.
Creates a new service. Slug is auto-generated from name if not provided.
{ "name": "MERN Stack Development", "slug": "mern-stack", "shortDesc": "...", "isActive": true, "featured": false }
Full update of a service. Also accepts the full pageConfig blob for per-service landing page management.
Soft-deletes (sets isActive=false). The service remains in the DB and can be restored.
Reorders services by providing a full ordered array of IDs. Sets the order field on each document atomically.
{ "order": ["id_1", "id_2", "id_3"] }
Portfolio, Gallery & Case Studies
Content showcasing past work. Portfolio items, curated gallery collections, and long-form case studies all follow the same CRUD pattern.
Creates a new portfolio item.
Updates a portfolio item.
Soft-deletes a portfolio item.
Lists all published case studies.
Creates a new case study.
Updates an existing case study.
Deletes a case study.
Lists all published gallery collections.
Creates a new gallery collection.
Team & Testimonials
Team member profiles and client testimonials. Testimonials require an approve step before appearing publicly.
Returns all active team members in display order.
Creates a new team member.
Reorders team members by providing a full ordered array of IDs.
{ "order": ["id_1", "id_2", "id_3"] }
Approves a testimonial so it appears on the public site. Transition: pending → approved.
Pricing
Pricing plan management. All money values are stored as integer minor units (paise). Currency is on the pricing.currency field.
Creates a new pricing plan. Price values in minor units (paise).
Updates a pricing plan.
Deletes a pricing plan.
Blog — Posts, Categories, Comments
Post body is TipTap JSON (bodyJson) plus the rendered HTML (body). Legacy paths (/posts, /post/*) are kept at root for SEO compatibility.
Paginated public post list. Only returns status=published entries unless an authenticated admin request.
Fetches a single post by slug. Increments the post's views counter on each unique visit (tracked via visitor cookie).
Adds an emoji reaction to a post. One reaction per visitor per post (tracked via kra_vid cookie).
{ "type": "👍" // or ❤ 🔥 💡 }
Creates a new blog post. Requires both TipTap rendered HTML (body) and the raw JSON (bodyJson). Post starts as draft unless status="published" is set and the user has post.publish permission.
{ "title": "MERN Stack Best Practices", "slug": "mern-stack-best-practices",
"body": "<p>...</p>", "bodyJson": { ... },
"category": "cat-id", "status": "draft",
"seo": { "metaTitle": "...", "metaDescription": "..." } }
Updates an existing post. Changing status to published additionally requires post.publish.
Soft-deletes a post by slug.
Updates the focus keywords for a post's SEO scoring. Max 8 keywords.
{ "focusKeywords": ["mern", "node.js", "react"] }
Admin list that includes drafts, scheduled, and archived posts. Supports all standard filtering and pagination params.
Returns all active categories (used in the blog sidebar/filters).
Admin list including inactive categories.
Creates a new post category.
Creates a public comment. Returns status="pending" — requires admin approval before appearing publicly.
{ "author": "John Doe", "email": "john@example.com", "body": "Great article!" }
Approves, rejects, or marks a comment as spam.
{ "status": "approved" // or "rejected" | "spam" }
Media
File upload to S3, media library management. Uploads are streamed (never buffered). ClamAV scan runs async in a worker. Max: 50 MB (images), 200 MB (video).
Uploads a file to S3 via multipart/form-data. Server streams directly to S3. Returns the new media document with a scan.status="pending"; the ClamAV worker updates the row asynchronously.
201{ "success": true, "data": {
"_id": "f-001", "url": "https://cdn.kraviona.com/...",
"variants": [{ "name": "thumb", "url": "...", "width": 320 }],
"scan": { "status": "pending" }
} }
Content-Type header. Virus-positive files are deleted and return 502 { code: "upload_failed", meta: { reason: "virus_detected" } }.Updates metadata for a file. Does not replace the file binary.
{ "alt": "Hero illustration", "caption": "Q1 2026", "folder": "Marketing", "starred": true }
Removes the file from S3, deletes all variants, and removes the DB record. This action is irreversible.
Re-triggers the transcode worker for an existing file (useful if a variant failed). The worker runs async and emits progress via Socket.IO.
Messages & Leads
Current implemented contact and lead capture endpoints. Public create routes do not require login and do not return saved private data. Read, update, and delete routes require an authenticated admin or super_admin.
Creates a normal website contact message. Use for the contact page form. No auth required. The public response intentionally does not echo submitted user data.
{ "firstName": "User", "lastName": "Name",
"email": "user@example.com", "phone": "+919608553167",
"subject": "Project inquiry",
"message": "I want to discuss a MERN stack project." }
201{ "message": "Message created successfully", "success": true }
Lists stored contact messages. Requires logged-in admin or super admin.
Fetches one contact message by Mongo id.
Partially updates a contact message. Common use: change status.
{ "status": "read" }
Deletes a contact message. Requires logged-in admin or super admin.
Creates a sales lead. Use this for service popup forms and service page lead capture. No auth required. The public response intentionally does not echo submitted user data.
{ "name": "User full name",
"email": "user@example.com",
"phone": "+919608553167",
"subject": "Lead Inquiry: MERN Stack Development",
"message": "Service: MERN Stack Development\nBudget: Need guidance\nCompany: Company Name\n\nUser requirement\n\nLead source: current page URL",
"leadType": "service-popup",
"page": "/services/mern-stack-development",
"service": "MERN Stack Development",
"budget": "Need guidance",
"company": "Company Name" }
201{ "message": "Lead created successfully", "success": true }
Lists leads for the admin dashboard.
Fetches one lead by Mongo id.
Partially updates a lead. Common admin use: move pipeline status, add notes, score the lead, or update deal value.
{ "status": "Contacted",
"score": 70,
"notes": "Called once. Asked for proposal.",
"dealValue": 50000 }
Deletes a lead. Requires logged-in admin or super admin.
Subscribers & Campaigns
Email newsletter subscriptions with double opt-in, and campaign management with scheduling and send stats.
Creates a new newsletter subscription with double opt-in. A confirmation email is sent automatically. Rate-limited.
{ "email": "reader@example.com", "name": "Jane", "source": "blog-footer" }
200{ "success": true, "data": { "confirmTokenSent": true } }
Confirms a subscription via token sent in the confirmation email. Transitions pending → confirmed.
Creates a new email campaign in draft status.
Enqueues the email.sendCampaign BullMQ job. Requires an Idempotency-Key header. Can be scheduled for the future.
{ "scheduledFor": "2026-07-01T10:00:00Z" }
Returns delivery, open, and click counters for a sent campaign.
Notifications
In-app admin notifications. REST for initial load + count; live updates via Socket.IO /notifications namespace.
Returns paginated notifications with the total unread count for the badge.
200{ "success": true, "data": { "items": [...], "totalUnread": 5, "total": 42 } }
Marks a single notification as read.
Marks all of the current user's notifications as read at once.
Analytics
Lightweight first-party analytics. Public tracking pixel + admin read endpoints. No third-party dependency.
Client-side tracking pixel. Sets a kra_vid cookie (1 year) as a visitor ID if absent. Rate-limited.
{ "kind": "pageview", "path": "/blog/mern-stack",
"referrer": "https://google.com",
"utm": { "source": "twitter", "campaign": "q2" } }
200{ "success": true, "data": { "series": [{ "ts": "2026-06-01T00:00:00Z", "count": 342 }] } }
Returns the KPI summary used on the /dashboard home page.
200{ "total": 18421, "today": 312, "changePct": +12.3, "byDay": [...] }
Audit Logs
Immutable, HMAC-chained audit trail. Every state-changing admin action is recorded. Read-only at the API level — no write or delete endpoints exist, even for super-admins.
Paginated audit log with filtering. Prefix match on action (e.g. post.* returns all post actions). Each row includes hmac and prevHash for chain verification.
{ "action": "post.publish", "actor": { "id": "u-001", "name": "Aarav" },
"targetKind": "Post", "target": "post-id",
"meta": { "fromStatus": "draft", "toStatus": "published" },
"hmac": "sha256:...", "prevHash": "sha256:...", "createdAt": "..." }
insert, find, count on the auditlogs collection.Fetch a single audit log row with full HMAC chain metadata.
Settings
Grouped key/value settings. Public slice exposes analytics-enabled flag and social links. Admin read/write accesses all groups.
Returns the public-safe settings slice: analytics enabled flag, social links, and other front-end config.
Returns all settings groups in one response.
Partial merge per group. Only the keys provided are updated.
{ "general": { "siteName": "Kraviona", "supportEmail": "hi@kraviona.com" } }
Updates a single setting by key.
{ "value": "dark" }
SEO
Global SEO settings, sitemap and robots.txt management, and per-page SEO audit scoring.
Returns merged SEO settings used by the public site to render <head> tags (defaultTitle, titleTemplate, defaultOgImage, etc.).
Updates global SEO settings. Automatically triggers sitemap.rebuild job on save.
{ "robotsPolicy": "index_follow", "noindexPages": ["/thank-you"], "defaultOgImage": "https://cdn..." }
Manually enqueues a sitemap rebuild BullMQ job. CDN cache for /sitemap.xml is invalidated on completion.
Returns on-page SEO audit for a specific path: title length, meta description length, canonical URL, schema.org presence, and image alt coverage.
Cached at the CDN. Invalidated when a sitemap rebuild job completes.
Generated dynamically from SeoSettings.robotsPolicy and noindexPages. Cached at edge.
Content Decay
Monitors content freshness and flags stale pages for review. Scores are computed nightly by a BullMQ worker.
Returns a list of stale content items with their decay scores (0–100, lower = more stale).
Updates decay rules (weights for age, traffic drop, link rot, etc.).
Enqueues a one-off BullMQ recompute job, bypassing the nightly schedule.
Tech Stack
Technologies and tools to showcase in the public site. Categorized and orderable.
Adds a new technology entry.
Updates a tech stack entry.
Deletes a tech stack entry.
Dashboard (Composite)
Convenience aggregation endpoints that bundle multiple data sources into one call, preventing the dashboard from making 10 parallel requests on load.
Returns everything the /dashboard home page renders: KPIs, recent leads, recent posts, recent audit rows, and unread notification count — all in one response.
200{ "kpis": { "pageviews": 18421, "leads": 247, "posts": 68 },
"recentLeads": [...], "recentPosts": [...],
"recentAudit": [...], "notifications": { "unread": 3 } }
Returns a recent activity feed across all modules (posts published, leads received, settings changed, etc.).
Error Reference
All errors share the same envelope. The code field is machine-readable and stable across versions. The message is human-readable. errors[] carries per-field validation failures.
{ "success": false, "data": null, "message": "Validation failed",
"code": "validation_failed", "requestId": "01HX0Q4Z3E3RD6P5SJ6B8B3Q7V",
"errors": [{ "path": "email", "message": "Invalid email" }] }
| HTTP | code | When |
|---|---|---|
| 200 | — | OK |
| 201 | — | Created — POST that returned a new resource |
| 204 | — | No content — empty DELETE responses |
| 400 | validation_failed | Zod schema failed — errors[] has field paths |
| 400 | bad_json | Request body is not valid JSON |
| 401 | unauthorized | No token, expired token, or invalid RS256 signature |
| 401 | token_revoked | Refresh token family was killed (theft detection) |
| 403 | forbidden | Authenticated but missing the required permission |
| 403 | account_blocked | User's isBlocked=true |
| 403 | csrf_failed | Missing or mismatched X-CSRF-Token header |
| 404 | not_found | Resource doesn't exist |
| 409 | conflict | Duplicate slug or unique index violation |
| 409 | version_mismatch | Optimistic concurrency — If-Match header didn't match |
| 410 | gone | Soft-deleted resource (treat as gone for public) |
| 413 | payload_too_large | Body > 1 MB, or upload exceeds size limit |
| 415 | unsupported_media_type | Wrong Content-Type for upload |
| 422 | unprocessable_entity | Semantic check failed (campaign in the past, post without category) |
| 429 | rate_limited | Per-IP or per-user rate limit hit. meta.retryAfter in seconds |
| 429 | otp_locked | Too many OTP attempts — locked for 15 min |
| 500 | internal_error | Unhandled — logged with full stack, never returned to client |
| 502 | upstream_error | S3 / SMTP / third-party call failed |
| 503 | service_unavailable | DB or Redis is down |
| 504 | upstream_timeout | Downstream service timed out |