Skip to content

Documentation

Everything you need to secure your AI-built projects

REST API

REST API

Queue a scan, get the base report, then follow deep scanners

The BoringSec API is designed for durable automation. Every protected endpoint validates a scoped bsk_ key, enforces the workspace plan, rate limits requests, and returns resource links you can keep instead of guessing URLs.

HTTP 202 durable queue

A production-safe first integration in three requests

Create a scoped key, send async: true, and poll the returned status link. The base report can be ready while authorized Injection, XSS, Ports, Nuclei, ZAP, and Medusa jobs continue independently.

Scoped access

Grant only the scopes used by this integration

Retry guidance

Honor Retry-After instead of aggressive polling

Owned results

Scan reads are checked against the API key owner

Authentication and scopes

Send the API key in the Authorization header on every request. BoringSec stores API keys as SHA-256 hashes. The raw value is shown once when the key is created.

http
Authorization: Bearer bsk_your_key_here

Standard scan automation

Grant scan:write and scan:read. These scopes are available from Pro.

Full data and webhooks

Grant scan:read:full or webhook scopes only when needed. These capabilities require Business or Enterprise.

Treat the raw key like a production secret
Keep it in a server-side secret manager or CI secret. Never put a bsk_ value in browser JavaScript, a mobile bundle, source control, screenshots, or support messages. Revoke and replace any key that may have been exposed.

Quickstart

1. Queue the scan

terminal
curl --request POST 'https://www.boringsec.com/api/v1/scan' \
  --header "Authorization: Bearer ${BORINGSEC_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{"url":"https://example.com","async":true}'

2. Save the returned links

HTTP 202 response
{
  "scan": {
    "id": "cln...",
    "url": "https://example.com/",
    "hostname": "example.com",
    "domainId": "cld...",
    "status": "PENDING",
    "reused": false,
    "links": {
      "status": "/api/v1/scans/cln.../status",
      "result": "/api/v1/scans/cln...",
      "fullResult": "/api/v1/scans/cln.../full"
    }
  },
  "message": "Scan queued. Poll the status endpoint for progress and results."
}

3. Poll until base and deep work are terminal

Node.js
const origin = 'https://www.boringsec.com'
const headers = { Authorization: `Bearer ${process.env.BORINGSEC_API_KEY}` }

let statusPath = queued.scan.links.status

