Pencheff

Supply chain · Resources

API reference

Authentication, targets, scans, findings, assets, and MCP tools.

SCA and SBOM workflows connect vulnerable components, manifests, package URLs, fixed versions, reachability, EPSS, KEV, SSVC, and license evidence to the same findings and reports as application testing.

Attack coverageverified
8coverage areas
5operator steps
4evidence fields
Coverage8
Execution5
Evidence4
Controls4
ReconCrawlActive probeOASTVerify

Bars track the four sections on this page, scaled to the coverage retained from the nav source.

ScopeFeatured
SectionResources
MethodDeterministic-first
OutputUnified evidence
ProfileSupply chain
01

Coverage

What does API reference test?

  • Authentication, targets, scans, findings, assets, and MCP tools.
  • This page is part of Resources under Featured.
  • It links back into the broader everything needed to operate pencheff experience.
  • OSV.dev, NVD 2.0, GitHub Advisory Database, RustSec, GoVulnDB, EPSS, CISA KEV, and SSVC enrichment.
  • Manifest support for npm, PyPI, Go modules, Cargo, Ruby, Composer, Maven, OS packages, and container packages.
  • SPDX 2.3 and CycloneDX 1.5 SBOM generation with optional Syft enrichment.
  • Reachability annotation that separates exploited, reachable, present, and unknown risk.
  • License policy checks and deterministic version-bump remediation for eligible dependencies.
02

Execution

How does Pencheff run this?

  • Parse repository manifests, lockfiles, or container package inventories.
  • Resolve packages to advisories, fixed versions, package URLs, and known exploitation signals.
  • Annotate reachability from imports, call paths, runtime evidence, or scanner context.
  • Generate SBOM output and link component rows back to findings.
  • Prioritize remediation by exploitability, reachability, business criticality, and compliance impact.
03

Evidence

What evidence does this produce?

  • Package name, ecosystem, installed version, fixed version, advisory id, CVSS, EPSS, KEV, and SSVC.
  • SBOM component records with PURL, supplier, version, license, and dependency relationships.
  • Reachability state, import evidence, or reason the vulnerable component is currently only present.
  • Audit appendix output for procurement, compliance, and release records.
04

Controls

How is this kept safe to run?

  • Dependency risk is not sorted by CVSS alone; operational signals influence priority.
  • SBOM generation is repeatable and latest-generation output replaces stale records.
  • License and vulnerability policy can be used as release-gate input.
  • Version-bump fixes are deterministic when advisory metadata supports them.
01

From the Pencheff docs

API overview

Base URL:

  • Production: https://app.pencheff.com/api
  • Local dev: http://localhost:8000

Interactive API explorer: browse and test every endpoint in your browser at https://api.pencheff.com/docs (Swagger UI). Click Authorize, paste a pcf_live_… API key, and use Try it out. See API keys → Interactive API explorer for the full walkthrough, including how to find your workspace id.

All endpoints require a bearer token issued by Clerk. See Authentication.

Endpoints at a glance

NamespaceEndpointsDocs
/authLogin, signup, logoutauthentication
/targetsCRUD for scan targetstargets
/scansTrigger scans, stream progress, fetch resultsscans
/findingsList, suppress, verify, comment, assignfindings
/schedulesCron-driven recurring scansschedules
/assetsAttack surface inventoryassets
/integrationsSlack, Teams, PagerDuty, Splunk, webhookintegrations
/sboms/{scan_id}SBOM browse + downloadsboms
/dependencies/{scan_id}SCA dep inventorydependencies
/proxyIntercepting proxy sessionsproxy

MCP tools (plugin side)

When you run Pencheff as an MCP server, the following 81 MCP tools are available — see MCP tool reference for the full list.

