Vulnerability Vines AI — REST API

Developers Guide
Vines JSON Graphs
Base URL: https://vines.rosebird.org
Auth: Bearer token in Authorization header: Authorization: Bearer <YOUR_TOKEN>
Generate your token in the Vines portal → Profile > API Tokens.

Contents

Quickstart Authentication Endpoints KPIs & CVSS Sample Responses CI/CD Gates CI/CD Examples SDK Snippets OpenAPI Mini-Spec Rate Limits & Security Errors Best Practices Try It — Live

Quickstart

Start a scan

curl -sS -X POST https://vines.rosebird.org/api/v1/scans \
  -H "Authorization: Bearer <YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"target":"https://example.com","scan_type":"both"}'
# → {"status":"success","scan_id":"...","target":"https://example.com"}

Python

import requests
BASE = "https://vines.rosebird.org"
TOKEN = "YOUR_TOKEN"
h = {"Authorization": f"Bearer {TOKEN}"}
payload = {"target": "https://example.com", "scan_type": "both"}
r = requests.post(f"{BASE}/api/v1/scans", json=payload, headers=h)
print(r.json())  # {"status":"success","scan_id":"...","target":"https://example.com"}

Wait for completion

SCAN="<scan_id_from_previous_step>"
while true; do
  curl -sS "https://vines.rosebird.org/api/v1/scan/$SCAN/status" \
    -H "Authorization: Bearer <YOUR_TOKEN>" | jq .
  sleep 3
done

Python

import time, requests
BASE = "https://vines.rosebird.org"
TOKEN = "YOUR_TOKEN"
SCAN = "<scan_id_from_previous_step>"
h = {"Authorization": f"Bearer {TOKEN}"}
while True:
    r = requests.get(f"{BASE}/api/v1/scan/{SCAN}/status", headers=h)
    print(r.json())
    time.sleep(3)

Fetch report (JSON)

curl -sS "https://vines.rosebird.org/api/v1/report/$SCAN/json" \
  -H "Authorization: Bearer <YOUR_TOKEN>" -o report.json

Python

import requests, json
BASE = "https://vines.rosebird.org"
TOKEN = "YOUR_TOKEN"
SCAN = "<scan_id_from_previous_step>"
h = {"Authorization": f"Bearer {TOKEN}"}
r = requests.get(f"{BASE}/api/v1/report/{SCAN}/json", headers=h)
open("report.json","w").write(json.dumps(r.json(), indent=2))

Fetch report (PDF)

curl -L "https://vines.rosebird.org/api/v1/report/$SCAN/pdf" \
  -H "Authorization: Bearer <YOUR_TOKEN>" -o report.pdf

Python

import requests
BASE = "https://vines.rosebird.org"
TOKEN = "YOUR_TOKEN"
SCAN = "<scan_id_from_previous_step>"
h = {"Authorization": f"Bearer {TOKEN}"}
r = requests.get(f"{BASE}/api/v1/report/{SCAN}/pdf", headers=h)
open("report.pdf","wb").write(r.content)

Authentication

All API calls require a Bearer token:

Authorization: Bearer <YOUR_TOKEN>

Get your token in Profile → API Tokens. Store it in CI/CD secret managers (GitHub Secrets, GitLab Variables, Jenkins Credentials, Azure Library, CircleCI Contexts, etc.).

Endpoints

POST /api/v1/scans

Description: Start a scan. Respects plan & capacity limits.

Request body

