API Reference¶
The CloviScan API lets you integrate CloviScan into your own applications and automations.
All endpoints are served over HTTPS from https://cloviscan.com.
Authentication¶
Authenticate every request with a Bearer token in the Authorization header. Generate a
token from your account settings on the CloviScan dashboard.
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 |
CloviScan API Reference¶
Service: CloviScan — website security and audit scanner
Base URL: https://cloviscan.com
Port: 8983 (internal)
Source: /root/cloviscan/server.js + /root/cloviscan/clovitrust.js
Last audited: 2026-06-16
Authentication Overview¶
CloviScan has two auth layers that work together:
| Method | Cookie/Header | Scope |
|---|---|---|
| cl_session SSO | cl_session (JWT cookie, set by clovitek-sso :8991) |
Web app pages + API routes |
| CloviScan_token | CloviScan_token (JWT cookie, set by /api/auth/login or /api/auth/register) |
Product-native login |
| Bearer token | Authorization: Bearer <token> |
Same JWT from either source |
requireAuth (used on web pages) redirects to /login on failure.
requireAuthApi (used on JSON routes) returns 401 { error: "authentication required" } on failure.
Both accept any of the three credential forms above.
Most scan routes are public (no auth required) with IP-based rate limiting (10 scans/hour). Only /api/scan/detonate, /api/scan/history, and /dashboard require auth.
Note: A public developer API with scoped API keys is not yet available. All routes below are the same application routes used by the web UI. A
csk_*API key is generated at registration but is not yet enforced on any route (the column exists, enforcement is planned).
Plans, Trial & Per-Tier Rate Limits¶
CloviScan has no free plan. There are four paid plans — Lite, Starter, Pro, and Agency — and each includes a 7-day free trial with full access to that plan's limits. During the trial your subscription status is in_trial and limits behave exactly like an active plan; on trial expiry it auto-converts to active and limits enforce normally.
Public scan routes (the lead-magnet instant scan) keep a flat IP-based limit of 10 scans/hour. Programmatic API access is gated to the higher plans, with per-tier request rates:
| Plan | API access | API rate limit | Deep scans / mo | Monitored domains | Report retention |
|---|---|---|---|---|---|
| Lite ($19) | — | — | 1 | 3 | 30 days |
| Starter ($39) | — | — | 3 | 10 | 90 days |
| Pro ($99) | Yes | 60 req/min | Unlimited | Unlimited | 365 days |
| Agency ($299) | Yes | 120 req/min | Unlimited | Unlimited (client pool) | 365 days |
GET /api/billing/limits¶
Returns the live per-tier limits ladder (drives pricing/docs UI). Auth: None.
{ "trial_days": 7, "tiers": [ { "tier": "lite", "deep_scans_month": 1, "monitored_domains": 3, ... }, ... ] }
POST /api/billing/quota-check¶
Checks whether a subscription may run a metered action. Body: { customer_id, plan, status, unit, amount } where status is the Chargebee subscription status (in_trial grants full plan limits). Returns 200 when allowed, 402 when the subscription is not entitled or over quota (with an upgrade_url). Usage is tracked via CloviMeter.
Application Routes (Web Pages)¶
GET /¶
Serves the landing page (landing.html).
Auth: None
GET /landing.html¶
Alias for /; serves the landing page.
Auth: None
GET /login¶
Serves the login page (login.html).
Auth: None
GET /dashboard¶
Serves the dashboard page (dashboard.html).
Auth: requireAuth — redirects to /login if not authenticated
GET /scan-github¶
Serves the GitHub repository scan UI page (inline HTML).
Auth: None
GET /report¶
Runs a live website security scan and renders the full HTML report page.
Auth: None (public)
Query Parameters:
- src or url (string, required) — URL or domain to scan (e.g. example.com)
- fresh (string, optional) — "1" to bypass the result cache and force a fresh scan
- detonate (string, optional) — "1" to trigger a live detonation via CloviBrowse before rendering
Behavior: Serves from cache if available; runs a fresh scan with a 13-second total budget otherwise. Embeddable in iframes (no X-Frame-Options restriction).
Errors:
- 400 — missing or invalid URL
- 500 — scan failed
GET /widget/¶
Embeddable security badge or scorecard widget (iframe-friendly HTML).
Auth: None (CORS: *)
Query Parameters:
- url (string) — URL or domain to display a score for
- theme (string, optional) — "light" (default) or "dark"
- style (string, optional) — "badge" (default) or "card" (full scorecard with category chips)
GET /embed/cloviscan.js¶
One-line embed loader script. Drop a <script> tag on any page to auto-mount a security badge.
Auth: None (CORS: *)
Cache-Control: public, max-age=300
Usage:
<script src="https://cloviscan.com/embed/cloviscan.js"
data-url="example.com"
data-key="csk_..."
data-theme="dark"
data-style="badge"></script>
GET /bookmarklet¶
Serves an HTML page with a drag-to-bookmarks-bar button for zero-install scanning.
Auth: None
GET /bookmarklet.txt¶
Returns the raw javascript: bookmarklet URL as plain text.
Auth: None
Content-Type: text/plain
Scan API Routes¶
POST /api/scan¶
Full website security scan. Runs SSL, HTTP headers, DNS, reputation, PageSpeed, exposure, and multi-page crawler checks in parallel.
Auth: None (public). Rate limit: 10 scans/hour per IP.
Content-Type: application/json
Body Parameters:
- url (string, required) — URL or domain to scan
Example:
curl -X POST https://cloviscan.com/api/scan \
-H "Content-Type: application/json" \
-d '{"url": "example.com"}'
Response:
{
"url": "https://example.com/",
"scanned_at": "2026-06-16T12:00:00.000Z",
"duration_ms": 4200,
"score": 74,
"grade": "C",
"ssl": { "valid": true, "days_remaining": 87, "issuer": "Let's Encrypt", "expires": "2026-09-01T00:00:00.000Z" },
"headers": { "score": 5, "max": 8, "found": ["X-Frame-Options"], "missing": ["Content-Security-Policy"] },
"dns": { "a_records": ["93.184.216.34"], "mx_records": [], "spf_found": true, "dmarc_found": true, "dmarc_policy": "reject", "dnssec_ds": false },
"reputation": { "status": "clean", "reason": "no threats detected" },
"speed": { "mobile_score": 72, "desktop_score": 88 },
"cert": { "expires": "2026-09-01T00:00:00.000Z", "days_remaining": 87, "valid": true, "issuer": "Let's Encrypt" },
"exposure": { "score": 68, "max": 100, "findings": [{ "id": "dns_no_dnssec", "severity": "medium", "detail": "...", "cwe": "CWE-345" }], "cms": "WordPress" },
"crawl": { "pages_crawled": 5, "truncated": false, "robots_respected": true }
}
Errors:
- 400 — url missing or invalid
- 429 — rate limit exceeded
- 500 — scan failed
POST /api/scan/github¶
GitHub repository security scan. Checks for secrets in the file tree and git commit history, sensitive files, dependency vulnerabilities, light SAST, and repo hygiene.
Auth: None (public). Rate limit: 10 scans/hour per IP.
Content-Type: application/json
Body Parameters:
- repo_url (string, required) — GitHub repository URL (e.g. https://github.com/owner/repo)
- token (string, optional) — GitHub personal access token for private repos (used in-memory only, never logged or persisted)
- email (string, optional) — Email for optional lead capture
Example:
curl -X POST https://cloviscan.com/api/scan/github \
-H "Content-Type: application/json" \
-d '{"repo_url": "https://github.com/owner/repo", "email": "[email protected]"}'
Response: Repository scan report JSON (shape defined by ./services/githubScan; token is never included in the response).
Errors:
- 400 — repo_url missing or invalid (code: "BAD_URL")
- 401 — GitHub auth failed (code: "AUTH")
- 404 — repository not found (code: "NOT_FOUND")
- 429 — rate limit exceeded
- 503 — GitHub scanner module unavailable
- 500 — scan failed (code: "SCAN_FAIL")
POST /api/scan/detonate¶
Opens a suspicious URL in a disposable isolated browser (via the CloviBrowse internal service). Returns behavioral analysis including screenshot ID.
Auth: requireAuthApi — requires cl_session cookie, CloviScan_token cookie, or Authorization: Bearer <token>
Content-Type: application/json
Body Parameters:
- url (string, required) — URL to detonate in the isolated browser
Example:
curl -X POST https://cloviscan.com/api/scan/detonate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"url": "https://suspicious-site.example.com"}'
Response:
Errors:
- 400 — url missing or invalid
- 401 — authentication required
- 502 — detonation error
- 503 — detonation capability unavailable (CloviBrowse not loaded)
POST /api/scan/detonate/report¶
Public (no auth) detonation endpoint used by the "Run Live Detonation" button inside the /report page.
Auth: None (public)
Content-Type: application/json
Body Parameters:
- url (string, required) — URL to detonate
Example:
curl -X POST https://cloviscan.com/api/scan/detonate/report \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
Response:
On failure (detonation unavailable or any error): { "ok": false, "error": "unavailable" } — always HTTP 200.
POST /api/scan/verify¶
Re-scans a URL with cache bypass and returns a before/after delta comparing the new result against the previously cached scan.
Auth: None (public). Rate limit: 10 scans/hour per IP.
Content-Type: application/json
Body Parameters:
- url (string, required) — URL or domain to re-scan
Example:
curl -X POST https://cloviscan.com/api/scan/verify \
-H "Content-Type: application/json" \
-d '{"url": "example.com"}'
Response:
{
"domain": "example.com",
"prevScore": 68,
"newScore": 74,
"prevGrade": "C",
"newGrade": "C",
"scoreDelta": 6,
"resolved": ["ssl_weak_cipher"],
"newFindings": [],
"noChange": false,
"scanned_at": "2026-06-16T12:05:00.000Z",
"prev_scanned_at": "2026-06-15T09:00:00.000Z"
}
Errors:
- 400 — url missing or invalid
- 429 — rate limit exceeded
- 500 — verify scan failed
- 504 — re-scan timed out (40-second hard limit)
GET /api/scan/history¶
Returns the 10 most recent scans for the authenticated user (in-memory, not persisted across restarts).
Auth: requireAuth — requires cl_session cookie
Query Parameters: None
Example:
Response:
{
"history": [
{ "url": "https://example.com/", "scanned_at": "...", "score": 74, "grade": "C", ... }
]
}
Errors:
- 302 redirect to /login if not authenticated
Widget API Routes¶
GET /api/widget/public/score¶
Returns a lightweight score summary for a URL. Used by the badge widget iframe.
Auth: None (CORS: *)
Query Parameters:
- url (string, required) — URL or domain to score
Example:
Response:
{
"url": "https://example.com/",
"score": 74,
"grade": "C",
"ssl_ok": true,
"ssl_days": 87,
"headers_score": 5,
"headers_max": 8
}
Errors: 400 — url missing or invalid
GET /api/widget/public/card¶
Returns a richer scorecard roll-up for the card-style widget, including per-category pass/fail status.
Auth: None (CORS: *)
Query Parameters:
- url (string, required) — URL or domain
Example:
Response:
{
"url": "https://example.com/",
"score": 74,
"grade": "C",
"verdict": "Needs Attention",
"counts": { "critical": 0, "high": 1, "medium": 3, "low": 2 },
"controls_tested": 18,
"controls_total": 22,
"categories": [
{ "label": "TLS / SSL", "status": "pass", "severity": null },
{ "label": "HTTP Headers", "status": "fail", "severity": null }
]
}
Pending response (scan still running): { "url": "...", "pending": true, "message": "re-scanning" } — HTTP 200.
Errors: 400 — url missing or invalid; 500 — card roll-up failed
POST /api/widget/install¶
Registers or heartbeats a widget embed install. Called automatically by embed/cloviscan.js on page load. Idempotent (upserts).
Auth: None (CORS: *)
Content-Type: application/json
Body Parameters:
- domain (string, required) — Installing domain (e.g. example.com)
- key or api_key (string, optional) — CloviScan API key for plan-level features
Example:
curl -X POST https://cloviscan.com/api/widget/install \
-H "Content-Type: application/json" \
-d '{"domain": "example.com", "key": "csk_..."}'
Response:
Errors: 400 — domain missing
POST /api/widget/feedback¶
Records a user feedback report about a scan result. Feeds the training ledger.
Auth: None (CORS: *)
Content-Type: application/json
Body Parameters:
- domain (string) — Domain the feedback is about
- url (string, optional) — Full page URL (max 2048 chars)
- issue (string) — One of: wrong_score, false_positive, missing_check, bad_layout, other (defaults to other)
- note (string, optional) — Free-text explanation (max 1000 chars)
Example:
curl -X POST https://cloviscan.com/api/widget/feedback \
-H "Content-Type: application/json" \
-d '{"domain": "example.com", "issue": "wrong_score", "note": "SSL cert is valid but shown as invalid"}'
Response:
Report API Routes¶
POST /api/report/pdf¶
Renders the security report for a URL as a PDF (requires the report_pdf module to be loaded).
Auth: None
Content-Type: application/json
Body Parameters:
- url or src (string, required) — URL or domain to generate the PDF for
Example:
curl -X POST https://cloviscan.com/api/report/pdf \
-H "Content-Type: application/json" \
-d '{"url": "example.com"}'
Response:
(exact shape defined by./report_pdf.renderReportPdf)
Errors:
- 400 — url missing or invalid
- 500 — PDF render failed
- 501 — PDF engine not available (report_pdf module missing)
Auth API Routes¶
POST /api/auth/register¶
Self-serve user registration. Creates a new account with a free plan and a csk_* API key. Sets a CloviScan_token cookie on success.
Auth: None
Content-Type: application/json
Body Parameters:
- email (string, required) — Valid email address
- password (string, required) — Minimum 6 characters
- full_name or name (string, optional) — Display name
Example:
curl -X POST https://cloviscan.com/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "securepass", "full_name": "Jane Doe"}'
Response (201):
{
"ok": true,
"token": "<jwt>",
"user": { "id": 1, "email": "[email protected]", "name": "Jane Doe", "role": "user", "plan": "free" }
}
Errors:
- 400 — invalid email or password too short
- 409 — email already registered
- 500 — registration failed (DB error)
POST /api/auth/login¶
Authenticates an existing user. Sets a CloviScan_token cookie (30-day expiry).
Auth: None
Content-Type: application/json
Body Parameters:
- email (string, required)
- password (string, required)
Example:
curl -X POST https://cloviscan.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "securepass"}'
Response:
{
"ok": true,
"token": "<jwt>",
"user": { "id": 1, "email": "[email protected]", "name": "Jane Doe", "role": "user", "plan": "free" }
}
Errors:
- 400 — email or password missing
- 401 — invalid credentials
GET /api/auth/me¶
Returns the current authenticated user. Accepts cl_session SSO cookie, CloviScan_token cookie, or Bearer token.
Auth: Any valid token (returns 401 if none)
Query Parameters: None
Example:
Response:
{ "ok": true, "user": { "id": 1, "email": "[email protected]", "role": "user", "plan": "free" } }
Errors: 401 — Unauthorized
GET /api/me¶
Alias for GET /api/auth/me. Returns the same user object without the ok wrapper key.
Auth: Any valid token (returns 401 if none)
Response:
{ "user": { "id": 1, "email": "[email protected]", "role": "user", "plan": "free" } }
Errors: 401 — Unauthorized
POST /api/auth/logout¶
Clears the CloviScan_token cookie.
Auth: None required (silently ignores missing cookie)
Example:
Response:
Lead Capture Route¶
POST /api/lead¶
Gated email lead capture from the report page. Stores the lead in SQLite (cloviscan.db) and a JSONL backup file. Triggers best-effort CRM capture.
Auth: None
Content-Type: application/json
Body Parameters:
- email (string, required) — Valid email address (max 254 chars)
- url (string, optional) — URL that was scanned (max 2048 chars)
- score (number, optional) — Scan score at time of capture
- grade (string, optional) — Scan grade (max 8 chars)
Example:
curl -X POST https://cloviscan.com/api/lead \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "url": "example.com", "score": 74, "grade": "C"}'
Response:
Errors: 400 — invalid or missing email
Monitoring Routes¶
POST /api/monitor/subscribe¶
Subscribes an email address to weekly monitoring alerts for a domain. Idempotent — re-subscribing the same email/domain pair returns success without creating a duplicate.
Auth: None. Rate limit: 5 subscribes/hour per IP (separate bucket from scan rate limit).
Content-Type: application/json
Body Parameters:
- url (string, required) — URL or domain to monitor
- email (string, required) — Alert destination email
Example:
curl -X POST https://cloviscan.com/api/monitor/subscribe \
-H "Content-Type: application/json" \
-d '{"url": "example.com", "email": "[email protected]"}'
Response (new subscription):
{ "ok": true, "domain": "example.com", "email": "[email protected]", "message": "Subscribed to weekly monitoring alerts." }
Response (already subscribed):
{ "ok": true, "domain": "example.com", "email": "[email protected]", "message": "Already subscribed to monitoring alerts.", "already_subscribed": true }
Errors:
- 400 — url or email missing, or invalid values
- 429 — rate limit exceeded
- 500 — database error
GET /api/monitor/subscriptions¶
Returns all active monitoring subscriptions (internal/admin use — no auth gate currently enforced).
Auth: None (no auth gate in current code)
Query Parameters: None
Example:
Response:
{
"subscriptions": [
{ "domain": "example.com", "email": "[email protected]", "created_at": "2026-06-16T12:00:00.000Z" }
]
}
Detonation Screenshot Proxy¶
GET /api/detonation/screenshot/:id¶
Proxies a screenshot PNG from the internal CloviBrowse service so report <img> tags can reference it on-domain. Validates the :id path parameter against [a-zA-Z0-9_-]{8,64} to prevent path traversal.
Auth: None (public — uses internal X-Clovi-Secret header for the upstream CloviBrowse request)
Path Parameters:
- id (string) — Screenshot ID (8–64 chars, alphanumeric + _-)
Example:
Response: PNG image (Content-Type: image/png; Cache-Control: public, max-age=3600)
Errors:
- 404 — invalid ID format or screenshot not found in CloviBrowse
- 504 — upstream timeout (10 seconds)
Health Check¶
GET /api/health¶
Health check endpoint.
Auth: None
Example:
Response:
CloviTrust Badge Routes¶
Mounted via clovitrust.js router at the application root.
GET /badge/:check¶
Returns a live status badge for a domain, powered by CloviScan scan data. Supports HTML (default), JSON, SVG, and redirect formats.
Auth: None (CORS: *)
Cache-Control: public, max-age=300
Path Parameters:
- check (string) — Badge check type (see CHECKS registry in clovitrust.js)
Query Parameters:
- domain or src or url (string, required) — Domain to badge
- format (string, optional) — "html" (default), "json", "svg", or "redirect"
Example:
Response (format=json):
{
"check": "security",
"domain": "example.com",
"color": "#22c55e",
"label": "Security",
"by": "CloviScan",
"report": "https://cloviscan.com/report?src=example.com",
"state": "pass",
"text": "A+"
}
Errors:
- 400 — domain missing
- 404 — unknown or disabled check type
GET /clovitrust.js¶
Embeddable trust badge widget JavaScript.
Auth: None (CORS: *)
Cache-Control: public, max-age=600
Content-Type: application/javascript
GET /clovitrust.css¶
Shared badge CSS for standalone inclusion.
Auth: None (CORS: *)
Cache-Control: public, max-age=600
Content-Type: text/css
GET /clovitrust/demo¶
Standalone design demo page for the CloviTrust badge widget.
Auth: None (CORS: *)
Content-Type: text/html
Rate Limiting Summary¶
| Route group | Limit | Bucket |
|---|---|---|
POST /api/scan, POST /api/scan/github, POST /api/scan/verify |
10/hour per IP | shared scan bucket |
POST /api/monitor/subscribe |
5/hour per IP | separate subscribe bucket |
| All other routes | No rate limit | — |
Privacy & Egress Architecture¶
All outbound requests to scanned target sites are routed through an egress proxy (configured via CLOVISCAN_EGRESS_PROXY env var) so the target sees a VPN IP, not the server's real IP. This is fail-closed: if the proxy is configured but broken, target fetches refuse rather than falling back to direct egress.
Google API calls (PageSpeed, Safe Browsing) are NOT proxied — they are calls from CloviScan to Google, not to the scanned target, so proxying them provides no privacy benefit.