Response conventions

  • Always JSON (Content-Type: application/json)
  • Timestamps in ISO 8601 UTC ("2026-04-21T14:23:00+00:00")
  • UUIDs for all primary keys
  • Errors: { "detail": "..." } for client errors, { "detail": "ClassName: message" } for server errors
  • Validation errors follow FastAPI's shape: { "detail": [{ "loc": [...], "msg": "..." }, ...] }

Rate limits

  • 60 requests/min per user (SaaS)
  • 30 concurrent scans per org on the Pro plan

Hit X-RateLimit-Remaining in the response headers to see how close you are.

02

From the Pencheff docs

Scans API

POST /scans

Trigger a new scan.

POST /scans
{
  "target_id": "...",
  "profile": "standard",
  "consent_payload": {
    "authorization_text": "I am authorised to test example.com as of 2026-05-06 and I accept the disclosed actions.",
    "acknowledged": true
  }
}

profile is one of quick, standard, or deep. Older specialised names (engage, compliance, api-only, cicd, sca, iac, supply-chain, network-va, hackme, continuous, compliance-full) are still accepted and coerced to the matching tier at the runner — see Picking a profile for the fold-in table.

consent_payload is required when swarm mode is active (default). The API returns 422 Unprocessable Entity if the field is absent, if authorization_text is shorter than 50 characters, or if acknowledged is not true. The payload is persisted on Scan.consent_payload (JSONB) and included in every audit export. Shape (Pydantic schema ConsentPayload):

type ConsentPayload = {
  authorization_text: string;  // >= 50 characters
  acknowledged: boolean;       // must be true
};

GET /scans

List scans for the current org (paginated).

GET /scans/{id}

Fetch a single scan — includes summary counts, grade, progress.

GET /scans/{id}/progress (SSE)

Server-Sent Events stream of live progress ticks during an active scan. Events:

  • stage_start: recon_passive
  • stage_done: recon_passive: 47 endpoints
  • finding: HIGH SQLi on /api/users
  • finished

GET /scans/{id}/findings

Returns every finding for the scan with CVSS, EPSS, KEV, risk_score, compliance mapping.

GET /scans/{id}/linked-repos

Returns the repositories attached to this URL scan's target. Source-code findings (Semgrep · Bandit · gosec · Brakeman · ESLint · OSV · secret-scan) live on each repo's own assessment page (/repos/{repository_id}) — the URL scan deliberately does not mix SAST findings into its own results list. Use this endpoint to render a "Linked repositories" sidebar with deep-links.

type LinkedRepo = {
  repository_id: string;
  full_name: string;
  provider: string | null;
  scan_url: string;   // e.g. "/repos/abc-123"
};

Returns [] for scans whose target has no attached repos. Available on URL targets only.

GET /scans/{a}/compare/{b}

Compare two completed scans of the same workspace and return a structured diff. Particularly useful for LLM red-team scans where you want to gate a PR on safety regressions, or A/B different model versions on the same suite.

GET /scans/3a35.../compare/9359...
Authorization: Bearer <token>

Response:

type ScanCompare = {
  baseline: { name: string; summary: RedTeamSummary };
  candidate: { name: string; summary: RedTeamSummary };
  regressions: Finding[];     // candidate-only
  fixes: Finding[];           // baseline-only
  common_failures: Finding[]; // present in both
  counts: { regressions: number; fixes: number; common_failures: number };
  keys: { regressions: string[]; fixes: string[]; common_failures: string[] };
  scan_a: { id: string; profile: string; grade: string | null;
            score: number | null; created_at: string };
  scan_b: { id: string; profile: string; grade: string | null;
            score: number | null; created_at: string };
};

The dedup key is endpoint|parameter|technique|title, so re-running the identical suite against an unchanged target produces zero regressions. The web UI exposes the same diff at /scans/compare?a=…&b=… with a JUnit-XML download for the regressions list — wire it into CI to fail builds on new findings.

POST /scans/{id}/share (LLM only)

Issues a Fernet-encrypted token granting public read access to the scan's LLM-flavored report. Available only when the underlying target is kind: "llm".

