FlawPilot
REST API

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-tenant, trigger code scan10 req / minPOST /v1/code-scans/trigger only (on top of the general limit)
Per-tenant, trigger Site Health8 req / HOURPOST /v1/site-health/trigger only. The one limit on this surface measured per hour rather than per minute - a full-site crawl is far heavier than a single scan.
Per-tenant, Site Health reads30 req / minGET /v1/site-health/:id and GET /v1/projects/:projectId/site-health
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. The original scan endpoints carry no separate machine-readable code - match on statusCode plus, if needed, the exact message text documented per endpoint. The Code Scan and Site Health endpoints do return a named code, listed below.

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. Endpoint docs below list intended-but-not-yet-guaranteed cases separately from confirmed ones.

Named codes - Code Scan and Site Health

CodeStatusMeaning
PROJECT_NOT_FOUND404The shortId does not resolve to a project.
PROJECT_ARCHIVED400The project exists but is archived.
INTEGRATION_REPO_NOT_FOUND404Unknown or foreign integrationRepoId.
INTEGRATION_REPO_NOT_ASSIGNED400The repo is not assigned to a project yet, so there is nothing to scan it against.
INTEGRATION_REPO_DISABLED400The repo is connected but excluded from scanning.
INTEGRATION_REPO_NO_BRANCH400No branch is selected or resolvable.
INTEGRATION_REPO_PROJECT_MISMATCH400The repo moved project between resolution and the trigger.
INTEGRATION_REPO_ACCESS_DENIED403The provider returned 403 - the token is valid but the repo is outside its access list.
INTEGRATION_REPO_BRANCH_NOT_FOUND404The provider has no such branch.
TENANT_INTEGRATION_NOT_FOUND404The connection row itself is missing.
TENANT_INTEGRATION_NOT_ACTIVE400Connection status is not ACTIVE. Checked before any provider call.
TENANT_INTEGRATION_TOKEN_EXPIRED400The stored token is known to be expired.
TENANT_INTEGRATION_INVALID401The provider returned 401 - the credentials are dead and the connection needs reconnecting.
UNSUPPORTED_INTEGRATION_PROVIDER400No adapter exists for this provider.
CODE_SCAN_JOB_NOT_FOUND404Unknown or foreign job id.
CODE_SCAN_RUN_NOT_FOUND404Unknown or foreign scanRunId.
SITE_HEALTH_NOT_FOUND404Unknown or foreign Site Health id.
VALIDATION_ERROR400A request field failed validation.

Health checks

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": "[email protected]"
}

Skipping the email (these three are equivalent)

email: null
{ "projectId": "PRJ-3TV1T", "url": "https://example.com", "email": null }
email: ""
{ "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": "[email protected]"
  }'

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.

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": "[email protected]",
  "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
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.

List connected repos

GET/v1/integration-repos

Every repository connected to the tenant, flattened across all Git connections. Auth required; general 60/min limit.

Optional ?projectId= filters to one project. Use integrationRepoId from this response to trigger a code scan.

Response - 200

200 OK
[
  {
    "integrationRepoId": "a1b2c3d4-...",
    "name": "checkout-service",
    "fullName": "acme/checkout-service",
    "projectId": "PRJ-3TV1T",
    "enabled": true,
    "defaultBranch": "main",
    "selectedBranch": "main",
    "connectionName": "acme GitHub",
    "connectionStatus": "ACTIVE"
  }
]

Fields

FieldNotes
projectIdThe project’s shortId, or null when the repo is not assigned to a project yet. An unassigned repo cannot be scanned.
enabledfalse means the repo is connected but excluded from scanning.
connectionStatusACTIVE means the connection is healthy. Anything else (DISCONNECTED, EXPIRED, INVALID, REVOKED) means a trigger will fail until the connection is restored. Proactive visibility, not a guarantee - status can change between listing and triggering.
curl
curl https://api.flawpilot.com/v1/integration-repos \
  -H "Authorization: Bearer YOUR_API_KEY"

Trigger a code scan

POST/v1/code-scans/trigger

Queues one scan job per requested dimension. Auth required; general limit AND a stricter 10/min trigger limit.

There is no projectId field. The project is resolved server-side from the repo you name, so the two can never disagree.

Request body

FieldTypeRequiredNotes
integrationRepoIdstringYesFrom GET /v1/integration-repos. The repo must be assigned to a project, enabled, and have a branch.
dimensionsarray of SAST | SCA | SECRETS | CODE_QUALITYNoOmitted or [] = all four. One job is created per dimension, sharing one scanRunId.