while (true) {
  const response = await fetch(new URL(statusPath, origin), { headers })
  if (!response.ok) throw new Error(`Status request failed: ${response.status}`)

  const { scan } = await response.json()
  console.log(scan.status, scan.progress, scan.deepScan?.status ?? 'base scan only')

  if (scan.status === 'FAILED') throw new Error(scan.error ?? 'Scan failed')
  if (scan.status === 'COMPLETED' && scan.deepScan?.active !== true) break

  const seconds = Math.max(1, Number(response.headers.get('Retry-After') ?? 5))
  await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
Do not restart a scan because a deep job is still running
A base COMPLETED status means the first report is ready. Continue polling while deepScan.active is true. Stop when the deep state is terminal, or handle authorization_required by verifying an owned domain before starting a new scan.

Scan lifecycle

1

Queue

POST /api/v1/scan returns HTTP 202 and durable links.

2

Base scan

PENDING and SCANNING expose progress and an estimated wait.

3

Report ready

COMPLETED exposes score, grade, and result links when assessable.

4

Heavy scan

Authorized Injection, XSS, Ports, Nuclei, ZAP, and Medusa jobs finish independently.

Scanning endpoints

POST/api/v1/scan

Set async to true for the recommended HTTP 202 queue contract. Clients that omit async use a bounded legacy compatibility wait and should migrate to explicit polling.

Pro+Scope: scan:write + scan:read for async
Request and response example

Request body

json
{ "url": "https://example.com", "async": true }

Response

json
{
  "scan": {
    "id": "cln...",
    "url": "https://example.com/",
    "hostname": "example.com",
    "domainId": "cld...",
    "status": "PENDING",
    "reused": false,
    "links": {
      "status": "/api/v1/scans/cln.../status",
      "result": "/api/v1/scans/cln...",
      "fullResult": "/api/v1/scans/cln.../full"
    }
  },
  "message": "Scan queued. Poll the status endpoint for progress and results."
}
POST/api/v1/scans/async

Queue a scan with an optional SSRF-validated webhook URL and request metadata. Use this endpoint when the callback is part of the integration.

Business+Scope: scan:write + scan:read
Request and response example

Request body

json
{
  "url": "https://example.com",
  "webhookUrl": "https://your-app.example/boringsec-webhook",
  "metadata": { "build": "2026.07.14" }
}

Results and deep-scanner status

GET/api/v1/scans/{id}/status

Poll the exact links.status value returned by the queue response. Honor Retry-After. A legacy scan may omit deepScan instead of inventing a not-requested state.

Pro+Scope: scan:read
Request and response example

Response

json
{
  "scan": {
    "id": "cln...",
    "status": "COMPLETED",
    "progress": 100,
    "score": null,
    "grade": null,
    "links": {
      "status": "/api/v1/scans/cln.../status",
      "result": "/api/v1/scans/cln...",
      "fullResult": "/api/v1/scans/cln.../full"
    },
    "deepScan": {
      "status": "running",
      "active": true,
      "terminal": false,
      "jobs": [
        { "category": "injection", "status": "completed", "attemptCount": 1, "maxAttempts": 3 },
        { "category": "xss", "status": "running", "attemptCount": 1, "maxAttempts": 3 },
        { "category": "ports", "status": "completed", "attemptCount": 1, "maxAttempts": 3 },
        { "category": "nuclei", "status": "running", "attemptCount": 1, "maxAttempts": 3 },
        { "category": "zap", "status": "retry_scheduled", "attemptCount": 1, "maxAttempts": 3 },
        { "category": "medusa", "status": "queued", "attemptCount": 0, "maxAttempts": 3 }
      ]
    }
  }
}
Scores are final only after assessable coverage settles
While deepScan.active is true, clients must treat a null score and grade as unavailable, not as zero or a passing result. Keep showing completed findings and each of the six heavy-job states while polling.
GET/api/v1/scans/{id}

Load the owned scan result and its normalized findings after the base scan is ready.

Pro+Scope: scan:read
GET/api/v1/scans/{id}/full

Load complete owned scan data, including sanitized scanner metadata and full report fields.

Business+Scope: scan:read:full
GET/api/v1/scans/{id}/compliance/{framework}

Generate an evidence mapping. Business includes PCI_DSS_4, GDPR, and SOC2. Enterprise adds HIPAA and ISO27001. Missing, partial, or unavailable scanner evidence remains an explicit coverage gap and never becomes a pass. This is a technical mapping, not certification.

Business+Scope: compliance:read

Developer tools

GET/api/v1/usage

Read the current plan, period, scan and domain usage, API rate-limit window, feature flags, and extra-usage pricing.

Pro+Scope: scan:read
Request and response example

Response

json
{
  "tier": "PRO",
  "scans": { "used": 4, "limit": 10, "remaining": 6, "unlimited": false },
  "apiCalls": { "limit": 100, "remaining": 87, "unlimited": false },
  "features": { "monitoring": true, "fixPrompts": true, "compliance": false }
}
GET/api/v1/fix-suggestions/{issueCode}

Load remediation prompts for a supported issue code and adapt them to an AI coding workflow.

Pro+Scope: scan:read
POST/api/v1/generate-rules

Generate .cursorrules, AGENTS.md, or both from the selected stack and optional owned scan evidence.

Pro+Scope: scan:read
Request and response example

Request body

json
{
  "format": "both",
  "stack": "nextjs-supabase",
  "scanId": "cln..."
}

Domains

GET/api/v1/domains

List domains owned by the API key user, including verification and monitoring state.

Pro+Scope: domain:read
POST/api/v1/domains

Add an SSRF-validated public HTTPS target and receive the domain verification token.

Pro+Scope: domain:write
Request and response example

Request body

json
{ "url": "https://example.com" }
GET/api/v1/domains/{id}/history

Read paginated scan history for an owned domain.

Business+Scope: history:read

Webhooks

GET/api/v1/webhooks

List webhook endpoints owned by the current API key user.

Business+Scope: webhook:read
POST/api/v1/webhooks

Create an SSRF-validated webhook for selected events. The signing secret is returned only when the endpoint is created.

Business+Scope: webhook:write
Request and response example

Request body

json
{
  "url": "https://your-app.example/webhook",
  "events": ["SCAN_COMPLETED", "SCAN_FAILED", "ISSUE_NEW_CRITICAL"],
  "maxRetries": 3,
  "isActive": true
}
Base completion does not promise deep completion
SCAN_COMPLETED means the base report is ready. Use the scan status endpoint to decide whether independent deep jobs are still active.

Errors, retries, and rate limits

Stable error envelope

Handle the HTTP status and machine-readable errorCode. Do not parse the human message for control flow.

HTTP 400
{
  "error": "Validation failed",
  "code": 400,
  "errorCode": "SCAN_VALIDATION_FAILED"
}

Rate-limit headers

Read the remaining quota and Unix reset time from every rate-limited response. A 429 response also includes Retry-After.

http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1783998000
Retry-After: 5
400

Fix the payload or URL before retrying.

401

Replace the missing, invalid, expired, or revoked key.

403

Add the required scope, verify ownership, or use a plan with that capability.

404

Treat the resource as absent. Owned-resource endpoints do not reveal another user’s data.

429

Wait for Retry-After and add jitter before the next request.

500–503

Retry idempotent reads with bounded exponential backoff. Keep the original scan ID.

Ship the first API integration with a scoped key

Start with one test domain, log the returned scan ID, honor Retry-After, and add the full-result or webhook scope only when the integration needs it.