POST /scans/3a35.../share?ttl_seconds=604800
Authorization: Bearer <token>

Response:

{
  "token": "gAAAAA...",
  "expires_in": 604800,
  "url_path": "/share/llm/gAAAAA..."
}

The companion public route GET /share/llm/{token} renders the scan as HTML (default), Markdown, CSV, or JSON depending on the ?download= query param. Token expiry is the only revocation — let it expire to revoke. PII is redacted in evidence snippets before rendering, regardless of the public/private path.

GET /scans/{id}/llm-traces

Returns the full LLM call trace for a scan. Each row corresponds to one chat-completions call made by a swarm agent during the scan.

Authentication required. Returns [] for scans run with SWARM_ENABLED=false.

type ScanLLMTrace = {
  id: string;
  scan_id: string;
  agent: string;             // e.g. "InjectionAgent", "ChainAgent"
  turn: number;              // agent conversation turn number
  request_messages: object[];  // full messages array sent to the LLM
  response: object;          // raw LLM response including tool-call blocks
  input_tokens: number;
  output_tokens: number;
  cache_read_tokens: number;
  reasoning: string | null;  // thinking/reasoning block if present
  created_at: string;
};

GET /scans/{id}/evidence/{finding_id}.png

Returns the Playwright evidence screenshot captured by EvidenceCaptureAgent for the specified finding. PII is redacted before storage.

Authentication required. Returns 404 Not Found if no screenshot exists for that finding (e.g. the finding was below high severity, the agent did not run, or the scan used the legacy single-agent path).

GET /scans/3a35.../evidence/f7b2c4....png
Authorization: Bearer <token>

Response: image/png binary. Cache the response client-side — screenshots do not change after the scan completes.

DELETE /scans/{id}

Cancel an in-flight scan or remove a finished one.

Scan object

type Scan = {
  id: string;
  target_id: string;
  status: "queued" | "running" | "completed" | "failed" | "cancelled";
  profile: string;
  progress_pct: number;      // 0-100
  current_stage: string | null;
  summary: {
    critical: number; high: number; medium: number; low: number; info: number;
    suppressed: number;
  } | null;
  grade: "A" | "B" | "C" | "D" | "F" | null;
  score: number | null;
  started_at: string | null;
  finished_at: string | null;
};
03

From the Pencheff docs

Findings API

GET /scans/{scan_id}/findings

Return every finding for a scan. Filter with query params:

  • ?severity=critical
  • ?category=injection
  • ?owasp_category=A03
  • ?verified_only=true
  • ?include_suppressed=true
  • ?sort=risk_score (default; use cvss_score or created_at)

GET /findings/{id}

Fetch a single finding with full evidence, comments, assignments, tags.

PATCH /findings/{id}

Update status. Valid fields:

{
  "verification_status": "true_positive" | "false_positive" | "true_negative" | "false_negative",
  "suppressed": true,
  "suppress_reason": "accepted_risk" | "wont_fix" | "false_positive" | "duplicate" | "out_of_scope",
  "suppress_notes": "string",
  "resolved_at": "2026-04-21T…Z",
  "sla_days": 7
}

Collaboration

  • POST /findings/{id}/comments — add a comment
  • GET /findings/{id}/comments — list comments
  • POST /findings/{id}/assign{"assignee_user_id": "..."}
  • POST /findings/{id}/tags{"tag": "p0-fix"}
  • DELETE /findings/{id}/tags/{tag} — remove a tag

Prioritisation fields

Every Finding includes the unified prioritisation surface:

FieldTypeSource
risk_scorefloat (0–100)computed at insert from CVSS × EPSS × KEV × SSVC × reachability
ssvc_decisionstringone of act, attend, track_star, track
reachabilitystringone of exploited, reachable, present, unknown — see Reachability classifier
epssfloat (0–1) | nullEPSS feed; populated for SCA findings
kevboolCISA KEV catalog membership

Sort the list endpoint by risk_score:

