API reference

Pepper exposes a JSON REST API rooted at /api. Endpoints accept two authentication mechanisms: session cookies for the web UI, and bearer API keys for everything else (CI, IDE plugins, pre-commit, webhooks).

Authentication

For machine-to-machine, pass an API key in the Authorization header:

Authorization: Bearer ppr_xxxxxxxxxxxxxxxxxxxxxxxx

Create keys in Settings → API Keys or via POST /api/apikeys. Keys are shown once and stored hashed (SHA-256). Revoke with DELETE /api/apikeys/<id>.

All organisation-scoped endpoints return 403 when the caller is not a member of the org the resource belongs to.

Scans

Create a scan

POST /api/scans — multipart or JSON. The most common form is a tarball upload:

curl -X POST $PEPPER_API_URL/api/scans \
  -H "Authorization: Bearer $PEPPER_API_KEY" \
  -F "source=@source.tar.gz" \
  -F "scanType=FULL" \
  -F "branch=main" \
  -F "commitSha=$(git rev-parse HEAD)"
# → { "scanId": "cl…", "projectId": "cl…", "status": "QUEUED" }

Fields:

Read a scan

GET /api/scans/<scanId>
# → {
#   "id": "…", "status": "RUNNING|COMPLETED|FAILED|…",
#   "gateResult": "PASSED|FAILED|PENDING",
#   "criticalCount": 0, "highCount": 3, ...,
#   "scannerProgress": { "SAST_LLM": { "status": "RUNNING", "filesCompleted": 12, "filesTotal": 88 } }
# }

List scans

GET /api/scans?projectId=<id>&status=COMPLETED&limit=20

Lifecycle

Method & pathEffect
POST /api/scans/<id>/pausePause a running scan (worker honours it at safe points).
POST /api/scans/<id>/resumeResume a paused or stopped scan.
POST /api/scans/<id>/stopStop early; partial findings are kept.
POST /api/scans/<id>/cancelDiscard the scan (partial findings deleted).
POST /api/scans/<id>/rescanRe-queue the same source.

Artifacts & SBOMs

Every scan can produce up to seven artifact types stored in MinIO: SARIF, SBOM_CYCLONEDX, SBOM_SPDX, SCAN_LOG, CONTAINER_REPORT, DAST_REPORT, SIGNATURE.

GET /api/scans/<scanId>/artifacts
# → { "artifacts": [ { "type": "SBOM_CYCLONEDX", "size": 12453, "downloadPath": "..." }, … ] }

GET /api/scans/<scanId>/artifacts/cyclonedx     # CycloneDX 1.5 JSON
GET /api/scans/<scanId>/artifacts/spdx          # SPDX 2.3 JSON
GET /api/scans/<scanId>/artifacts/sarif         # SARIF v2 (when written)
GET /api/scans/<scanId>/artifacts/container     # Trivy raw report (when run)
GET /api/scans/<scanId>/artifacts/dast          # dapper report (when DAST ran)
GET /api/scans/<scanId>/artifacts/signature     # cosign / RSA signature bundle
GET /api/scans/<scanId>/artifacts/log           # JSON log lines

Findings

GET /api/scans/<scanId>/findings?severity=HIGH&status=OPEN&limit=100
# → { "findings": [ { "id": …, "severity": "HIGH", "title": …, "filePath": …, "startLine": … }, … ] }

PATCH /api/findings/<findingId>
{ "status": "RESOLVED" | "FALSE_POSITIVE" | "ACCEPTED_RISK" | "IN_PROGRESS" | "OPEN",
  "statusNote": "fixed in #1234" }

POST /api/findings/bulk
{ "ids": ["…","…"], "status": "FALSE_POSITIVE" }

POST /api/scans/<scanId>/findings/<findingId>/suggest-fix
# → { "diff": "--- a/foo.ts\n+++ b/foo.ts\n@@ …" }

POST /api/scans/<scanId>/findings/<findingId>/open-pr
{ "branch": "pepper/fix-cl123" }
# → { "url": "https://github.com/acme/foo/pull/42" }

