Skip to main content

REST API reference

The API-key-authenticated REST surface for scripts, CI/CD pipelines, and custom integrations. To drive scans from an AI assistant instead, see the MCP setup guide.

Base URL

Every endpoint below is relative to this base - e.g. POST /v1/scans/trigger means POST https://api.flawpilot.com/v1/scans/trigger.

Base URL
https://api.flawpilot.com

Authentication

Every endpoint except the two health checks requires a Bearer API key. Create and manage keys from the dashboard under Settings → API Keys. A key identifies a workspace (tenant) - there is no per-user auth on this surface, and the same key works for the MCP server too.

Authorization header
Authorization: Bearer YOUR_API_KEY

Missing or invalid keys get:

401 Unauthorized
{ "statusCode": 401, "message": "Unauthorized" }

Rate limits

ScopeLimitApplies to
Per-tenant, general60 req / minEvery authenticated endpoint, combined
Per-tenant, trigger scan5 req / minPOST /v1/scans/trigger only (on top of the general limit)
Per-IP, scan status2000 req / minGET /v1/scans/:id/status - limited by IP, not the general tenant limit, since the website's own progress UI also polls the same underlying service.

Exceeding a limit returns 429 with a Retry-After header (seconds until the window resets):

429 Too Many Requests
{ "error": "Too many requests. Please retry shortly." }

Error shape

Authenticated and validated endpoints that fail return a consistent shape. There is no separate machine-readable error code - match on statusCode plus, if needed, the exact message text documented per endpoint.

Error response
{ "statusCode": 400, "message": "<human-readable message>" }
Known limitation

A few scan-creation failures from the scanning backend (e.g. the target domain failing DNS resolution, or the backend's own rate limit) are not yet mapped to their intended status code and currently surface as a generic 500. The endpoint docs below list intended-but-not-yet-guaranteed cases separately from confirmed ones.

Health checks

Both health checks are unauthenticated.

GET /health

Liveness probe. No auth, not rate-limited beyond infrastructure defaults.

200 - always
{ "status": "ok", "uptime": 1234.5 }

GET /ready

Readiness probe - checks the database and Redis are reachable (1s timeout each).

200 - both checks pass
{ "status": "ready" }
503 - a check failed
{ "status": "not_ready", "checks": { "db": false, "redis": true } }

List projects

GET /v1/projects

Lists the tenant's projects. Use this to discover a projectId before triggering a scan. Auth required; general tenant rate limit only.

Query parameters

ParamTypeDefaultDescription
includeArchivedboolean (string "true")falseInclude archived projects in the results.

Response - 200

200 OK
[
{ "projectId": "PRJ-3TV1T", "name": "Marketing Site", "status": "ACTIVE" }
]
FieldTypeDescription
projectIdstringShort, stable identifier - pass to trigger_scan. Not a DB ID.
namestringProject display name.
statusstringACTIVE or ARCHIVED.

Example

curl
curl https://api.flawpilot.com/v1/projects \
-H "Authorization: Bearer YOUR_API_KEY"

Trigger a scan

POST /v1/scans/trigger

Queues a scan for a URL against one of the tenant's projects. Auth required; general limit and the stricter 5/min trigger-scan limit.

Request body

FieldTypeRequiredNotes
projectIdstringYesFrom GET /v1/projects. null is rejected the same as omitting it.
urlstringYesMust be http:// or https:// and include the protocol. null is rejected the same as omitting it.
pillarsarray of SECURITY | PERFORMANCE | INFRASTRUCTURE | SEONoOmitted, null, or [] = full scan across all four pillars.
emailstring (valid email)NoSends the report here on completion. To skip it, omit the field or send null or "" - all three are equivalent. Does not accept the literal "skip".

With an email

Request body
{
"projectId": "PRJ-3TV1T",
"url": "https://example.com",
"pillars": ["SECURITY"],
"email": "jane@example.com"
}

Skipping the email - these three are equivalent:

email: null
{ "projectId": "PRJ-3TV1T", "url": "https://example.com", "email": null }
email: empty string
{ "projectId": "PRJ-3TV1T", "url": "https://example.com", "email": "" }
email omitted
{ "projectId": "PRJ-3TV1T", "url": "https://example.com" }

Example

curl
curl -X POST https://api.flawpilot.com/v1/scans/trigger \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectId": "PRJ-XXXXX",
"url": "https://example.com",
"pillars": ["SECURITY"],
"email": "jane@example.com"
}'

Response - 202 Accepted

202 Accepted
{ "scanJobId": "b3f1...", "status": "QUEUED", "queuedAt": "2026-07-27T10:00:00.000Z" }

Errors (confirmed)

StatusCause
400Request body failed validation (missing/invalid projectId, url, pillars, or email).
400"This project is archived and cannot be modified!"
404"Project not found!" - projectId doesn't exist or belongs to another tenant.
429Tenant or trigger-scan-specific limit exceeded.

Errors (intended, not yet guaranteed)

StatusCause
400Target domain isn't DNS-resolvable ("We couldn't reach that site…").
429The scanning backend's own internal rate limit ("Scan limit reached…").

Scan status

GET /v1/scans/:id/status

Poll scan progress. Auth required - send your Bearer API key, same as every other endpoint here. Rate-limited per IP, not the general tenant limit. Responses are cached a few seconds, so polling every ~5s is fine.

Always authenticated

This public route is always authenticated. The website's own progress UI polls a separate, unauthenticated version of the same underlying service for logged-in browser sessions - but this API route never accepts unauthenticated calls.

Response - 200 (non-terminal)

200 OK - RUNNING
{
"status": "RUNNING",
"completed": 8,
"total": 20,
"startedAt": "2026-07-27T10:00:05.000Z",
"url": "https://example.com",
"email": "jane@example.com",
"categories": [
{
"pillar": "SECURITY",
"title": "Security",
"score": 62.5,
"max_score": 100,
"band": "ELEVATED_RISK",
"results": [ /* subcategory scores */ ]
}
]
}

Once status reaches a terminal value (COMPLETE, PARTIAL, or FAILED), the response also includes:

FieldDescription
completedAtISO-8601 completion time.
bandPRODUCTION_READY | GROWTH_READY | ELEVATED_RISK | CRITICAL_RISK - see Risk bands.
score_security, score_performance, score_infrastructure, score_seoPer-pillar scores. score_total is not included on this API surface - it is present on the website's own status polling, but withheld from public API and MCP responses.
shareToken / shortCodeSame value, two names. Treat as a bearer credential - anyone holding it can view the public report.
slugPresent if a human-readable slug was generated for the share URL.
reportLinkFull URL to the public report page, e.g. https://app.flawpilot.com/report/<shortCode>.

Errors

StatusCause
401"Unauthorized" - missing or invalid API key.
404"Scan not found!" - id doesn't correspond to any scan.