Response - 201

201 Created
[
  {
    "id": "job-uuid",
    "dimension": "SAST",
    "status": "QUEUED",
    "branch": "main",
    "commitSha": "9f2c1ab...",
    "scanRunId": "run-uuid",
    "repoName": "checkout-service",
    "repoFullName": "acme/checkout-service",
    "createdAt": "2026-09-10T10:00:00.000Z"
  }
]

Keep scanRunId - it is shared by every dimension in this trigger and is what you poll next.

curl
curl -X POST https://api.flawpilot.com/v1/code-scans/trigger \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "integrationRepoId": "a1b2c3d4-...",
    "dimensions": ["SAST", "SECRETS"]
  }'

Code scan status

GET/v1/code-scans/runs/:scanRunId

Progress and scores for every dimension in one run. Auth required; 60/min.

Response - 200

200 OK
{
  "scanRunId": "run-uuid",
  "branch": "main",
  "commitSha": "9f2c1ab...",
  "repoName": "checkout-service",
  "repoFullName": "acme/checkout-service",
  "dimensions": [
    { "dimension": "SAST",    "status": "COMPLETED", "score": 82.5, "findingsCount": 14 },
    { "dimension": "SECRETS", "status": "RUNNING",   "score": null, "findingsCount": 0 }
  ]
}

A run is done when every entry in dimensions has reached a terminal status. score is null until that dimension finishes.

Code scan findings

GET/v1/code-scans/jobs/:id/findings

Finding-level detail for one job. Auth required; 60/min. Note this takes a job id, not a scanRunId.

Query parameters

ParameterNotes
severityCRITICAL | HIGH | MEDIUM | LOW. Omit for all severities.
reviewStatusFilter by triage state.
pageDefaults to 1.
limitDefaults to 10 on this surface.

Results are already sorted by severity, critical first, so the first page is the part worth acting on.

curl
curl "https://api.flawpilot.com/v1/code-scans/jobs/JOB_ID/findings?severity=CRITICAL&limit=25" \
  -H "Authorization: Bearer YOUR_API_KEY"

Trigger a Site Health check

POST/v1/site-health/trigger

Crawls the whole site and scores every reachable page with Lighthouse. Auth required; general limit AND a stricter 8-per-HOUR trigger limit.

This is the one endpoint whose action limit is measured per hour rather than per minute - a full-site crawl is far heavier than a single-URL scan.

Request body

FieldTypeRequiredNotes
projectIdstringYesFrom GET /v1/projects.
urlstringYesThe entry point to crawl from. Include the protocol.
modeDESKTOP | MOBILENoDefaults to DESKTOP. The MCP tool asks for this explicitly; the API keeps the default.
emailstring (valid email)NoSends the report here on completion.

Response - 201

201 Created
{
  "id": "sh-uuid",
  "status": "QUEUED",
  "url": "https://example.com",
  "mode": "DESKTOP",
  "createdAt": "2026-09-10T10:00:00.000Z"
}

Site Health status

GET/v1/site-health/:id

Poll one check. Auth required; 30/min.

Response - 200

200 OK
{
  "id": "sh-uuid",
  "status": "COMPLETED",
  "url": "https://example.com",
  "mode": "DESKTOP",
  "pageCount": 42,
  "errorMessage": null,
  "scorePerformance": 71.2,
  "scoreAccessibility": 88.0,
  "scoreBestPractices": 92.0,
  "scoreSeo": 95.5,
  "scoreOverall": 86.7,
  "expiresAt": "2026-10-10T10:00:00.000Z",
  "createdAt": "2026-09-10T10:00:00.000Z",
  "updatedAt": "2026-09-10T10:06:31.000Z"
}

Scores are null until the crawl finishes. Per-page results, findings and screenshots are dashboard-only - this endpoint returns the top-line scores.

List Site Health checks

GET/v1/projects/:projectId/site-health

Check history for one project, newest first. Auth required; 30/min.

Query parameters

ParameterNotes
statusFilter by run status.
modeDESKTOP | MOBILE.
urlFilter by the crawled URL.
dateFrom / dateToISO dates bounding createdAt.
page / limitDefaults to page 1, limit 10.

Response - 200

200 OK
{
  "items": [ /* same shape as GET /v1/site-health/:id */ ],
  "total": 37,
  "page": 1,
  "limit": 10
}

Featured on

Featured on tinyshelf
Featured on saasfame.com
Featured on toolfame.com
Featured on aitoolfame.com