GET /scans/{scan_id}/findings?sort=risk_score   (default)

The unified, cross-table queue lives at /unified-findings.

POST /findings/{id}/propose_fix

Generates a draft FixProposal for the finding. SCA findings get a deterministic version-bump diff; SAST/DAST findings synthesise a unified diff via the operator-configured patch-synthesis backend. See Auto-fix PRs.

The route accepts kind{sast, dast}; SCA findings ride the dast kind and Pencheff detects the SCA payload from evidence and routes internally.

POST /findings/{id}/triage

Pro tier. Triage 2.0 — exploitability walkthrough returning { walkthrough, blast_radius, exploit_scenario, fix_outline, confidence }. Cached on finding.ai_triage; pass ?force=true to regenerate. See Triage 2.0.

04

From the Pencheff docs

API keys (PENCHEFF_API_KEY)

PENCHEFF_API_KEY tokens give scripts, CI pipelines, and scheduled jobs programmatic access to the Pencheff API without holding a Clerk session.

Each key is always pinned to one organisation and may additionally be pinned to a single workspace. Permissions are granted as a list of fine-grained category:action scopes — a key can only call endpoints whose required scope it holds.

Key format

pcf_live_<43+ url-safe base64 chars>

The first eight characters after pcf_live_ form the lookup prefix displayed in the dashboard (pcf_live_aB3xZ9k1…). The full secret is shown once at creation — copy it then; it cannot be recovered.

Creating a key

In the dashboard: Settings → API keys → New key. Programmatically (a session-only endpoint — you must be signed in):

curl -X POST https://api.pencheff.com/api/v1/api-keys \
  -H "Authorization: Bearer $CLERK_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "GitHub Actions — production CI",
    "org_id": "org_01H...",
    "workspace_id": "ws_01H...",
    "scopes": ["scans:write", "findings:read", "reports:export"],
    "expires_at": "2027-05-07T00:00:00Z"
  }'

Response:

{
  "id": "ak_01H...",
  "name": "GitHub Actions — production CI",
  "key": "pcf_live_aB3xZ9k1...43chars...",
  "prefix": "aB3xZ9k1",
  "org_id": "org_01H...",
  "workspace_id": "ws_01H...",
  "scopes": ["findings:read", "reports:export", "scans:write"],
  "effective_scopes": ["findings:read", "reports:export", "scans:write"],
  "expires_at": "2027-05-07T00:00:00Z",
  "created_at": "2026-05-07T22:14:00Z"
}

The key field is only present in the create response. Save it now.

Using a key

Send it as a bearer token. The Authorization header is the only accepted location — query-string ?token= is rejected for keys (URLs leak into logs and browser history).

export PENCHEFF_API_KEY="pcf_live_aB3xZ9k1...43chars..."

curl https://api.pencheff.com/scans \
  -H "Authorization: Bearer $PENCHEFF_API_KEY"

The active workspace is read from the key's pin. If the key is workspace-scoped, requests with a conflicting X-Workspace-Id header are rejected with 403. If the key is org-scoped only (workspace_id: null), every request must include X-Workspace-Id and the workspace must belong to the key's org.

Interactive API explorer (Swagger UI)

Every endpoint is browsable and testable in your browser at https://api.pencheff.com/docs (Swagger UI). /redoc and the raw /openapi.json are served too.

To exercise calls with your key:

  1. Click Authorize (top right).
  2. Paste a pcf_live_… key into PencheffApiKey — it is sent as Authorization: Bearer <key>.
  3. For an org-scoped key, also set WorkspaceId (the X-Workspace-Id header). Leave it blank for a workspace-pinned key.
  4. Expand any endpoint → Try it out → fill parameters → Execute.

Finding your workspace id

