Vulnix
API

Runs

Start a pentest, follow it while it executes, and pull its findings, agent trace, and signed report.

A run is one execution of the agent against a scope. Starting a run charges credits, so this is the resource where getting the preconditions right matters most.

The run object

Prop

Type

Statuses

Prop

Type

finished, released, failed, and stopped are terminal. Poll until the status is one of those four, not until it equals finished — a run that ends any other way would poll forever.

A failed or stopped run still has data

Partial findings, the agent trace, and the phase timeline all survive. Do not discard a run because it did not reach finished.

Start a run

POST /scopes/{scope_id}/runs · scope runs:write · role admin · returns 201

Prop

Type

curl -X POST https://api.vulnix.dev/scopes/01M2NMSFVBHZTYR842YNP45TF3/runs \
  -H "Authorization: Bearer $VULNIX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "deploy-a1b2c3d4",
    "custom_headers": [{ "name": "X-Staging-Bypass", "value": "..." }]
  }'
{
  "id": "01M2NP4K8QW3ZR7YT2XVB9CDEF",
  "scope_id": "01M2NMSFVBHZTYR842YNP45TF3",
  "status": "queued",
  "workflow_id": "pentest-01M2NP4K8QW3ZR7YT2XVB9CDEF",
  "created_at": "2026-09-16T18:04:11.229104+00:00",
  "completed_at": null,
  "cost_usd": null
}

Idempotency is mandatory, and it is load-bearing

idempotency_key is required because starting a run spends money. If a run already exists for the key, that existing run is returned — no second run, no second charge. A network timeout where you never saw the response is therefore safe to retry with the same key.

Derive the key from the thing that caused the run — a commit SHA, a deploy id, a date — not from a random value generated at call time, or a retry after a timeout will mint a new key and charge you twice.

Credits are charged before the run starts

The charge happens up front, after the checks below pass. A run that is refused for a failed precondition is never charged.

Preconditions

They are checked in this order, and each returns 422:

The scope must have targets. A blackbox scope with none returns scope_has_no_targets. A whitebox scope may have none — its repository is the target — but must have a repository set, or it returns whitebox_pentest_missing_repository.

Every target must still be verified. Any target not in verified returns scope_verification_lost, with details.target_ids naming them. Verification is re-checked at launch, not just when the target was added, so a DNS record someone deleted last month surfaces here.

The organization must be able to pay. No active plan or unexpired trial returns credits_exhausted, with details.required_usd and details.available_usd.

{
  "error": {
    "code": "scope_verification_lost",
    "message": "one or more targets have lost verification since this scope was authorized",
    "details": { "target_ids": ["01M2NMT9770P4XX9AB8PCZZGB1"] }
  },
  "request_id": "01M2NMT9NFC1THFAPJVQVB5FPC"
}

List runs

GET /runs · scope runs:read · role admin

Every run in the organization, most recent first. Each entry is a run object plus a summary of what it was pointed at:

Prop

Type

{
  "runs": [
    {
      "id": "01M2NP4K8QW3ZR7YT2XVB9CDEF",
      "scope_id": "01M2NMSFVBHZTYR842YNP45TF3",
      "status": "finished",
      "workflow_id": "pentest-01M2NP4K8QW3ZR7YT2XVB9CDEF",
      "created_at": "2026-09-16T18:04:11.229104+00:00",
      "completed_at": "2026-09-16T18:49:37.004881+00:00",
      "cost_usd": 4.13,
      "scan_type": "blackbox",
      "target_summary": { "values": ["acme-demo.com"], "total_count": 1 }
    }
  ]
}

Get a run

GET /runs/{run_id} · scope runs:read · role admin

The run object, the same target_summary, and two fields the list does not carry:

Prop

Type

Findings from a run

GET /runs/{run_id}/findings · scope runs:read · role admin

{
  "findings": [],
  "severity_breakdown": { "critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0 }
}

findings holds finding objects. severity_breakdown counts exactly the findings in that array. An empty array is a valid 200 — a clean run is a result, not an error.

Phase timeline

GET /runs/{run_id}/phases · scope runs:read · role analyst

What the agent planned and how far it got, reduced to one final state per task:

{
  "phases": [
    { "task_id": "t1", "title": "Map authentication surface", "status": "completed" },
    { "task_id": "t2", "title": "Probe session handling", "status": "in_progress" }
  ]
}

Readable during and after the run. Returns an empty array once the underlying event stream ages out after 7 days.

Agent trace

GET /runs/{run_id}/agent-trace · scope runs:read · role analyst

The tree of agents that worked the run, and what each one did.

Prop

Type

Report

GET /runs/{run_id}/report?format=… · scope runs:read · role admin

Returns the file itself, not JSON, with Content-Disposition: attachment.

Prop

Type

curl -L "https://api.vulnix.dev/runs/$RUN_ID/report?format=pdf" \
  -H "Authorization: Bearer $VULNIX_TOKEN" \
  -o report.pdf

An unsupported format returns 422 invalid_report_format. On a free trial, a run containing any critical or high finding returns 403 premium_detail_required for every format, including SARIF.

Compare two runs

GET /runs/compare?run_a=…&run_b=… · scope runs:read · role admin

Both parameters are required. Returns three arrays of finding objects — what regression testing actually needs:

Prop

Type

Stop a run

POST /runs/{run_id}/stop · scope runs:write · role admin

Returns the updated run object. A run already in a terminal status returns 409 run_not_stoppable. Findings produced before the stop are kept, and credits already charged are not refunded.

Live events

GET /runs/{run_id}/events · scope runs:read · role admin

Server-Sent Events for the duration of the run. See live event streams for resumption. Polling GET /runs/{run_id} every few seconds is simpler and is the right choice for CI.

On this page