FieldTypeReqDescription
targetstringyesURL or host (e.g., https://example.com)
scan_typestringnonmap | zap | both (default)

curl

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
curl -sS -X POST "$BASE/api/v1/scans" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target":"https://example.com","scan_type":"both"}'

sample response

{"status":"success","scan_id":"9106bad7-fda6-464e-980f-3d7154409a69","target":"https://example.com"}
GET /api/v1/reports ?limit=1..100 (default 25)

Description: List your scans (id, target, date, status, counts, ports).

curl

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
curl -sS "$BASE/api/v1/reports?limit=10" \
  -H "Authorization: Bearer $TOKEN" | jq .

sample response (trimmed)

[
  {
    "scan_id": "9106bad7-fda6-464e-980f-3d7154409a69",
    "target": "https://niles.rosebird.org",
    "scan_date": "2025-08-26T05:46:40Z",
    "status": "completed",
    "vuln_count": 9,
    "open_ports": 4,
    "max_risk_score": 5.0
  }
]
GET /api/v1/scan/<scan_id>/status

Description: Poll current scan status.

curl

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"
curl -sS "$BASE/api/v1/scan/$SCAN_ID/status" \
  -H "Authorization: Bearer $TOKEN" | jq .

sample response

{"status":"running","message":"Web scan (quick)…","progress":55,"target":"https://example.com"}
GET /api/v1/scans/active

Description: Snapshot of your initializing/running scans.

curl

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
curl -sS "$BASE/api/v1/scans/active" -H "Authorization: Bearer $TOKEN" | jq .

sample response

[]
POST /api/v1/scan/<scan_id>/abort

Description: Abort a running scan (idempotent).

curl

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"
curl -sS -X POST "$BASE/api/v1/scan/$SCAN_ID/abort" \
  -H "Authorization: Bearer $TOKEN" | jq .

sample response

{"ok":true}
GET /api/v1/report/<scan_id>/json

Description: Full report JSON (scan_info, open_ports, vulnerabilities, risk_scores).

curl

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"
curl -sS "$BASE/api/v1/report/$SCAN_ID/json" \
  -H "Authorization: Bearer $TOKEN" | jq .

sample response (trimmed)

{
  "scan_info": {
    "scan_id": "9106bad7-fda6-464e-980f-3d7154409a69",
    "target": "https://niles.rosebird.org",
    "scan_date": "2025-08-26 05:46:40",
    "status": "completed"
  },
  "open_ports": [
    {"port":80,"protocol":"tcp","service":"http","product":"nginx","version":"1.23.x"},
    {"port":443,"protocol":"tcp","service":"https","product":"nginx","version":"1.23.x"}
  ],
  "vulnerabilities": [
    {
      "source":"zap",
      "name":"Content Security Policy (CSP) Header Not Set",
      "severity":"medium",
      "cvss_score":5.0,
      "details":{"cwe_id":"CWE-693","url":"https://niles.rosebird.org/","instances":[{"url":"https://niles.rosebird.org/"}]}
    }
  ],
  "risk_scores": { "max":5.0, "overall_score":3.26,
    "summary":{"critical":0,"high":0,"medium":2,"low":3,"info":4} }
}
GET /api/v1/report/<scan_id>/pdf

Description: PDF report stream.

curl

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"
curl -sSL "$BASE/api/v1/report/$SCAN_ID/pdf" \
  -H "Authorization: Bearer $TOKEN" -o report.pdf

headers

Content-Type: application/pdf
Content-Disposition: inline; filename="Vulnerability_Vines_YYYY-MM-DD.pdf"

KPIs & CVSS

GET /api/v1/scan/<scan_id>/kpi

Description: KPIs for a single scan (same JSON shape as /kpi/latest), filtered by scan_id.

curl examples

# Env-based (recommended)
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"

curl -sS -H "Authorization: Bearer $TOKEN" \
  "$BASE/api/v1/scan/$SCAN_ID/kpi" | jq .

# One-liner (shows HTTP code and avoids jq errors on HTML)
curl -sS -w "\nHTTP %{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  "$BASE/api/v1/scan/$SCAN_ID/kpi" | (jq . 2>/dev/null || cat)

sample response (trimmed)

{
  "cvss": { "max": 5.0, "mean": 2.56 },
  "risk_scores": { "max": 5.0, "overall_score": 3.26,
    "summary": { "critical": 0, "high": 0, "medium": 2, "low": 3, "info": 4 } },
  "scan_id": "9106bad7-fda6-464e-980f-3d7154409a69",
  "target": "https://niles.rosebird.org",
  "scan_date": "2025-08-26 05:46:40",
  "totals": { "vulnerabilities": 9, "open_ports": 4 }
}
GET /api/v1/kpi/latest ?limit=1..50 (default 1)

Description: Latest N scans’ KPIs for the authenticated user.

curl examples

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"

# Latest scan KPI
curl -sS -H "Authorization: Bearer $TOKEN" \
  "$BASE/api/v1/kpi/latest" | jq .

# Latest 10 scan KPIs
curl -sS -H "Authorization: Bearer $TOKEN" \
  "$BASE/api/v1/kpi/latest?limit=10" | jq .

KPI + Vulnerability Titles

GET /api/v1/scan/<scan_id>/kpi+titles

Description: Returns KPIs (counts, CVSS, risk summary) for a single scan, plus a lightweight list of vulnerabilities with title and severity. No sensitive details or evidence are included.

curl example

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"

curl -sS -H "Authorization: Bearer $TOKEN" \
  "$BASE/api/v1/scan/$SCAN_ID/kpi+titles" | jq .

Sample Response

{
  "cvss": { "max": 5.0, "mean": 2.56 },
  "risk_scores": {
    "max": 5.0,
    "overall_score": 3.26,
    "summary": { "critical": 0, "high": 0, "info": 4, "low": 3, "medium": 2 }
  },
  "scan_date": "2025-08-26 05:46:40",
  "scan_id": "9106bad7-fda6-464e-980f-3d7154409a69",
  "target": "https://niles.rosebird.org",
  "totals": {
    "critical": 0,
    "high": 0,
    "info": 4,
    "low": 3,
    "medium": 2,
    "open_ports": 4,
    "vulnerabilities": 9
  },
  "vulnerabilities": [
    { "severity": "medium", "title": "Content Security Policy (CSP) Header Not Set" },
    { "severity": "medium", "title": "Missing Anti-clickjacking Header" },
    { "severity": "low", "title": "Strict-Transport-Security Header Not Set" },
    { "severity": "low", "title": "Timestamp Disclosure - Unix" },
    { "severity": "low", "title": "X-Content-Type-Options Header Missing" },
    { "severity": "info", "title": "Information Disclosure - Suspicious Comments" },
    { "severity": "info", "title": "Modern Web Application" },
    { "severity": "info", "title": "Re-examine Cache-control Directives" },
    { "severity": "info", "title": "User Agent Fuzzer" }
  ]
}

Json Sample Responses

Report JSON (trimmed)

Click to expand JSON
{
  "scan_info": {
    "scan_id": "9106bad7-fda6-464e-980f-3d7154409a69",
    "target": "https://niles.rosebird.org",
    "scan_date": "2025-08-26 05:46:40",
    "has_zap": true,
    "is_nmap_only": false
  },
  "open_ports": [
    {"port":80,"protocol":"tcp","service":"http","product":"nginx","version":"1.23.x"},
    {"port":443,"protocol":"tcp","service":"https","product":"nginx","version":"1.23.x"}
  ],
  "vulnerabilities": [
    {
      "source":"zap",
      "name":"Content Security Policy (CSP) Header Not Set",
      "severity":"medium",
      "cvss_score":5.0,
      "details":{
        "cwe_id":"CWE-693",
        "url":"https://niles.rosebird.org/",
        "instances":[{"url":"https://niles.rosebird.org/"}]
      }
    }
  ],
  "risk_scores": {
    "max":5.0,
    "overall_score":3.26,
    "summary":{"critical":0,"high":0,"medium":2,"low":3,"info":4}
  }
}

Report PDF

Headers: Content-Type: application/pdf, Content-Disposition: inline; filename="Vulnerability_Vines_YYYY-MM-DD.pdf"

curl

curl -L "https://vines.rosebird.org/api/v1/report/$SCAN/pdf" \
  -H "Authorization: Bearer <YOUR_TOKEN>" \
  -o report.pdf

Python

import requests
h = {"Authorization": "Bearer YOUR_TOKEN"}
r = requests.get(f"https://vines.rosebird.org/api/v1/report/{SCAN}/pdf", headers=h)
with open("report.pdf","wb") as f: f.write(r.content)

Node

import fs from 'node:fs';
const res = await fetch(`${BASE}/api/v1/report/${SCAN}/pdf`, {
  headers: { Authorization: `Bearer ${TOKEN}` }
});
const buf = Buffer.from(await res.arrayBuffer());
fs.writeFileSync('report.pdf', buf);

CI/CD Gates

GET /api/v1/scan/<scan_id>/gate

Description: Returns pass/fail (ok) and reasons, based on query thresholds.

Typical policies

EnvironmentQuery
Prod strict?max_critical=0&max_high=0&max_cvss=8.9
Staging?max_critical=0&max_high=2&max_cvss=9.4
Hardened surface?max_open_ports=0
GET /api/v1/kpi/latest/gate

Description: Gate on the latest scan for your token’s user. Same query params.

CI/CD Integration Examples

Store your token in secrets. Each example exits non-zero when the gate fails.

Generic bash

RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN/gate?max_critical=0&max_cvss=8.9" \
  -H "Authorization: Bearer $TOKEN")
echo "$RESP" | jq .
if [ "$(echo "$RESP" | jq -r '.ok')" != "true" ]; then
  echo "Gate failed: $(echo "$RESP" | jq -r '.reasons | join(\"; \")')" >&2
  exit 1
fi

GitHub Actions

- name: Gate latest scan
  env: { BASE: https://vines.rosebird.org, VINES_TOKEN: ${{ secrets.VINES_TOKEN }}, SCAN_ID: ${{ vars.VINES_SCAN_ID }} }
  run: |
    RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
    echo "$RESP" | jq .
    test "$(echo "$RESP" | jq -r '.ok')" = "true"

GitLab CI

vines_gate:
  image: alpine:latest
  stage: test
  before_script: [ 'apk add --no-cache curl jq' ]
  script:
    - RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
    - echo "$RESP" | jq .
    - test "$(echo "$RESP" | jq -r '.ok')" = "true"

Jenkins Full CI/CD

sh '''
set -e
# Deps: curl, jq
# Env you can set at the job or pipeline level:
#   BASE         (default: https://vines.rosebird.org)
#   TARGET       (e.g., https://example.com)
#   VINES_TOKEN  (store as Jenkins Secret Text and inject to env)
#   MAX_CVSS     (default: 8.9)
#   MAX_CRITICAL (default: 0)

: "${BASE:=https://vines.rosebird.org}"
: "${TARGET:?Set TARGET (e.g., https://example.com)}"
: "${VINES_TOKEN:?Set VINES_TOKEN in Jenkins credentials}"
MAX_CVSS="${MAX_CVSS:-8.9}"
MAX_CRITICAL="${MAX_CRITICAL:-0}"

echo "== Start scan → $TARGET"
RESP=$(curl -sS -X POST "$BASE/api/v1/scans" \
  -H "Authorization: Bearer $VINES_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"target\":\"$TARGET\",\"scan_type\":\"both\"}")
echo "$RESP" | jq .
SCAN_ID=$(echo "$RESP" | jq -r .scan_id)
test -n "$SCAN_ID" || { echo "Failed to obtain scan_id"; exit 1; }
echo "scan_id=$SCAN_ID"

echo "== Poll status"
while true; do
  SRESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/status" \
    -H "Authorization: Bearer $VINES_TOKEN")
  STATUS=$(echo "$SRESP" | jq -r .status)
  PROG=$(echo "$SRESP" | jq -r '.progress // "n/a"')
  echo "status=$STATUS progress=$PROG"
  case "$STATUS" in
    completed|failed|aborted) break ;;
    *) sleep 3 ;;
  esac
done
[ "$STATUS" = "completed" ] || { echo "Scan did not complete successfully: $STATUS"; exit 1; }

echo "== KPI (informational)"
KPI=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/kpi" \
  -H "Authorization: Bearer $VINES_TOKEN")
echo "$KPI" | jq .

echo "== Gate (policy)"
GATE=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=${MAX_CRITICAL}&max_cvss=${MAX_CVSS}" \
  -H "Authorization: Bearer $VINES_TOKEN")
echo "$GATE" | jq .
test "$(echo "$GATE" | jq -r .ok)" = "true" || { echo "Gate failed"; exit 1; }

echo "== Download PDF"
curl -sSL "$BASE/api/v1/report/$SCAN_ID/pdf" \
  -H "Authorization: Bearer $VINES_TOKEN" -o report.pdf
test -s report.pdf && echo "Saved report.pdf"
'''

Azure Pipelines

- task: Bash@3
  env: { BASE: https://vines.rosebird.org, VINES_TOKEN: $(VINES_TOKEN), SCAN_ID: $(SCAN_ID) }
  inputs:
    targetType: 'inline'
    script: |
      RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
      echo "$RESP" | jq .
      if [ "$(echo "$RESP" | jq -r '.ok')" != "true" ]; then
        echo "##vso[task.logissue type=error]$(echo "$RESP" | jq -r '.reasons | join(\"; \")')"
        exit 1
      fi

Jenkins Full CI/CD

sh '''
# ... your Jenkins shell script ...
'''
📖 Read here about Vines AI Jenkins Plugin

CircleCI

version: 2.1
jobs:
  vines_gate:
    docker: [{ image: cimg/base:stable }]
    steps:
      - checkout
      - run: sudo apt-get update && sudo apt-get install -y jq
      - run: |
          RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
          echo "$RESP" | jq .
          test "$(echo "$RESP" | jq -r '.ok')" = "true"
workflows:
  gate:
    jobs: [vines_gate]

Google Cloud Build

steps:
- name: gcr.io/cloud-builders/curl
  entrypoint: 'bash'
  args:
    - -c
    - |
      apk add --no-cache jq || true
      RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
      echo "$RESP" | jq .
      test "$(echo "$RESP" | jq -r '.ok')" = "true"

Bitbucket Pipelines

pipelines:
  default:
    - step:
        image: alpine:latest
        script:
          - apk add --no-cache curl jq
          - RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
          - echo "$RESP" | jq .
          - test "$(echo "$RESP" | jq -r '.ok')" = "true"

SDK Snippets

Python

import os
import requests

BASE = "https://vines.rosebird.org"
TOKEN = os.environ["VINES_TOKEN"]
H = {"Authorization": f"Bearer {TOKEN}"}

def start_scan(target, scan_type="both"):
    r = requests.post(f"{BASE}/api/v1/scans",
                      json={"target": target, "scan_type": scan_type},
                      headers=H)
    r.raise_for_status()
    return r.json()

def status(scan_id):
    r = requests.get(f"{BASE}/api/v1/scan/{scan_id}/status", headers=H)
    r.raise_for_status()
    return r.json()

def report_json(scan_id):
    r = requests.get(f"{BASE}/api/v1/report/{scan_id}/json", headers=H)
    r.raise_for_status()
    return r.json()

def report_pdf(scan_id, path="report.pdf"):
    r = requests.get(f"{BASE}/api/v1/report/{scan_id}/pdf", headers=H)
    r.raise_for_status()
    with open(path, "wb") as f:
        f.write(r.content)

def gate(scan_id, **policy):
    q = "&".join(f"{k}={v}" for k, v in policy.items())
    r = requests.get(f"{BASE}/api/v1/scan/{scan_id}/gate?{q}", headers=H)
    r.raise_for_status()
    return r.json()

Node (ESM)

import { writeFileSync } from "node:fs";

const BASE = "https://vines.rosebird.org";
const TOKEN = process.env.VINES_TOKEN;

const H = {
  Authorization: `Bearer ${TOKEN}`,
  "Content-Type": "application/json",
};

export async function startScan(target, scan_type = "both") {
  const res = await fetch(`${BASE}/api/v1/scans`, {
    method: "POST",
    headers: H,
    body: JSON.stringify({ target, scan_type }),
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

export async function status(id) {
  const res = await fetch(`${BASE}/api/v1/scan/${id}/status`, {
    headers: { Authorization: H.Authorization },
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

export async function reportJson(id) {
  const res = await fetch(`${BASE}/api/v1/report/${id}/json`, {
    headers: { Authorization: H.Authorization },
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

export async function reportPdf(id, path = "report.pdf") {
  const res = await fetch(`${BASE}/api/v1/report/${id}/pdf`, {
    headers: { Authorization: H.Authorization },
  });
  if (!res.ok) throw new Error(await res.text());
  const buf = Buffer.from(await res.arrayBuffer());
  writeFileSync(path, buf);
}

export async function gate(id, policy = {}) {
  const q = new URLSearchParams(policy).toString();
  const res = await fetch(`${BASE}/api/v1/scan/${id}/gate?${q}`, {
    headers: { Authorization: H.Authorization },
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

OpenAPI Mini-Spec (importable)

Show YAML
openapi: 3.0.0
info: { title: Vulnerability Vines AI API, version: "1.0" }
servers: [ { url: https://vines.rosebird.org } ]
components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
security: [ { bearerAuth: [] } ]
paths:
  /api/v1/scans:
    post:
      summary: Start a scan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                target: { type: string }
                scan_type: { type: string, enum: [nmap, zap, both] }
              required: [target]
      responses:
        "200": { description: OK }
  /api/v1/scan/{scan_id}/status:
    get:
      summary: Scan status
      parameters: [ { name: scan_id, in: path, required: true, schema: { type: string } } ]
      responses: { "200": { description: OK } }
  /api/v1/scan/{scan_id}/abort:
    post:
      summary: Abort scan
      parameters: [ { name: scan_id, in: path, required: true, schema: { type: string } } ]
      responses: { "200": { description: OK } }
  /api/v1/report/{scan_id}/json:
    get:
      summary: Report JSON
      parameters: [ { name: scan_id, in: path, required: true, schema: { type: string } } ]
      responses: { "200": { description: OK } }
  /api/v1/report/{scan_id}/pdf:
    get:
      summary: Report PDF
      parameters: [ { name: scan_id, in: path, required: true, schema: { type: string } } ]
      responses: { "200": { description: PDF stream } }
  /api/v1/kpi/latest:
    get:
      summary: Latest KPIs
      parameters: [ { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 50, default: 1 } } ]
      responses: { "200": { description: OK } }
  /api/v1/scan/{scan_id}/kpi:
    get:
      summary: Single-scan KPI
      parameters: [ { name: scan_id, in: path, required: true, schema: { type: string } } ]
      responses: { "200": { description: OK } }
  /api/v1/scan/{scan_id}/gate:
    get:
      summary: Gate (pass/fail)
      parameters:
        - { name: scan_id, in: path, required: true, schema: { type: string } }
        - { name: max_cvss, in: query, schema: { type: number } }
        - { name: max_critical, in: query, schema: { type: integer } }
        - { name: max_high, in: query, schema: { type: integer } }
        - { name: max_medium, in: query, schema: { type: integer } }
        - { name: max_low, in: query, schema: { type: integer } }
        - { name: max_info, in: query, schema: { type: integer } }
        - { name: max_total, in: query, schema: { type: integer } }
        - { name: max_open_ports, in: query, schema: { type: integer } }
      responses: { "200": { description: OK } }

Tip: copy the YAML into Postman/Insomnia to generate a workspace quickly.

Rate Limits & Security

Errors

HTTPBodyMeaning
401{"error":"missing_token"|"invalid_token"}Token header missing/invalid
403{"error":"subscription_required"}Subscription inactive/expired
403{"error":"plan_limit_active"|"plan_limit_scans"}Plan constraints
404{"error":"not_found"|"not_owner"}Not found / no ownership
429{"error":"capacity"}Global capacity full

Policy Gates & CI Fail Rules

The /api/v1/scan/{scan_id}/gate endpoint enforces severity and CVSS thresholds. It returns {"ok":true|false,"reasons":[...]} based on query parameters. CI pipelines should call this endpoint to fail builds if limits are exceeded.

Supported parameters

  • max_cvss (float 0.0–10.0)
  • max_critical, max_high, max_medium, max_low, max_info (integers)
  • max_total (all severities combined)
  • max_open_ports (network exposures)

Interpretation: max_high=5 allows up to 5 High; fails on 6+. max_critical=0 disallows any Critical.

Common policies

  • Fail if any Critical: ?max_critical=0
  • Fail if High > 5: ?max_high=5
  • Strict Prod: ?max_critical=0&max_high=0&max_cvss=8.9
  • Relaxed Staging: ?max_critical=0&max_high=2&max_cvss=9.4
  • Fail if more than 20 issues: ?max_total=20
  • Zero open ports: ?max_open_ports=0

Curl example

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN="$SCAN_ID"

# Example: fail if Critical>0, High>5, or CVSS>8.9
Q="max_critical=0&max_high=5&max_cvss=8.9"
RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN/gate?$Q" -H "Authorization: Bearer $TOKEN")
echo "$RESP" | jq .
test "$(echo "$RESP" | jq -r .ok)" = "true"

Jenkins KPI Checks

sh '''
set -e
: "${BASE:=https://vines.rosebird.org}"
: "${VINES_TOKEN:?Missing token}"
: "${MAX_CRITICAL:=0}"
: "${MAX_HIGH:=5}"
: "${MAX_CVSS:=8.9}"
SID=$(cat .scan.id)

Q="max_critical=${MAX_CRITICAL}&max_high=${MAX_HIGH}&max_cvss=${MAX_CVSS}"
echo "== Gate with $Q"
RESP=$(curl -sS "$BASE/api/v1/scan/$SID/gate?$Q" -H "Authorization: Bearer $VINES_TOKEN")
echo "$RESP" | jq .
if [ "$(echo "$RESP" | jq -r .ok)" != "true" ]; then
  echo "Gate failed: $(echo "$RESP" | jq -r '.reasons | join("; ")')"
  exit 1
fi
'''

GitHub Actions step

- name: Gate policy
  env:
    BASE: https://vines.rosebird.org
    VINES_TOKEN: ${{ secrets.VINES_TOKEN }}
    MAX_CRITICAL: 0
    MAX_HIGH: 5
    MAX_CVSS: 8.9
  run: |
    SID=$(cat .scan.id)
    Q="max_critical=${MAX_CRITICAL}&max_high=${MAX_HIGH}&max_cvss=${MAX_CVSS}"
    R=$(curl -sS "$BASE/api/v1/scan/$SID/gate?$Q" -H "Authorization: Bearer $VINES_TOKEN")
    echo "$R" | jq .
    test "$(echo "$R" | jq -r .ok)" = "true"

GitLab CI fragment

SID=$(cat .scan.id)
Q="max_critical=0&max_high=5&max_cvss=8.9"
R=$(curl -sS "$BASE/api/v1/scan/$SID/gate?$Q" -H "Authorization: Bearer $VINES_TOKEN")
echo "$R" | jq .
test "$(echo "$R" | jq -r .ok)" = "true"

Tips

  • Keep prod strict (no Critical/High, cap CVSS 8.9); allow relaxed gates in staging.
  • Always echo reasons for developer feedback.
  • Set thresholds via environment vars so each branch/CI job can differ.
  • Use Idempotency-Key on POST /scans in CI retries to avoid duplicate scans.

Best Practices

Pagination & Filtering

Endpoints that return lists (e.g., /api/v1/reports, /api/v1/kpi/latest) support cursor-based pagination.

Query params

ParamTypeDefaultDescription
limitinteger (1–100)25Max items to return.
cursorstringOpaque token from the previous response’s next_cursor.
targetstringFilter reports whose target contains this substring.
statusenuminitializing|running|completed|aborted|failed.
sinceISO-8601Only items with scan_date ≥ this timestamp (UTC).
untilISO-8601Only items with scan_date ≤ this timestamp (UTC).
sortstringscan_dateField to sort by.
orderenumdescasc|desc.

Example

curl -sS "$BASE/api/v1/reports?limit=10&status=completed&since=2025-08-01T00:00:00Z" \
  -H "Authorization: Bearer $VINES_TOKEN"

Response shape

{
  "items": [ /* ... */ ],
  "next_cursor": "eyJvZmZzZXQiOjEwfQ"  // present only if more data is available
}

Rate Limits

All responses include standard rate-limit headers. Plans may vary.

HeaderMeaning
X-RateLimit-LimitMax requests permitted in the current window.
X-RateLimit-RemainingRemaining requests in the current window.
X-RateLimit-ResetUnix epoch seconds when the window resets.
Retry-AfterSeconds to wait before retrying (only on 429).

Exponential backoff (bash)

for d in 1 2 4 8 16; do
  code=$(curl -s -w "%{http_code}" -o .out "$URL" -H "Authorization: Bearer $VINES_TOKEN" | tail -c 3)
  if [ "$code" -lt 500 ] && [ "$code" != "429" ]; then break; fi
  sleep "$d"
done
cat .out

Errors — Canonical Schema

We always return JSON on errors with a stable shape:

{
  "error": "plan_limit_active",
  "message": "Concurrent scan limit reached",
  "request_id": "req_9zFQ",
  "hint": "Wait for active scans to finish or upgrade plan."
}

Common error codes

errorHTTPMeaning
missing_token / invalid_token401Bearer token missing/invalid/expired.
subscription_required403Subscription inactive/expired.
plan_limit_active403Concurrent scan limit reached.
plan_limit_scans403Retention exceeded for your plan.
not_found404Resource not found.
not_owner404Resource exists but not owned by token.
capacity429Global capacity saturated; back off and retry.

Idempotency

Recommended for POST /api/v1/scans. Send an Idempotency-Key header (e.g., a UUID). Retries with the same key will not create duplicate scans.

curl -sS -X POST "$BASE/api/v1/scans" \
  -H "Authorization: Bearer $VINES_TOKEN" \
  -H "Idempotency-Key: 9106bad7-fda6-464e-980f-3d7154409a69" \
  -H "Content-Type: application/json" \
  -d '{"target":"https://example.com","scan_type":"both"}'

Stable Headers You Can Rely On

HeaderEndpointsNotes
X-Request-IdAllInclude this in support tickets.
ETag/api/v1/report/{id}/jsonUse for cache validation (If-None-Match).
Content-Disposition/api/v1/report/{id}/pdfinline; filename="Vulnerability_Vines_YYYY-MM-DD.pdf"

Field Conventions

  • Timestamps: ISO-8601 UTC (e.g., 2025-08-26T05:46:40Z).
  • IDs: UUID v4 strings unless specified.
  • Enums: documented per endpoint (e.g., status, scan_type).
  • Scores: CVSS is a number 0.0–10.0 (double).

Versioning, Deprecations & Changelog

Policy

  • All current routes live under /api/v1.
  • Breaking changes will ship under a new base path (/api/v2).
  • Deprecated fields/routes return Deprecation: true and Sunset: headers with at least 90 days’ notice.

Changelog

Recent changes
2025-08-26
- Added: KPIs endpoints and /kpi/latest
- Added: Gate endpoints for CI/CD policies
- Improved: Report JSON includes risk_scores.max and overall_score
- Docs: Pagination, idempotency, rate-limit headers, canonical error schema

Data Retention & Export

  • Retention: Reports and KPIs are retained per plan limits; expired artifacts may be pruned.
  • Guarantee: /api/v1/report/{id}/json remains fetchable while within your plan’s retention window.
  • Bulk export: Use /reports?since=...&until=... and /kpi/latest?limit=N on a nightly job to your SIEM/Data Lake.

Export example

curl -sS "$BASE/api/v1/reports?limit=100&since=2025-08-01T00:00:00Z" \
  -H "Authorization: Bearer $VINES_TOKEN" | jq -r '.items[] | @json' >> reports.ndjson

Security & Acceptable Use

  • Only scan assets you own or are explicitly authorized to test.
  • Tokens are personal; rotate immediately if compromised.
  • Respect robots/target rules where applicable; we may throttle/deny abusive patterns.
  • Contact us for prg/security questions at [email protected].

Support Checklist

  • Include X-Request-Id, endpoint, and full HTTP status.
  • Attach request timestamp (UTC) and the minimal repro curl.
  • For auth issues, confirm token freshness and scope (if applicable).
  • For rate limits, share the limit headers and your backoff config.

Common Recipes

Start → Poll → Gate → Fetch PDF

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"

# 1) Start
SCAN=$(curl -sS -X POST "$BASE/api/v1/scans" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"target":"https://example.com","scan_type":"both"}' | jq -r '.scan_id')

# 2) Poll
while true; do
  s=$(curl -sS "$BASE/api/v1/scan/$SCAN/status" -H "Authorization: Bearer $TOKEN" | jq -r '.status')
  echo "status: $s"; [ "$s" = "completed" -o "$s" = "failed" -o "$s" = "aborted" ] && break
  sleep 3
done

# 3) Gate
curl -sS "$BASE/api/v1/scan/$SCAN/gate?max_critical=0&max_cvss=8.9" \
  -H "Authorization: Bearer $TOKEN" | tee gate.json | jq .

# 4) PDF (if ok)
jq -e '.ok == true' gate.json >/dev/null && \
  curl -sSL "$BASE/api/v1/report/$SCAN/pdf" -H "Authorization: Bearer $TOKEN" -o report.pdf

Latest completed scan for a target (bash)

SCAN=$(curl -sS "$BASE/api/v1/reports?limit=1&target=niles.rosebird.org&status=completed&order=desc" \
  -H "Authorization: Bearer $TOKEN" | jq -r '.items[0].scan_id')

Node: fail CI if CVSS > 8.9

const url = `${BASE}/api/v1/scan/${SCAN_ID}/gate?max_critical=0&max_cvss=8.9`;
const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` }});
const reqId = res.headers.get('x-request-id');
const body = await res.json();
if (!body.ok) { throw new Error(`Gate failed (${reqId}): ${body.reasons?.join('; ')}`); }

OpenAPI Addendum

YAML updates (pagination, errors, idempotency)
paths:
  /api/v1/reports:
    get:
      summary: List reports
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
        - { name: cursor, in: query, schema: { type: string } }
        - { name: target, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { type: string, enum: [initializing, running, completed, aborted, failed] } }
        - { name: since,  in: query, schema: { type: string, format: date-time } }
        - { name: until,  in: query, schema: { type: string, format: date-time } }
        - { name: sort,   in: query, schema: { type: string, default: scan_date } }
        - { name: order,  in: query, schema: { type: string, enum: [asc, desc], default: desc } }
      responses:
        "200":
          description: OK
          headers:
            X-RateLimit-Limit:      { schema: { type: integer } }
            X-RateLimit-Remaining:  { schema: { type: integer } }
            X-RateLimit-Reset:      { schema: { type: integer } }
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { type: object } }
                  next_cursor: { type: string }
  /api/v1/scans:
    post:
      summary: Start a scan
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string, description: "Client-provided unique key to dedupe retries" }
      responses:
        "200": { description: OK }
components:
  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
        message: { type: string }
        request_id: { type: string }
        hint: { type: string }

Scanning Private/Internal Apps

If your application is inside a private network (NAT/VPN/firewall), the Vines AI scanners must be able to reach it over the Internet to perform HTTP/HTTPS DAST (and any host/port checks you allow). Below are safe, reversible ways to provide reachability for a limited time.

Key options

  • Temporary tunnel (recommended for staging): Use ngrok to expose only the app’s HTTP(S) port with a public URL.
  • Cloudflare DNS-only (no proxy): Point a subdomain to your origin with DNS only and allowlist Vines AI IPs.
  • Cloudflare tunnel + bypass rules: If you must keep Cloudflare, create “skip” rules for Vines AI IPs and disable Bot/WAF just for the scanning path or subdomain.

Tip: For the most thorough results, use a staging environment that mirrors prod, and run scans during a known window (e.g., off-peak).

Option A — Expose a Temporary URL with ngrok

1) Install & authenticate

# macOS (brew) / Linux (apt) / Windows (choco) — pick one
brew install ngrok
# or: curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc
#     && echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list
#     && sudo apt update && sudo apt install ngrok

# Add your ngrok token (from dashboard)
ngrok config add-authtoken <NGROK_AUTH_TOKEN>

2) Start a tunnel

Expose your local app to the Internet. Use a reserved domain for a stable URL (recommended), otherwise an ephemeral URL is created each run.

HTTP app on port 8080

# Ephemeral URL
ngrok http 8080

# Reserved domain (requires ngrok reserved domain)
ngrok http --domain=api-yourapp.ngrok.app 8080

HTTPS app on 8443 (backend TLS)

ngrok http https://localhost:8443
# With reserved domain
ngrok http --domain=secure-yourapp.ngrok.app https://localhost:8443

Forward the original Host header (helps apps that key off host)

ngrok http --host-header=rewrite 8080

Disable the ngrok web inspector (slightly harder to enumerate)

ngrok http 8080 --inspect=false

3) Config file (repeatable)

Create ~/.config/ngrok/ngrok.yml (or ngrok.yml) and run ngrok start --all.

# ~/.config/ngrok/ngrok.yml
authtoken: <NGROK_AUTH_TOKEN>
version: 2
tunnels:
  app:
    addr: 8080
    proto: http
    host_header: rewrite
    inspect: false
    # Optional reserved domain for stability:
    # domain: api-yourapp.ngrok.app
  app-https:
    addr: https://localhost:8443
    proto: http
    inspect: false

4) Feed the URL to Vines AI

BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
TARGET="https://api-yourapp.ngrok.app"   # from ngrok

curl -sS -X POST "$BASE/api/v1/scans" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"target\":\"$TARGET\",\"scan_type\":\"both\"}" | jq .

Heads‑up: Port scans will reflect the public edge (ngrok endpoint), not your internal LAN. Web vulnerabilities (ZAP) are still valid against your app.

Programmatic helpers

Python (start, read URL, scan)

import subprocess, json, time, requests, os
BASE = "https://vines.rosebird.org"
TOKEN = os.environ["VINES_TOKEN"]
H = {"Authorization": f"Bearer {TOKEN}"}

# start ngrok in background
proc = subprocess.Popen(["ngrok","http","--inspect=false","8080"],
                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
time.sleep(2)  # give ngrok a moment

# fetch public URL from local API
j = requests.get("http://127.0.0.1:4040/api/tunnels").json()
TARGET = [t["public_url"] for t in j["tunnels"] if t["proto"].startswith("http")][0]

# start scan
r = requests.post(f"{BASE}/api/v1/scans", json={"target": TARGET, "scan_type": "both"}, headers=H)
print(r.json())

Node (start, read URL, scan)

import { spawn } from "node:child_process";
const BASE = "https://vines.rosebird.org";
const TOKEN = process.env.VINES_TOKEN;

const ng = spawn("ngrok", ["http","--inspect=false","8080"], { stdio: "ignore", detached: true });

const url = "http://127.0.0.1:4040/api/tunnels";
const tunnels = await (await fetch(url)).json();
const TARGET = tunnels.tunnels.find(t => t.proto.startsWith("http")).public_url;

const res = await fetch(`${BASE}/api/v1/scans`, {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({ target: TARGET, scan_type: "both" }),
});
console.log(await res.json());

Firewall & WAF Allowlisting

To reduce false negatives and avoid throttling, allowlist the scanner egress IPs and relax WAF rules just for the scan window/subdomain.

Checklist

  • Allow inbound 80/443 to the exposed endpoint (ngrok handles this if used).
  • Allowlist Vines AI scanner IP(s) on your firewall/WAF. (Contact support for the current list.)
  • Temporarily relax/bypass rate‑limit, bot management, and aggressive WAF signatures for the scan host.
  • Disable geo/IP reputation blocking for the allowlisted scanner IPs.
  • Ensure outbound egress can reach vines.rosebird.org for APIs if you trigger scans from inside.

Authentication & Session Strategy

Authentication gates can hide routes from the scanner. Use one of these patterns:

  1. IP‑based bypass (safest scope): For the scan subdomain, if client IP ∈ {Vines AI} then skip auth middleware and CSRF checks; leave auth on for everyone else.
  2. Dedicated test user: Create a non‑privileged account without MFA/SSO prompts and share a long‑lived token via the Authorization header for the scan window.
  3. Staging mirror without auth: Duplicate prod config but disable auth only in staging and only during the scan window.

Example: NGINX allowlist to skip auth (pseudocode)

map $remote_addr $skip_auth {
  default 0;
  # Vines AI scanner egress IPs (sample placeholders)
  203.0.113.10 1;
  203.0.113.11 1;
}
server {
  server_name scan.staging.example.com;
  location / {
    if ($skip_auth) { set $auth_bypass 1; }
    # your app reads X-Bypass-Auth to skip middleware
    proxy_set_header X-Bypass-Auth $auth_bypass;
    proxy_pass http://app:8080;
  }
}

Example: Express middleware

const SCANNER_IPS = new Set(["203.0.113.10","203.0.113.11"]);
app.use((req,res,next) => {
  if (SCANNER_IPS.has(req.ip) || req.get("X-Bypass-Auth") === "1") return next(); // bypass
  return requireAuth(req,res,next);
});

Avoid disabling protections globally. Limit by IP + subdomain + timeframe and revert after the scan.

Cloudflare — Make Origin Reachable

Option 1 — DNS Only (no proxy)

In DNS, set your scan subdomain’s record to DNS Only (gray cloud). This exposes the origin IP directly. Combine with firewall allowlisting of Vines AI IPs.

  • Turn off “Proxied” (orange cloud) → becomes gray cloud “DNS only”.
  • Ensure port 80/443 open to the Internet for the origin (or to your forward proxy/ELB).
  • Revert to orange cloud after the scan.

Option 2 — Keep proxy but bypass security for Vines AI

Create a WAF Custom Rule: if ip.src in {Vines IPs} then Skip WAF, Bot Management, Rate Limiting, and Managed Challenge for the scan subdomain or path.

Option 3 — Cloudflare Tunnel

If you use cloudflared tunnels, keep the tunnel but add custom rules to Skip WAF/Bot/RL for scanner IPs or create a separate scan‑only subdomain routed to the same origin.

Disable Bot Fight & UAM (scoped)

  • Turn off “Under Attack Mode” for the scan subdomain.
  • Disable “Super Bot Fight Mode” (or set to “Allow Verified Bots + Skip for scanners”).
  • Set Security Level “Essentially Off” for the scan subdomain rule only.

Cloudflare caching can mask dynamic responses. Add a Page Rule or Cache Rule to Bypass cache for the scan host.

Throttling, Windows & Safety

  • Choose a window: Run scans in an agreed maintenance window to avoid alert fatigue.
  • Rate limits: Temporarily increase or disable rate limits for the scan host/IPs.
  • DoS protections: Ensure WAF anomaly scoring doesn’t auto‑block during crawling.
  • Robots: Allow scanning crawler in /robots.txt for the scan host; or temporarily serve Disallow: only for production, not staging.
  • Data safety: Use a staging DB without real PII/data mutations. If scanning prod, restrict dangerous verbs (DELETE/PUT) behind feature flags.
  • TLS: Support modern ciphers; present complete chain; avoid certificate pinning during scans.

Quick Allowlist Snippets

NGINX (allowlist scanner IPs)

# Only allow Vines AI for this server; others get 403
allow 203.0.113.10;  # placeholder
allow 203.0.113.11;  # placeholder
deny all;

Apache

Require ip 203.0.113.10
Require ip 203.0.113.11
# Otherwise:
Require all denied

Replace placeholder IPs with the official Vines AI egress IP list you receive from support.

Optional — Authenticated Routes

If you want to scan authenticated areas:

  • Create a test user with broad read‑only access (no 2FA/MFA/SSO prompts).
  • Expose a login endpoint that accepts basic creds or a static token during the scan window.
  • Whitelist the scanner IPs from brute‑force/lockout policies.

Header example

curl -sS "$TARGET/protected" \
  -H "Authorization: Bearer <TEST_STATIC_TOKEN>"

DevSecOps Pre‑Scan Checklist

  • ✅ Reachability confirmed (ngrok URL or DNS-only host resolves publicly).
  • ✅ Firewall/WAF allowlisting applied to scanner IPs; rate‑limits relaxed for scan host.
  • ✅ Auth plan chosen (IP bypass, test user, or staging without auth).
  • ✅ Cache disabled and CDN/WAF “skip” rules in place for scan host.
  • ✅ Database/test data prepared; destructive ops gated.
  • ✅ Maintenance window agreed; monitoring teams notified.
  • ✅ Post‑scan revert plan documented (undo bypasses, re‑enable protections).

Quick Tools

Curl env bootstrap

export BASE="https://vines.rosebird.org"
export VINES_TOKEN="<your_token>"
alias vines-get='curl -sS -H "Authorization: Bearer $VINES_TOKEN"'
vines-get "$BASE/api/v1/reports?limit=5" | jq .

Tiny Makefile (start→poll→pdf)

BASE ?= https://vines.rosebird.org
TOKEN ?= $(VINES_TOKEN)
TARGET ?= https://example.com
SCAN_FILE := .scan.id

start:
\t@curl -sS -X POST "$(BASE)/api/v1/scans" \\
\t  -H "Authorization: Bearer $(TOKEN)" -H "Content-Type: application/json" \\
\t  -d '{"target":"$(TARGET)","scan_type":"both"}' | jq -r .scan_id > $(SCAN_FILE) && \\
\techo "scan=$$(cat $(SCAN_FILE))"

poll:
\t@sid=$$(cat $(SCAN_FILE)); \\
\twhile true; do \\
\t  st=$$(curl -sS "$(BASE)/api/v1/scan/$$sid/status" -H "Authorization: Bearer $(TOKEN)" | jq -r .status); \\
\t  echo status: $$st; \\
\t  [ "$$st" = "completed" -o "$$st" = "failed" -o "$$st" = "aborted" ] && break; \\
\t  sleep 3; \\
\tdone

pdf:
\t@sid=$$(cat $(SCAN_FILE)); \\
\tcurl -sSL "$(BASE)/api/v1/report/$$sid/pdf" -H "Authorization: Bearer $(TOKEN)" -o report.pdf && \\
\techo "saved report.pdf"

Troubleshooting (2-minute fixes)

SymptomLikely causeFix
401 invalid_tokenWrong/expired tokenRotate in Profile → API Tokens; re-export VINES_TOKEN.
404 not_ownerUsing someone else’s scan_idList yours via /api/v1/reports and retry.
429 capacityBusy scanners / plan limitBackoff (1,2,4,8s) or try later; reduce concurrency.
Polling never completesTarget unreachable behind firewallUse ngrok/cloud DNS-only; allowlist scanner IPs; see “Internal Apps”.
All JSON emptyBlocked by WAF/bot rulesAdd bypass for scan subdomain + scanner IPs; disable CF proxy for the scan.
PDF downloads but blankCache/CDN serving error pageBypass cache for scan host; retry directly to origin.

Minimal Policies (what small teams actually use)

  • Prod gate: ?max_critical=0&max_high=0&max_cvss=8.9
  • Staging gate: ?max_critical=0&max_high=2&max_cvss=9.4
  • Quick smoke: ?max_cvss=7.0 (let everything else pass)

Keep it simple: one strict prod gate, one relaxed staging gate.

Lightweight Support

  • Email: [email protected] (include X-Request-Id and your curl).
  • Status/maintenance window: status page
  • Version pinning: lock to /api/v1; breaking changes will use /api/v2.

Quick Tools & Scripts (Small-Team Friendly)

Hoppscotch (Web)

Use the online client; import OpenAPI or run raw requests.

{
  "openapi": "3.0.0",
  "info": { "title": "Vines API", "version": "1.0" },
  "servers": [{ "url": "https://vines.rosebird.org" }],
  "paths": { "/api/v1/reports": { "get": { "summary": "List reports" } } }
}

In Hoppscotch: Collections → Import → OpenAPI JSON (above).

Bruno

Simple, local-first API client. Import a tiny collection.

{
  "version": "1",
  "type": "collection",
  "name": "Vines API",
  "slug": "vines-api",
  "requests": [
    {
      "name": "List Reports",
      "method": "GET",
      "url": "https://vines.rosebird.org/api/v1/reports?limit=5",
      "headers": [{ "name": "Authorization", "value": "Bearer {{VINES_TOKEN}}" }]
    }
  ],
  "envs": [{ "name": "default", "variables": { "VINES_TOKEN": "" } }]
}

Thunder Client (VS Code)

Import a JSON collection into Thunder Client.

{
  "client": "Thunder Client",
  "collectionName": "Vines API",
  "dateExported": "2025-08-26",
  "requests": [
    {
      "name": "Gate Latest",
      "method": "GET",
      "url": "https://vines.rosebird.org/api/v1/kpi/latest?limit=1",
      "headers": [{ "name": "Authorization", "value": "Bearer {{VINES_TOKEN}}" }]
    }
  ],
  "env": [{ "name": "default", "key": "VINES_TOKEN", "value": "" }]
}

RapidAPI Client (VS Code)

Import OpenAPI (same as Hoppscotch) → set VINES_TOKEN global var → run.

openapi: 3.0.0
info: { title: Vines API, version: "1.0" }
servers: [ { url: https://vines.rosebird.org } ]
paths:
  /api/v1/scan/{scan_id}/status:
    get:
      parameters:
        - { name: scan_id, in: path, required: true, schema: { type: string } }

REST Client (VS Code .http)

Save as vines.http, click “Send Request.”

### Set vars
@BASE = https://vines.rosebird.org
@TOKEN = {{VINES_TOKEN}}

### List reports
GET {{BASE}}/api/v1/reports?limit=5
Authorization: Bearer {{TOKEN}}

### Gate latest
GET {{BASE}}/api/v1/kpi/latest?limit=1
Authorization: Bearer {{TOKEN}}

HTTPie (CLI)

Human-friendly curl for quick calls.

http GET https://vines.rosebird.org/api/v1/reports \
  Authorization:"Bearer $VINES_TOKEN"

http POST https://vines.rosebird.org/api/v1/scans \
  Authorization:"Bearer $VINES_TOKEN" \
  Content-Type:application/json \
  target=https://example.com scan_type=both

Bash Helpers

Add these to ~/.bashrc/~/.zshrc for 1-liners.

export BASE="https://vines.rosebird.org"
export VINES_TOKEN="<token>"

vget(){  curl -sS "$1" -H "Authorization: Bearer $VINES_TOKEN"; }
vpost(){ curl -sS -X POST "$1" -H "Authorization: Bearer $VINES_TOKEN" -H "Content-Type: application/json" -d "$2"; }

# examples
vget "$BASE/api/v1/reports?limit=5" | jq .
vpost "$BASE/api/v1/scans" '{"target":"https://example.com","scan_type":"both"}' | jq .

PowerShell

Great for Windows users; uses Invoke-RestMethod.

$BASE = "https://vines.rosebird.org"
$TOKEN = $env:VINES_TOKEN
$H = @{ Authorization = "Bearer $TOKEN" }

# List reports
Invoke-RestMethod -Uri "$BASE/api/v1/reports?limit=5" -Headers $H -Method GET

# Start scan
$body = @{ target = "https://example.com"; scan_type = "both" } | ConvertTo-Json
Invoke-RestMethod -Uri "$BASE/api/v1/scans" -Headers ($H + @{ "Content-Type"="application/json" }) -Method POST -Body $body

Try It — Live

Supply your Base URL and token, pick an endpoint, then hit Run. For endpoints with {id}, fill scan_id.

Output will appear here…