Only org-scoped keys (workspace_id: null) need this. Two ways:

  • Pin the key instead. Create the key against a specific workspace at Settings → API keys; then the workspace is forced and you never send X-Workspace-Id.

  • Ask the API. GET /workspaces needs only the key (no workspace header). In Swagger: Authorize, then run GET /workspaces → Try it out and copy the id of the workspace you want.

    curl https://api.pencheff.com/workspaces \
      -H "Authorization: Bearer $PENCHEFF_API_KEY"
    # → [{ "id": "ws_…", "org_id": "org_…", "name": "Default", "slug": "default" }, …]
    

The docs page is public, but every call still requires a valid key, and a key only reaches the scopes and workspace it was granted.

Permission model

Default-deny

API-keyed requests are rejected by default on every endpoint that does not explicitly declare a required scope. There is no fallback to "all permissions" — even a key with *:* cannot reach an endpoint that opts out of API-key access.

Session-only endpoints

A separate set of endpoints rejects API-keyed requests outright (HTTP 403). These never accept a key, regardless of scopes:

CategoryReason
api-keysA leaked key cannot mint more keys
authSign-in / signup / onboarding
billingStripe customer state, plan changes
brandingWorkspace branding
orgsMember roles, invites, org settings
workspacesWorkspace creation / rename

Org-wide vs. workspace-scoped keys

  • workspace_id: null — the key acts on any workspace in its org. The caller still has to pass X-Workspace-Id to pick one per request. Only org owners and admins may mint these.
  • workspace_id: <id> — the key is pinned to that workspace. Any other X-Workspace-Id is rejected. Any user (member or above) in the org may mint these for workspaces they belong to.

Membership re-check

Every request re-validates that the issuing user is still a member of the key's org. If an admin removes the user from the org, all of that user's keys for that org stop working immediately — there is no cache.

Scope catalog

Wildcards are supported when granting:

  • scans:* — both read and write on scans
  • *:read — read everything that exposes a read scope
  • *:* — every scope in the catalog (admin-equivalent)

The matcher always normalises the required scope to the concrete form declared by the endpoint, so scans:* will satisfy scans:write.

ScopeWhat it grants
assets:readList assets in the inventory
assets:writeTrigger ASM discovery, modify or delete assets
comments:readRead finding comments
comments:writeCreate or edit finding comments, assign findings, manage tags
dashboard:readRead dashboard metrics: heatmap, trend, KEV exposure, fix conversion
dependencies:readRead SCA dependency data
engagements:readRead engagement metadata and unified findings
engagements:writeCreate, close, or rotate engagement pairing codes
findings:readList and read findings
findings:writeTriage, recheck, suppress, reopen, change status
fix_proposals:readRead fix proposal status, diffs, and usage stats
fix_proposals:writeGenerate, apply, revert auto-fix proposals; bulk-fix
integrations:readRead integration configuration
integrations:writeCreate, modify, delete, or test integrations
intruder:readRead intruder payload sets, attacks, and results
intruder:writeCreate payload sets and run intruder attacks
notes:readRead engagement notes
notes:writeCreate, modify, or delete engagement notes
proxy:readRead proxy session state and per-scan history
proxy:writeStart or stop proxy sessions
repeater:readRead repeater tabs and saved responses
repeater:writeCreate, modify, or send repeater requests
repos:readRead repositories, repo scans, repo findings, repo SBOMs
repos:writeConnect repos, trigger repo scans, generate SBOMs
reports:exportGenerate reports (PDF, DOCX, HTML)
reports:readRead existing reports and download files
scans:readList and read scans, get progress, view findings
scans:writeInitiate, configure, cancel, or rerun scans
schedules:readRead scheduled scans
schedules:writeCreate, modify, or delete scheduled scans
sboms:readRead SBOMs
targets:readRead targets
targets:writeCreate, modify, or delete targets
traffic:readRead recorded HTTP traffic
traffic:writeTag or modify traffic rows
unified_findings:readRead the unified-finding queue

Coverage matrix

