Skip to content

API Reference

The CloviNarrate API lets you integrate CloviNarrate into your own applications and automations. All endpoints are served over HTTPS from https://clovinarrate.clovitek.com.

Authentication

Authenticate every request with a Bearer token in the Authorization header. Generate a token from your account settings on the CloviNarrate dashboard.

curl https://clovinarrate.clovitek.com/api/me \
  -H "Authorization: Bearer $CLOVI_TOKEN"
import requests

resp = requests.get(
    "https://clovinarrate.clovitek.com/api/me",
    headers={"Authorization": f"Bearer {token}"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://clovinarrate.clovitek.com/api/me", {
  headers: { Authorization: `Bearer ${token}` },
});
const data = await resp.json();
console.log(data);
import axios from "axios";

const { data } = await axios.get(
  "https://clovinarrate.clovitek.com/api/me",
  { headers: { Authorization: `Bearer ${token}` } }
);
console.log(data);

Keep your token secret

Treat your API token like a password. Send it only over HTTPS and never commit it to source control — load it from an environment variable instead.

Responses & errors

All responses are JSON. Successful calls return 2xx; client errors return 4xx with a JSON body describing the problem. Common status codes:

Status Meaning
200 Success
400 Bad request — check your parameters
401 Missing or invalid token
404 Resource not found
429 Rate limit exceeded — slow down and retry
500 Server error — retry or contact support

CloviNarrate API Reference

Extracted from /root/clovinarrate/server.js — all routes verified against source code.
Port: 8969
Base URL (local): http://localhost:8969


Authentication

CloviNarrate uses a three-priority auth chain on every protected route (requireAuth):

Priority Mechanism Secret
1 cl_session cookie (CloviTek SSO JWT) JWT_SECRET from master.env
2 cn_token cookie or Authorization: Bearer <token> (standalone JWT) CLOVINARRATE_JWT_SECRET from master.env
3 X-User-Id header + X-Internal-Secret: clovitek-internal (server-to-server only) hardcoded sentinel

All three paths set req.user = { id, email, name, plan, role }.
Admin role is detected via req.isAdmin (roles: admin, super_admin, platform_admin).

