API

Scan in CI/CD

An endpoint to run a scan and get the result in a single request — built to block a deployment when the security score isn't high enough, before it ships to production.

Authentication

An API key is required (still no user account — a key is just tied to an email, like the rest of Vetora). Generate one. organization and project are optional free-text labels to group your keys; expiresInDays is optional too (omit it for a key that never expires):

curl -s -X POST https://www.vetora.site/api/api-keys \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","label":"CI GitHub Actions","organization":"acme-inc","project":"web-app","expiresInDays":90}'

# → { "apiKey": "vs_live_...", "email": "you@example.com", ... }

The key is shown only once — save it immediately (e.g. as a GitHub Actions secret). Send it on every request:

Authorization: Bearer vs_live_xxx

Every creation also emails a management link for that address — list every key tied to it, revoke one, or rotate one (replace its secret value while keeping the same key identity and usage history) without contacting support.

Endpoints

  • POST /api/api-keys — create a key (no auth required — see above).
  • POST /api/v1/scan — run a scan and get the result (bearer key required).

Endpoint

Limited to 5 scans per key per 10-minute window (no longer per IP — switching networks no longer resets the limit).

POST https://www.vetora.site/api/v1/scan
Content-Type: application/json
Authorization: Bearer vs_live_xxx

{
  "url": "https://your-app.vercel.app",
  "minScore": "B"
}

minScore is optional (A, B, C, D, or F). If present, the response indicates whether the score reached it via passed. Without it, passed is null — the request stays purely informational.

Response

{
  "scanId": "uuid",
  "url": "https://your-app.vercel.app",
  "score": "B",
  "minScore": "B",
  "passed": true,
  "detectedBuilder": "lovable",
  "reportUrl": "https://www.vetora.site/scan/uuid",
  "findings": {
    "base": [
      {
        "title": "Missing security header: ...",
        "severity": "medium",
        "description": "...",
        "fixInstructions": "...",
        "evidence": "HTTP 200\ncontent-type: text/html\ncontent-security-policy: NOT PRESENT",
        "confidence": "confirmed"
      }
    ],
    "premium": {
      "stripeAndSupabaseCount": 2,
      "vibeCodingCount": 1,
      "infrastructureCount": 1,
      "unlockUrl": "https://www.vetora.site/scan/uuid/unlock"
    }
  }
}

Findings in the "Base security" category (headers, HTTPS, cookies, CORS) are returned in full detail — that's the free part of the scan, same as on the site. Stripe & Supabase and vibe coding findings are counted but not detailed: same rule as an unpaid scan on the site, the API doesn't bypass the €49 full report.

detectedBuilder is "bolt", "lovable" or null — automatic, informational detection (not a vulnerability), based on these platforms' public attribution badge. v0 and Cursor aren't detected: no equivalent badge to look for.

Example: blocking a deployment (bash)

SCORE=$(curl -s -X POST https://www.vetora.site/api/v1/scan \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $VETORA_API_KEY" \
  -d '{"url":"https://your-app.vercel.app","minScore":"B"}' \
  | jq -r '.passed')

if [ "$SCORE" != "true" ]; then
  echo "Security score too low — deployment blocked."
  exit 1
fi

Example: GitHub Actions

- name: Vetora security gate
  env:
    VETORA_API_KEY: ${{ secrets.VETORA_API_KEY }}
  run: |
    RESULT=$(curl -s -X POST https://www.vetora.site/api/v1/scan \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer $VETORA_API_KEY" \
      -d '{"url":"${{ steps.deploy.outputs.url }}","minScore":"C"}')
    echo "$RESULT" | jq .
    echo "$RESULT" | jq -e '.passed == true' > /dev/null

Limits to know about

  • A scan usually takes a few seconds to ~30 seconds depending on the site — the request blocks until it finishes, so plan a generous timeout in CI.
  • The scanned site must be publicly reachable at call time — useful after a preview/staging deployment, not before.
  • No dedicated per-organization quota — only the per-key rate limit below. If your usage grows, contact us.

Errors

Every error response has the same shape: { "error": "human-readable message" }. Status codes:

  • 400 — malformed request (missing/invalid url, minScore not one of A/B/C/D/F, invalid email/label on key creation).
  • 401 — missing/malformed Authorization header, or the key is invalid, revoked, or expired.
  • 409 — key creation only: already 5 active keys for that e-mail. Revoke one first.
  • 429 — rate limit exceeded (see below).
  • 500 — internal error, not your fault. Safe to retry.
  • 502 — scan only: the target site was unreachable or timed out. Not a Vetora outage — check the URL is actually publicly reachable.

Rate limits

  • POST /api/v1/scan — 5 scans per API key per rolling 10-minute window.
  • POST /api/api-keys — 3 key creations per IP address per rolling 24-hour window.

A rate-limited request returns 429 with no Retry-After header yet — back off and retry after the stated window.

Webhooks

Vetora doesn't call a webhook for scan results — the API itself is synchronous. The webhook that exists is the opposite direction: a monitored subscription (continuous monitoring) can configure a generic webhook on its management page to receive an alert when a rescan detects a score drop, a regression, or a new critical/high finding — in addition to (or instead of) email and Slack.

Each delivery is a signed POST:

POST <your URL>
Content-Type: application/json
X-Vetora-Signature: <hex HMAC-SHA256 of the raw body>

{
  "event": "security_alert",
  "subscriptionId": "uuid",
  "url": "https://your-app.vercel.app",
  "reportUrl": "https://www.vetora.site/scan/uuid/report",
  "previousScore": "B",
  "currentScore": "D",
  "scoreDropped": true,
  "regressedFindings": [{ "title": "...", "severity": "critical" }],
  "newCriticalFindings": [],
  "fixedCount": 0,
  "timestamp": "2026-08-21T12:00:00.000Z"
}

The signing secret is shown once, when the webhook is first configured — verify every delivery against it (Node.js example):

const crypto = require('crypto');

function isValidSignature(rawBody, signatureHeader, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

Best-effort delivery, no retry queue yet: treat a missed delivery the same way you'd treat a missed email — the report itself (reportUrl) is always the source of truth.

OpenAPI

Full machine-readable spec (requests, responses, error schemas) for both endpoints above, importable into Postman, Insomnia, or any OpenAPI-based client generator:

curl -s https://www.vetora.site/openapi.yaml

/openapi.yaml