GET /api/scans/<scanId>/findings/export?format=csv | html | sarif

Projects

GET    /api/projects
POST   /api/projects                              # create
GET    /api/projects/<projectId>
PATCH  /api/projects/<projectId>            # name, description, repoUrl,
                                                  # defaultBranch, dastTargetUrl
DELETE /api/projects/<projectId>
POST   /api/projects/<projectId>/schedule   # nightly / weekly cron

Integrations

GET    /api/integrations           # list all integrations
POST   /api/integrations           # upsert
DELETE /api/integrations/<id>
POST   /api/integrations/test      # dry-run with provided config

Body shape

// Slack
{ "kind": "SLACK", "name": "Security channel",
  "config": { "webhookUrl": "https://hooks.slack.com/services/…",
              "channel": "#security",
              "notifyOn": ["scan_complete","gate_failed"] } }

// Jira
{ "kind": "JIRA", "name": "Security (SEC)",
  "config": { "baseUrl": "https://acme.atlassian.net",
              "email": "bot@acme.com", "apiToken": "…",
              "projectKey": "SEC", "issueType": "Bug",
              "openForSeverities": ["CRITICAL","HIGH"] } }

// SIEM
{ "kind": "SIEM", "name": "Splunk HEC",
  "config": { "endpoint": "https://splunk.example.com:8088/services/collector/raw",
              "format": "cef" | "leef" | "json",
              "apiKey": "Splunk <token>" } }
// Syslog target also supported: "udp://siem.example.com:514"

// Dapper (DAST)
{ "kind": "DAST", "name": "Dapper",
  "config": { "endpoint": "http://dapper:8080", "apiKey": "…" } }

API keys

GET    /api/apikeys                # list (no plaintext)
POST   /api/apikeys                # { "name": "github-actions-prod",
                                     #   "expiresAt": "2027-01-01T00:00:00Z" }
                                     # → returns "plaintext" ONCE
DELETE /api/apikeys/<id>

Audit log

GET /api/audit-log?action=scan.queued&userId=…&limit=50&cursor=…
# → { "entries": [ { "id": …, "action": "scan.queued", "resource": "scan",
#                     "resourceId": "…", "user": { "id": …, "email": … },
#                     "ipAddress": "…", "details": {...}, "createdAt": "…" } ],
#     "nextCursor": "…" }
GET /api/dashboard/trends?days=30
# → {
#     "days": 30,
#     "series": [ { "date": "2026-05-01", "critical": 0, "high": 2,
#                    "medium": 5, "low": 8, "scans": 3, "gateFailed": 1 }, … ],
#     "mttr": { "CRITICAL": { "count": 4, "meanHours": 12.5 }, "HIGH": {…} }
#   }

Pre-commit

GET  /api/precommit/install.sh           # installer script (no auth)
POST /api/precommit/scan                 # API-key auth
{ "files": [ { "path": "src/foo.ts", "content": "…" }, … ],
  "failOn": ["CRITICAL","HIGH"] }
# → { "findings": [ … ], "summary": {…}, "block": true|false }

IDE plugin endpoint

GET /api/ide/findings?projectId=<id>&filePath=src/foo.ts&minSeverity=HIGH
Authorization: Bearer ppr_xxx
# → { "project": {…}, "scan": {…},
#     "findings": [ { "id": …, "severity": "HIGH", "title": …,
#                      "filePath": "src/foo.ts", "startLine": 42, … } ] }

CI/CD templates

GET /api/cicd-templates/github     # GitHub Actions workflow
GET /api/cicd-templates/gitlab     # .gitlab-ci.yml fragment
GET /api/cicd-templates/jenkins    # Jenkinsfile

Webhooks (inbound)

POST /api/webhooks/github
  X-Hub-Signature-256: sha256=…    # validated against GITHUB_WEBHOOK_SECRET
POST /api/webhooks/gitlab
  X-Gitlab-Token: …                # validated against GITLAB_WEBHOOK_SECRET

Both endpoints accept push + PR/MR events and queue an INCREMENTAL scan against the head commit.