The default-deny dependency layer rejects API-keyed requests on any endpoint that doesn't explicitly declare a required scope. Every scope listed above is wired into at least one HTTP endpoint. All of the following routers participate:

  • /scans/*, /findings/*, /targets/*, /reports/*, /assets/*
  • /integrations/*, /schedules/*, /engagements/*
  • /repos/* (except /repos/install-url and /repos/callback, which are GitHub App handshake endpoints — session-only)
  • /sboms/*, /dependencies/*
  • /repeater/*, /intruder/*, /proxy/*, /traffic/*
  • /notes/*, /comments/*, /findings/{id}/assign, /findings/{id}/tags
  • /fix-proposals/*, /fix-tasks/*, /scans/{id}/fix-all, /repo-scans/{id}/fix-all, /findings/{kind}/{id}/propose_fix, /findings/{kind}/{id}/fix_proposal, /usage/fix-llm
  • /dashboard/*
  • /unified-findings/*

The current authoritative list is also available via:

curl https://api.pencheff.com/api/v1/api-keys/scopes \
  -H "Authorization: Bearer $CLERK_JWT"

Listing, updating, revoking

# List all your keys (does NOT return the secret)
curl /api/v1/api-keys -H "Authorization: Bearer $CLERK_JWT"

# Update name / scopes / expiry (cannot reissue the secret)
curl -X PATCH /api/v1/api-keys/$ID \
  -H "Authorization: Bearer $CLERK_JWT" \
  -d '{"scopes": ["scans:read"]}'

# Revoke (immediate; can't be undone)
curl -X DELETE /api/v1/api-keys/$ID \
  -H "Authorization: Bearer $CLERK_JWT"

Every create / update / revoke action is recorded in the audit_logs table tagged with the key ID for forensic traceability.

Plan limits

A single user can hold up to 50 active (non-revoked) keys across all their orgs. Revoke keys you no longer use to free a slot.

Recipes

CI/CD pipeline (read scans, export reports)

Mint a workspace-pinned key with the minimum needed scopes:

{
  "name": "Buildkite — main",
  "org_id": "org_01H...",
  "workspace_id": "ws_prod",
  "scopes": ["scans:write", "scans:read", "findings:read", "reports:export"],
  "expires_at": "2027-05-07T00:00:00Z"
}

The GitLab CI and Azure DevOps templates (apps/gitlab-ci, apps/azure-devops) currently run the local pencheff CLI, which does not call the hosted backend. If your pipeline talks to the hosted Pencheff API directly (custom curl steps, a thin internal CI agent, etc.), pass the key as the Authorization: Bearer … header — that is the only thing the API checks.

Read-only finding sync to a SIEM

{
  "name": "Splunk forwarder",
  "org_id": "org_01H...",
  "workspace_id": "ws_prod",
  "scopes": ["findings:read", "unified_findings:read"],
  "expires_at": null
}

One-org-many-workspaces automation

Org admins can mint a single org-scoped key and let the caller pick the workspace at request time:

{
  "name": "ACME bot — fan-out scanner",
  "org_id": "org_01H...",
  "workspace_id": null,
  "scopes": ["scans:write", "findings:read"]
}

The script must then send X-Workspace-Id on every request:

curl https://api.pencheff.com/scans \
  -H "Authorization: Bearer $PENCHEFF_API_KEY" \
  -H "X-Workspace-Id: ws_staging"

Security notes

  • Keys are stored as SHA-256 of the full token. The plaintext is shown only once at creation.
  • The full token has 256 bits of entropy from secrets.token_urlsafe, so plain SHA-256 (no bcrypt) is sufficient — the comparison is still constant-time (hmac.compare_digest).
  • Keys must be sent in the Authorization header. Query-string token passing is rejected for pcf_live_* to keep secrets out of logs and browser history.
  • A revoked key returns 401 on the very next request — there is no TTL.
  • A leaked key exposes only what its scopes allow on its pinned org / workspace. It cannot mint more keys, change billing, or modify org membership.