All /api/* routes return JSON. Unmatched /api/* paths return 404 {"error":"Not found"}.


No Auth Required

GET /health

Returns service health. No authentication required.

Response:

{ "status": "ok", "service": "clovinarrate", "port": 8969 }


Auth Routes (Standalone — no prior auth needed)

POST /api/auth/register

Register a new standalone CloviNarrate account. Sets cn_token HttpOnly cookie (30-day). Fires n8n onboarding webhook fire-and-forget.

Auth: None
Body:

{ "email": "user@example.com", "password": "secret", "full_name": "Jane Doe" }
- email and password are required. - full_name is optional.

Response 201:

{ "ok": true, "token": "<jwt>", "user": { "id": 1, "email": "user@example.com", "name": "Jane Doe", "plan": "free", "role": "user" } }
Errors: 400 {"error":"Email and password required"}, 409 {"error":"Email already registered"}, 500


POST /api/auth/login

Authenticate with standalone credentials. Sets cn_token HttpOnly cookie (30-day).

Auth: None
Body:

{ "email": "user@example.com", "password": "secret" }

Response 200:

{ "ok": true, "token": "<jwt>", "user": { "id": 1, "email": "user@example.com", "name": "Jane Doe", "plan": "free|pro|team", "role": "user" } }
Errors: 400 {"error":"Email and password required"}, 401 {"error":"Invalid email or password"}, 500


POST /api/auth/logout

Clears the cn_token cookie.

Auth: None (cookie is cleared regardless)
Response:

{ "ok": true }


User Routes

GET /api/me

Return the currently authenticated user's profile.

Auth: Any valid auth (cookie or Bearer)
Response:

{ "user": { "id": 1, "email": "user@example.com", "name": "Jane Doe", "plan": "free", "role": "user" } }
Errors: 401 {"error":"Not authenticated"}


GET /api/ai/usage

Return the authenticated user's AI credit usage for this session/period (sourced from aiCredits module).

Auth: Any valid auth
Response: Shape determined by aiCredits.usage(req) — varies by implementation.
Errors: 401


Narration Routes

All narration routes are owner-scoped: regular users can only access their own narrations. Admin users (role: admin/super_admin/platform_admin) have elevated access as noted.

GET /api/narrations

List narration projects.

Auth: Any valid auth
Scope: Own narrations only. Admin: Returns all users' narrations with user_email joined.

Example:

curl -H "Authorization: Bearer <token>" http://localhost:8969/api/narrations

Response:

{
  "narrations": [
    { "id": 1, "user_id": 1, "title": "My Deck", "source_type": "upload", "source_ref": null,
      "status": "complete", "slide_count": 5, "voice_id": "AJbGPofgjOsJCmSoV9SO",
      "full_video_key": "1/full.mp4", "created_at": "...", "updated_at": "..." }
  ]
}
(Admin response includes "user_email" on each narration.)
Errors: 401, 500


POST /api/narrations

Create a new narration project.

Auth: Any valid auth
Body:

{ "title": "My New Narration", "source_type": "upload", "source_ref": null }
- title is required. - source_type defaults to "upload". Valid values: "upload", "clovidecks". - source_ref is optional (e.g. CloviDecks project ID).

Response 201:

{ "narration": { "id": 2, "user_id": 1, "title": "My New Narration", "source_type": "upload", ... } }
Errors: 400 {"error":"title is required"}, 401, 500


GET /api/narrations/:id

Get a single narration plus all its slides, ordered by slide_num ASC.

Auth: Any valid auth
Scope: Owner only (no admin bypass on this route — uses user_id check).

Response:

{
  "narration": { "id": 1, "title": "...", "status": "complete", ... },
  "slides": [
    { "id": 10, "narration_id": 1, "slide_num": 1, "narration_text": "Hello...",
      "ssml_text": null, "image_key": "uploads/img.png", "status": "done",
      "audio_key": "1/slide_1.mp3", "video_key": "1/slide_1.mp4" }
  ]
}
Errors: 401, 404 {"error":"Not found"}


PUT /api/narrations/:id

Update narration metadata. At least one field must be provided.

Auth: Any valid auth
Scope: Owner only.
Body (partial):

{ "title": "Updated Title", "voice_id": "AJbGPofgjOsJCmSoV9SO" }

Response:

{ "narration": { "id": 1, "title": "Updated Title", ... } }
Errors: 400 {"error":"Nothing to update"}, 401, 404, 500


DELETE /api/narrations/:id

Delete a narration and all its slides. Also removes output/<id>/ directory from disk.

Auth: Any valid auth
Scope: Owner only. Admin: Can force-delete any user's narration.

Response:

{ "success": true }
Errors: 401, 404, 500


POST /api/narrations/:id/slides

Add a slide to a narration. Updates slide_count on the parent record.

Auth: Any valid auth
Scope: Owner only.
Body:

{ "slide_num": 1, "narration_text": "Welcome to the presentation.", "image_key": "uploads/slide1.png" }
- slide_num is required. - narration_text defaults to "". - image_key is optional (local absolute path, or relative key resolved under /root/clovidecks/uploads/).

Response 201:

{ "slide": { "id": 10, "narration_id": 1, "slide_num": 1, "narration_text": "...", "image_key": "...", "status": "pending", ... } }
Errors: 400 {"error":"slide_num is required"}, 401, 404 {"error":"Narration not found"}, 500


PUT /api/narrations/:id/slides/:num

Update narration text or SSML for a specific slide (identified by slide_num). At least one field must be provided.

Auth: Any valid auth
Scope: Owner only.
Path params: :id = narration ID, :num = slide_num value.
Body (partial):

{ "narration_text": "Updated narration.", "ssml_text": "<speak>Updated.</speak>" }

Response:

{ "slide": { "id": 10, "narration_id": 1, "slide_num": 1, "narration_text": "Updated narration.", "ssml_text": "<speak>Updated.</speak>", ... } }
(Returns "slide": null if slide not found after update.)
Errors: 400 {"error":"Nothing to update"}, 401, 404 {"error":"Narration not found"}, 500


POST /api/narrations/:id/generate

Start async audio + video generation for all slides in the narration. Resets all slides to status='pending' before firing the job via setImmediate.

Auth: Any valid auth
Scope: Owner only.
AI Credit Gate: Reserves ElevenLabs credits ($0.03 x slide_count) via aiCredits.reserve(). Returns 402 if quota exceeded.

Generation pipeline per slide: 1. ElevenLabs TTS (eleven_multilingual_v2, uses ssml_text if set, else narration_text) → MP3 2. ffmpeg: slide image + MP3 → per-slide MP4 (falls back to black 1280x720 background if image unavailable) 3. ffmpeg concat → full.mp4

Response:

{ "message": "Generation started", "narration_id": 1 }
Errors: 401, 402 <aiCredits gate body>, 404 {"error":"Narration not found"}, 500


GET /api/narrations/:id/status

Poll generation status. Use to track async generation progress.

Auth: Any valid auth
Scope: Owner only. Admin: Can monitor any user's narration (response also includes user_id).

Example:

curl -H "Authorization: Bearer <token>" http://localhost:8969/api/narrations/1/status

Response:

{
  "narration": { "id": 1, "status": "pending|processing|complete|error", "slide_count": 5, "full_video_key": "1/full.mp4", "updated_at": "..." },
  "slide_counts": { "pending": 2, "processing": 1, "done": 2, "error": 0 },
  "slides": [
    { "slide_num": 1, "status": "done", "audio_key": "1/slide_1.mp3", "video_key": "1/slide_1.mp4" }
  ]
}
Errors: 401, 404 {"error":"Not found"}, 500


GET /api/narrations/:id/video/:num

Stream/download a per-slide video file from output/<id>/slide_<num>.mp4.

Auth: Any valid auth
Scope: Owner only.
Path params: :id = narration ID, :num = slide number.

Response: MP4 file stream
Errors: 401, 404 {"error":"Not found"}, 404 {"error":"Video not ready"}, 500


GET /api/narrations/:id/fullvideo

Stream/download the concatenated full video from output/<id>/full.mp4.

Auth: Any valid auth
Scope: Owner only.

Response: MP4 file stream
Errors: 401, 404 {"error":"Not found"}, 404 {"error":"Full video not ready"}, 404 {"error":"Video file missing"}, 500


Integration Routes

POST /api/import

Import slides from CloviDecks into a new narration project. Runs in a DB transaction; rolls back on error.

Auth: Any valid auth
Security note: body.userId is intentionally ignored — the authenticated user's ID is always used, preventing cross-user injection.

Body:

{
  "projectId": "clovidecks-project-uuid",
  "title": "My Imported Deck",
  "slides": [
    { "slide_num": 1, "image_key": "uploads/img1.png", "narration_text": "Slide one text." },
    { "slide_num": 2, "image_key": "uploads/img2.png", "narration_text": "Slide two text." }
  ]
}
- slides array is required. - title defaults to "Imported from CloviDecks (<projectId>)" if omitted. - Creates narration with source_type='clovidecks' and source_ref=projectId.

Response 201:

{ "narration_id": 3 }
Errors: 400 {"error":"slides array is required"}, 401, 500


Admin Routes

All admin routes require role of admin, super_admin, or platform_admin. Returns 403 if authenticated but not admin.

GET /api/admin/users

List all registered users with their narration counts. For ops visibility.

Auth: Admin only (requireAdmin middleware — requireAuth + role check)

Response:

{
  "users": [
    { "id": 1, "email": "admin@clovitek.com", "full_name": "CloviTek Admin",
      "plan": "team", "role": "admin", "created_at": "...", "narration_count": 5 }
  ]
}
Errors: 401, 403 {"error":"Admin access required"}, 500


GET /api/admin/narrations

List all narrations across all users (latest 200, ordered by updated_at DESC). Includes user_email.

Auth: Admin only

Response:

{
  "narrations": [
    { "id": 1, "user_id": 2, "user_email": "user@example.com", "title": "...", "status": "complete", ... }
  ]
}
Errors: 401, 403 {"error":"Admin access required"}, 500


Shell / SPA Routes

These routes serve HTML pages (not JSON). Auth enforcement is handled client-side by the SPA shell.

Path Description
GET /app Serves shell/app.html — authenticated app shell (CloviTek shared UI-kit)
GET /login Serves shell/login.html — standalone login page
GET /register Serves shell/login.html — standalone registration page (same shell file)
GET /output/* Serves generated output files (MP3/MP4) from output/ directory. No auth at middleware level.
GET /* (non-API) SPA catch-all — serves public/index.html. Returns 503 if app not yet built.

Data Models

Narration (cn_narrations)

Field Type Notes
id int Primary key
user_id int FK to users.id
title varchar Required
source_type varchar "upload" or "clovidecks"
source_ref varchar CloviDecks project ID, or null
status varchar pending / processing / complete / error
slide_count int Auto-maintained
voice_id varchar ElevenLabs voice ID
full_video_key varchar Relative path: <id>/full.mp4
created_at datetime
updated_at datetime Updated on every change

Slide (cn_slides)

Field Type Notes
id int Primary key
narration_id int FK to cn_narrations.id
slide_num int 1-based
narration_text text Plain-text narration
ssml_text text SSML override (used first during generation if present)
image_key varchar Local path or CloviDecks upload key
status varchar pending / processing / done / error
audio_key varchar <narration_id>/slide_<num>.mp3
video_key varchar <narration_id>/slide_<num>.mp4

User (users)

Field Type Notes
id int Primary key
email varchar Unique
pw_hash varchar bcrypt, cost 12
full_name varchar
plan enum free / pro / team
role enum user / admin
created_at datetime

Voice & Generation Settings

Setting Value
Default Voice ID AJbGPofgjOsJCmSoV9SO
ElevenLabs Model eleven_multilingual_v2
Stability 0.40
Similarity Boost 0.82
Style 0.10
Speaker Boost true
ffmpeg binary /usr/bin/ffmpeg
Output directory <server_root>/output/<narration_id>/

Notes on Public Developer API

CloviNarrate does not currently expose a public developer API with API key auth. All routes use session cookie or Bearer token auth tied to registered user accounts. A public API surface (with per-tenant API keys, rate limiting, and webhook events) is planned for a future release.