{"openapi":"3.1.0","info":{"title":"Leonidus REST API","version":"2.4.0","summary":"Leonidus — Chief Security Officer AI for Fortune 500 — REST + MCP API reference.","description":"Leonidus is a Chief Security Officer AI companion that scans your code, ranks risks against industry frameworks, and surfaces authoritative remediation procedures from a 754-skill library.\n\n## Quick start\n1. Mint a Bearer key at [`/settings/api-keys`](https://app.leonidus.ai/settings/api-keys) (workspace-wide) or [`/repositories/<id>/keys`](https://app.leonidus.ai/repositories) (project-scoped — recommended).\n2. Curl any endpoint with `Authorization: Bearer pk_live_...`.\n3. Or install `npx @pisteyo/penta-mcp` for the Claude Code skill flow. See the dedicated [MCP reference at /mcp-docs](https://app.leonidus.ai/mcp-docs).\n\n## Authentication\nTwo schemes accepted, in this order:\n- **`bearerAuth`** — `pk_live_...` API key in `Authorization: Bearer <key>`. Used by CI/CD, MCP clients, and third-party tools. **Recommended for everything that isn't a browser session.**\n- **`cookieAuth`** — JWT cookie `pom-token` set by `/login`. Used by the Leonidus web UI.\n\nProject-scoped Bearer keys are **force-filtered** to their bound repository at every endpoint. Even if a tool queries `/api/v1/findings?fullName=other/repo` with a scoped key, the response is `403` and the key never sees other-repo data.\n\n## Client → Project → Repository hierarchy & isolation (v2.4.0)\nLeonidus organizes what you scan into a three-level hierarchy. A **Client** is a customer or account *inside* your workspace (a *tenant* is the whole workspace — clients live inside it). Each **Project** belongs to exactly one client and each **Repository** to one project. Compliance frameworks attach at client and/or project level; a project's *effective* frameworks are the union of both (`GET /api/v1/projects/{id}/frameworks`). Projects and repositories not yet under a client sit in an *unassigned* bucket (`GET /api/v1/hierarchy/map`) that stays visible workspace-wide.\n\n**Isolation semantics.** Clients are a permission boundary inside a workspace:\n- Contributor and viewer users who hold **≥ 1 client membership** are hard-scoped to those clients on every endpoint — clients, projects, repositories, scans, findings, compliance posture, hierarchy map and reports.\n- Users with **zero memberships** stay workspace-wide (back-compat — nothing changes until you create memberships).\n- **superadmin / admin / cso** always see everything.\n- **API keys inherit their creator's LIVE client scope.** Nothing is baked into the key: revoking a person's membership de-scopes every key they minted on the very next request; adding one narrows it. Keys minted by admin-tier users stay unrestricted.\n- **Assigning a repository team lead or contributor auto-creates that person's client membership**, so ownership can never point at someone scoped away from the repo they own.\n- **Scans snapshot `clientId` at creation** — moving a repo to another client later leaves historical scans, findings and evidence attributed to the original client.\n- Scoped callers get **`404`, never `403`**, for other clients' resources (observe-empty) — a scoped key cannot even confirm another customer exists.\n\nHierarchy endpoints are tagged **Clients**, **Projects** and **Hierarchy** below. Mutations (create/move/attach/assign/members) require an admin-tier role; reads honour the caller's scope. The MCP tools `penta_hierarchy`, `penta_client_posture` and `penta_client_report` wrap the read/report side.\n\n## Scopes\nEvery Bearer key carries a list of scopes. Default-minted keys grant: `scans:create`, `scans:view`, `findings:view`. Mint with `--scopes` for narrower keys (e.g. read-only). Scoped routes return `403` with `requiredPermission` + `yourScopes` when the check fails.\n\n## Rate limits\nLeonidus returns standard rate-limit headers on every response (success or 429):\n- `RateLimit-Limit` — total quota per window\n- `RateLimit-Remaining` — current window remaining\n- `RateLimit-Reset` — seconds until window reset\n- `Retry-After` — set on 429 only\n\nBuckets: `gate.create` 60/min, `gate.poll` 600/min, `mcp.tool` 120/min, `scans.create` 30/min, `auth.login` 5/min, `reports.generate` 20/min, `arch.bulk` 4/hr, `arch.portfolio` 3/hr.\n\n## Errors\nEvery error response is a JSON object with `error` (machine-readable code) + `message` (human-readable). 5xx responses include `requestId` you can grep in Vercel logs with `x-vercel-id`.\n\n## Versioning\nThe REST API is versioned via path prefix `/api/v1/`. Breaking changes ship under a new prefix (`/api/v2/`) with at least 6 months of overlap. Non-breaking additions (new fields, new endpoints) ship on the existing version — clients should ignore unknown fields. The Leonidus release version (`info.version` above) bumps independently and is purely informational.\n\n## Idempotency\nPOST endpoints that mutate state (`/scans/repo`, `/gate`) include a duplicate guard: a second identical request within 30 minutes returns `409 Conflict` with `conflictingScanId` so you can poll the original instead of re-running it.\n\n## MCP\nLeonidus exposes a remote Model Context Protocol server at `/api/mcp` (JSON-RPC 2.0 over HTTP). 19 tools available, including the read-only `penta_architecture_portfolio` fleet grid and the v2.4.0 hierarchy tools `penta_hierarchy`, `penta_client_posture` and `penta_client_report`. See the dedicated [MCP docs at `/mcp-docs`](https://app.leonidus.ai/mcp-docs) for full tool-by-tool reference + JSON-RPC examples + transport options (HTTP + stdio).","termsOfService":"https://app.leonidus.ai/legal/terms","contact":{"name":"Pisteyo Leonidus Support","url":"https://app.leonidus.ai/help","email":"support@pisteyo.com"},"license":{"name":"Leonidus API Terms of Service","url":"https://app.leonidus.ai/legal/terms"},"x-logo":{"url":"https://app.leonidus.ai/brand/penta-logo.png","altText":"Leonidus by Pisteyo"}},"externalDocs":{"description":"Full product docs + Skill library + MCP reference + changelog","url":"https://app.leonidus.ai/help"},"servers":[{"url":"https://app.leonidus.ai","description":"Production"}],"webhooks":{"gate-verdict":{"post":{"summary":"Gate verdict callback (Leonidus → customer)","description":"Posted by Leonidus when a gate scan reaches a terminal status (completed or failed) AND a `callbackUrl` was supplied on the original `/api/v1/gate` request. Up to 3 retries with exponential backoff.\n\n**Signature.** Body is HMAC-SHA256 signed with the workspace's webhook secret. Verify by computing `sha256=` + lowercase hex of `HMAC(secret, raw_body)` and comparing constant-time to the `x-penta-signature-256` header.\n\n**Idempotency.** Same `scanId` may be delivered up to 4 times across retries. Dedup on `scanId + status`.","operationId":"gate-verdict-webhook","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GateVerdictPayload"},"examples":{"allow":{"summary":"verdict: allow","value":{"scanId":"a096f305-d48b-465f-9491-044da0dc3ce1","status":"completed","verdict":"allow","blockReasons":[],"warnReasons":[],"findingsCount":{"critical":0,"high":0,"medium":3,"low":12,"info":5},"riskScore":22,"newFindingsVsPrior":0,"fixedFindingsVsPrior":1}},"block":{"summary":"verdict: block","value":{"scanId":"a096f305-d48b-465f-9491-044da0dc3ce1","status":"completed","verdict":"block","blockReasons":["3 findings at or above 'high' exceed gate threshold"],"findingsCount":{"critical":0,"high":3,"medium":5,"low":12,"info":0},"riskScore":78,"newFindingsVsPrior":3,"fixedFindingsVsPrior":0}}}}}},"parameters":[{"name":"x-penta-signature-256","in":"header","required":true,"schema":{"type":"string","example":"sha256=a3f2c1b45e20..."},"description":"`sha256=` + lowercase hex of HMAC-SHA256(workspace_secret, raw_body)."},{"name":"x-penta-delivery","in":"header","required":true,"schema":{"type":"string","format":"uuid"},"description":"Unique per-delivery UUID — use for idempotency."}],"responses":{"2XX":{"description":"Customer endpoint accepted the delivery. Leonidus does not retry on 2xx."},"4XX":{"description":"Leonidus logs but does not retry on 4xx."},"5XX":{"description":"Leonidus retries up to 3 times with exponential backoff (1s, 5s, 30s)."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"pk_live_<22-char-base64url>","description":"Workspace or project-scoped API key. Mint at [`/settings/api-keys`](https://app.leonidus.ai/settings/api-keys) (workspace-wide, for CI/CD) or [`/repositories/<id>/keys`](https://app.leonidus.ai/repositories) (project-scoped — recommended).\n\nFormat: `pk_live_<base64url>`. Plaintext is shown ONCE on mint. Leonidus stores only `sha256(key)`."},"cookieAuth":{"type":"apiKey","in":"cookie","name":"pom-token","description":"JWT session cookie set by `POST /api/v1/auth/login`. Used by the Leonidus web UI. NOT recommended for programmatic clients — use `bearerAuth` instead."}},"parameters":{"ScanId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"ScanRun UUID from `POST /scans/repo`.","example":"a096f305-d48b-465f-9491-044da0dc3ce1"},"FindingId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Finding UUID."},"SkillSlug":{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9-]+$"},"description":"CybersecuritySkill slug — kebab-case, lowercase.","example":"performing-jwt-none-algorithm-attack"},"ClientId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Client UUID. Client-scoped callers get `404` for clients they do not belong to."},"ProjectId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Project UUID. Out-of-scope projects return `404` for client-scoped callers."},"RepositoryId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Repository UUID (discover via `GET /api/v1/hierarchy/map`)."},"HierarchyReportId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Hierarchy report id returned by `POST /api/v1/reports/hierarchy`."},"ClientScopeQuery":{"name":"clientId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Narrow the response to one client. Client-scoped callers may only name clients they belong to — anything else is `404`. Never widens a scoped caller's view."},"ProjectScopeQuery":{"name":"projectId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Narrow the response to one project. Same scoping rules as `clientId`."}},"headers":{"RateLimit-Limit":{"description":"Total requests allowed in the current window.","schema":{"type":"integer","example":60}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer","example":57}},"RateLimit-Reset":{"description":"Seconds until the rate-limit window resets.","schema":{"type":"integer","example":47}},"Retry-After":{"description":"Seconds to wait before retrying (set on 429 only).","schema":{"type":"integer","example":47}},"x-vercel-id":{"description":"Edge region + correlation id. Include in support tickets.","schema":{"type":"string"}}},"responses":{"Unauthorized":{"description":"Missing or invalid Bearer / cookie.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missingAuth":{"summary":"HTTP 401","value":{"error":"Authentication required","message":"No Bearer token or cookie provided."}},"invalidKey":{"summary":"HTTP 401","value":{"error":"Invalid API key","message":"Bearer token is malformed, unknown, revoked, or expired."}}}}}},"Forbidden":{"description":"Authenticated, but key/role lacks required scope OR project-scoped key targeting different repo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missingScope":{"summary":"HTTP 403","value":{"error":"Forbidden","message":"API key does not grant 'reports:export'."}},"wrongRepo":{"summary":"HTTP 403","value":{"error":"Forbidden","message":"API key is scoped to a different repository."}}}}}},"NotFound":{"description":"Resource does not exist, belongs to a different workspace, OR is outside the caller's client scope (client-scoped users and keys get 404 — never 403 — for other clients' resources).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"Idempotency clash — same operation already in flight.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"conflictingScanId":{"type":"string","format":"uuid"}}}]},"examples":{"dup":{"summary":"HTTP 409","value":{"error":"Duplicate scan","message":"A scan for owner/repo:main is already running."}}}}}},"RateLimited":{"description":"Rate limit exceeded. See `Retry-After` header.","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ServerError":{"description":"Internal error. `requestId` correlates with logs via `x-vercel-id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Short machine-readable error code."},"message":{"type":"string","description":"Human-readable explanation; safe to surface to end-users."},"requestId":{"type":"string","nullable":true,"description":"Set on 5xx — correlate with logs via x-vercel-id."}}},"Severity":{"type":"string","enum":["critical","high","medium","low","info"]},"FindingStatus":{"type":"string","enum":["open","in_progress","fixed","accepted_risk","dismissed"]},"Verdict":{"type":"string","enum":["allow","warn","block","pending"]},"ScanStatus":{"type":"string","enum":["queued","scanning","analyzing","completed","failed","cancelled"]},"FindingsCount":{"type":"object","properties":{"critical":{"type":"integer","minimum":0},"high":{"type":"integer","minimum":0},"medium":{"type":"integer","minimum":0},"low":{"type":"integer","minimum":0},"info":{"type":"integer","minimum":0}}},"Finding":{"type":"object","required":["id","scanRunId","ruleId","engine","severity","title","status"],"properties":{"id":{"type":"string","format":"uuid"},"scanRunId":{"type":"string","format":"uuid"},"ruleId":{"type":"string","example":"javascript.lang.security.audit.sqli.tainted-sql-string"},"engine":{"type":"string","example":"semgrep"},"severity":{"$ref":"#/components/schemas/Severity"},"title":{"type":"string"},"cweId":{"type":"string","nullable":true,"example":"CWE-89"},"cveId":{"type":"string","nullable":true},"owaspCategory":{"type":"string","nullable":true,"example":"A03:2021 - Injection"},"filePath":{"type":"string","nullable":true},"lineStart":{"type":"integer","nullable":true,"minimum":1},"status":{"$ref":"#/components/schemas/FindingStatus"},"fingerprint":{"type":"string","nullable":true,"example":"F-A3F2C1B45E20"},"plainWhy":{"type":"string","nullable":true},"plainFix":{"type":"string","nullable":true},"dismissalCount":{"type":"integer","nullable":true},"relatedSkillSlugs":{"type":"array","items":{"type":"string"},"description":"v1.21.5+: top-N CybersecuritySkills slugs ranked for this finding.","example":["performing-jwt-none-algorithm-attack","implementing-secrets-management-with-vault"]}}},"FindingDetail":{"allOf":[{"$ref":"#/components/schemas/Finding"},{"type":"object","properties":{"description":{"type":"string"},"impact":{"type":"string","nullable":true},"lineEnd":{"type":"integer","nullable":true},"codeSnippet":{"type":"string","nullable":true},"remediation":{"type":"string","nullable":true},"remediationCode":{"type":"string","nullable":true},"complianceMapping":{"type":"string","nullable":true,"description":"JSON string of {framework: [controlIds]} mappings."},"relatedSkills":{"type":"array","description":"v1.21.5+: top-N skills resolved to {slug, title, subdomain, frameworks}.","items":{"type":"object","properties":{"slug":{"type":"string"},"title":{"type":"string"},"subdomain":{"type":"string"},"frameworks":{"type":"object","properties":{"mitreAttack":{"type":"array","items":{"type":"string"}},"nistCsf":{"type":"array","items":{"type":"string"}},"d3fend":{"type":"array","items":{"type":"string"}}}}}}}}}]},"Skill":{"type":"object","required":["slug","title","description","subdomain","body","license"],"properties":{"slug":{"type":"string","example":"performing-jwt-none-algorithm-attack"},"title":{"type":"string"},"description":{"type":"string"},"domain":{"type":"string","example":"cybersecurity"},"subdomain":{"type":"string","example":"api-security"},"verbPrefix":{"type":"string","example":"performing"},"tags":{"type":"array","items":{"type":"string"}},"body":{"type":"string","description":"Full procedure markdown — typically 5-15 KB per skill."},"whenToUse":{"type":"string","nullable":true},"prerequisites":{"type":"string","nullable":true},"frameworks":{"type":"object","properties":{"mitreAttack":{"type":"array","items":{"type":"string","example":"T1190"}},"mitreAtlas":{"type":"array","items":{"type":"string","example":"AML.T0051"}},"d3fend":{"type":"array","items":{"type":"string","example":"D3-MFA"}},"nistCsf":{"type":"array","items":{"type":"string","example":"PR.PS-01"}},"nistAiRmf":{"type":"array","items":{"type":"string","example":"GOVERN-6.1"}}}},"cweRefs":{"type":"array","items":{"type":"string","example":"CWE-89"}},"serviceTags":{"type":"array","items":{"type":"string","example":"aws-s3"}},"agentScriptPath":{"type":"string","nullable":true},"sourceUrl":{"type":"string","nullable":true,"format":"uri"},"license":{"type":"string","example":"Apache-2.0"},"author":{"type":"string","nullable":true},"importedAt":{"type":"string","format":"date-time"}}},"ScanRun":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/ScanStatus"},"inputType":{"type":"string","enum":["repo","url","paste","upload","image"]},"inputRef":{"type":"string","nullable":true},"branch":{"type":"string","nullable":true},"commitSha":{"type":"string","nullable":true},"repository":{"type":"string","nullable":true,"example":"owner/repo"},"summary":{"type":"object","properties":{"findingsCount":{"$ref":"#/components/schemas/FindingsCount"},"totalFiles":{"type":"integer","nullable":true},"totalLines":{"type":"integer","nullable":true},"scanDurationMs":{"type":"integer","nullable":true},"language":{"type":"string","nullable":true},"engine":{"type":"string","example":"semgrep+gitleaks+trivy"},"timestamp":{"type":"string","format":"date-time"}}},"findings":{"type":"array","items":{"$ref":"#/components/schemas/Finding"}},"aiAnalysis":{"type":"object","nullable":true,"additionalProperties":true},"createdAt":{"type":"string","format":"date-time"}}},"GatePolicy":{"type":"object","properties":{"minSeverity":{"type":"string","enum":["critical","high","medium","low","info","none"],"default":"high"},"maxRiskScore":{"type":"integer","minimum":0,"maximum":100,"default":70},"requireScanTypes":{"type":"array","items":{"type":"string","enum":["sast","dast","secrets","iac","deps"]}},"gateOnNewOnly":{"type":"boolean","default":false},"failOpen":{"type":"boolean","default":false}}},"GateVerdictPayload":{"type":"object","required":["scanId","verdict"],"properties":{"scanId":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/ScanStatus"},"verdict":{"$ref":"#/components/schemas/Verdict"},"blockReasons":{"type":"array","items":{"type":"string"}},"warnReasons":{"type":"array","items":{"type":"string"}},"findingsCount":{"$ref":"#/components/schemas/FindingsCount"},"riskScore":{"type":"integer","nullable":true,"minimum":0,"maximum":100},"riskLevel":{"type":"string","nullable":true},"newFindingsVsPrior":{"type":"integer","minimum":0},"fixedFindingsVsPrior":{"type":"integer","minimum":0},"policyApplied":{"$ref":"#/components/schemas/GatePolicy"},"scanUrl":{"type":"string","format":"uri"},"timestamp":{"type":"string","format":"date-time"}}},"ServiceDetectionResult":{"type":"object","properties":{"scanRunId":{"type":"string","format":"uuid"},"generatedAt":{"type":"string","format":"date-time"},"services":{"type":"array","items":{"type":"object","properties":{"identifier":{"type":"string","example":"aws-s3"},"name":{"type":"string","example":"AWS S3"},"vendor":{"type":"string"},"category":{"type":"string"},"thirdParty":{"type":"boolean"},"confidence":{"type":"integer","minimum":0,"maximum":100},"evidence":{"type":"array","items":{"type":"object","additionalProperties":true}}}}},"summary":{"type":"object","properties":{"totalServices":{"type":"integer"},"thirdPartyCount":{"type":"integer"},"internalCount":{"type":"integer"},"byCategory":{"type":"object","additionalProperties":{"type":"integer"}}}}}},"ComplianceDetectionResult":{"type":"object","properties":{"scanRunId":{"type":"string","format":"uuid"},"generatedAt":{"type":"string","format":"date-time"},"detectedFrameworks":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string","nullable":true},"applicability":{"type":"string","enum":["required","recommended","optional"]},"confidence":{"type":"integer","minimum":0,"maximum":100},"rationale":{"type":"string"},"signals":{"type":"array","items":{"type":"object","additionalProperties":true}}}}},"summary":{"type":"object","properties":{"totalSignals":{"type":"integer"},"requiredCount":{"type":"integer"},"recommendedCount":{"type":"integer"},"optionalCount":{"type":"integer"}}}}},"ApiKey":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"Production CI"},"prefix":{"type":"string","example":"pk_live_"},"scopes":{"type":"array","items":{"type":"string"}},"scopeType":{"type":"string","enum":["workspace","project"]},"repositoryId":{"type":"string","format":"uuid","nullable":true},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ApiKeyMint":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"object","properties":{"key":{"type":"string","description":"Plaintext key — shown ONCE. Save it now.","example":"pk_live_rhwCkHouzVy_qa4sItCq2sB3Kr5g2mww"}}}]},"PersonRef":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"email":{"type":"string","format":"email","nullable":true}}},"Client":{"type":"object","required":["id","name","slug","status"],"description":"A customer / account INSIDE a workspace. Acts as a permission boundary — see the *hierarchy & isolation* section above.","properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"name":{"type":"string","example":"Acme Corp"},"slug":{"type":"string","example":"acme-corp","description":"Generated from `name`; unique per workspace."},"description":{"type":"string","nullable":true},"industry":{"type":"string","nullable":true,"example":"fintech"},"contactName":{"type":"string","nullable":true},"contactEmail":{"type":"string","format":"email","nullable":true},"status":{"type":"string","enum":["active","archived"]},"frameworkSlugs":{"type":"array","items":{"type":"string"},"example":["soc2","hipaa"]},"projectCount":{"type":"integer","minimum":0},"memberCount":{"type":"integer","minimum":0},"createdBy":{"type":"string","nullable":true,"description":"Creator email."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ClientMember":{"type":"object","required":["userId","role"],"properties":{"userId":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"name":{"type":"string","nullable":true},"role":{"type":"string","enum":["member","manager"],"default":"member","description":"Client-level role. Workspace RBAC role still applies on top."},"addedBy":{"type":"string","nullable":true,"description":"Email of the admin who added them (or `auto` when created by a repo team-lead/contributor assignment)."},"createdAt":{"type":"string","format":"date-time"}}},"FrameworkAttachment":{"type":"object","required":["frameworkSlugs"],"properties":{"frameworkSlugs":{"type":"array","items":{"type":"string"},"description":"Frameworks attached directly at this level.","example":["soc2"]},"effectiveFrameworkSlugs":{"type":"array","items":{"type":"string"},"description":"Projects only: client frameworks ∪ project frameworks — what posture and reports use.","example":["soc2","pci-dss"]}}},"Project":{"type":"object","required":["id","name","slug"],"properties":{"id":{"type":"string","format":"uuid"},"workspaceId":{"type":"string","format":"uuid"},"clientId":{"type":"string","format":"uuid","nullable":true,"description":"`null` = unassigned (visible workspace-wide)."},"name":{"type":"string","example":"Payments"},"slug":{"type":"string","example":"payments"},"description":{"type":"string","nullable":true},"language":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","archived"]},"frameworkSlugs":{"type":"array","items":{"type":"string"}},"effectiveFrameworkSlugs":{"type":"array","items":{"type":"string"}},"repoCount":{"type":"integer","minimum":0},"scanCount":{"type":"integer","minimum":0},"createdBy":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"RepositoryPeople":{"type":"object","required":["contributors"],"properties":{"teamLead":{"allOf":[{"$ref":"#/components/schemas/PersonRef"}],"nullable":true,"description":"Exactly one owner per repository (`Repository.teamLeadId`)."},"contributors":{"type":"array","items":{"$ref":"#/components/schemas/PersonRef"}}}},"HierarchyRepo":{"type":"object","required":["id","fullName"],"properties":{"id":{"type":"string","format":"uuid"},"fullName":{"type":"string","example":"acme/payments-api"},"teamLead":{"allOf":[{"$ref":"#/components/schemas/PersonRef"}],"nullable":true},"contributors":{"type":"array","items":{"$ref":"#/components/schemas/PersonRef"}},"lastScan":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/ScanStatus"},"createdAt":{"type":"string","format":"date-time"},"riskScore":{"type":"integer","nullable":true}}}}},"HierarchyMap":{"type":"object","required":["clients","unassigned"],"description":"The whole tree, filtered to the caller's client scope. Unassigned rows are only returned to unrestricted callers.","properties":{"clients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["active","archived"]},"frameworkSlugs":{"type":"array","items":{"type":"string"}},"projects":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["active","archived"]},"frameworkSlugs":{"type":"array","items":{"type":"string"}},"effectiveFrameworkSlugs":{"type":"array","items":{"type":"string"}},"repos":{"type":"array","items":{"$ref":"#/components/schemas/HierarchyRepo"}}}}}}}},"unassigned":{"type":"object","properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"repos":{"type":"array","items":{"$ref":"#/components/schemas/HierarchyRepo"}}}}}},"HierarchyPerson":{"type":"object","required":["id","email"],"description":"One row per workspace user — answers 'who can see what'. Admin-tier users see every client regardless of the `clients` list.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"email":{"type":"string","format":"email"},"role":{"type":"string","description":"Workspace RBAC role (superadmin / admin / cso / contributor / viewer)."},"scoped":{"type":"boolean","description":"`true` when the user is hard-scoped (non-admin-tier with ≥ 1 membership)."},"clients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"role":{"type":"string","enum":["member","manager"]}}}},"teamLeadOf":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"fullName":{"type":"string"}}}},"contributorOf":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"fullName":{"type":"string"}}}}}},"HierarchyReport":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","generating","completed","failed"]},"clientId":{"type":"string","format":"uuid","nullable":true},"projectId":{"type":"string","format":"uuid","nullable":true},"frameworkSlugs":{"type":"array","items":{"type":"string"},"description":"Effective frameworks covered."},"createdBy":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time","nullable":true},"error":{"type":"string","nullable":true,"description":"Set when `status = failed`."},"exportUrl":{"type":"string","nullable":true,"example":"/api/v1/reports/hierarchy/7f3e.../export?format=html","description":"Set once `status = completed`."}}}}},"security":[{"bearerAuth":[]},{"cookieAuth":[]}],"tags":[{"name":"Public","description":"No auth required.","externalDocs":{"url":"https://app.leonidus.ai/help"}},{"name":"Scans","description":"Trigger and inspect scans."},{"name":"Findings","description":"Per-finding read and workflow.","externalDocs":{"url":"https://app.leonidus.ai/findings"}},{"name":"Gate","description":"Pre-deployment gate for CI/CD pipelines."},{"name":"Reports","description":"HTML / PDF / Excel / SARIF / SBOM exports."},{"name":"Skills","description":"754-skill CybersecuritySkills library (Apache-2.0)."},{"name":"Compliance","description":"167 frameworks; posture; auto-detection."},{"name":"Services","description":"Third-party service inventory."},{"name":"API Keys","description":"Workspace + project-scoped key management."},{"name":"Clients","description":"v2.4.0 — Clients (customers / accounts inside a workspace): CRUD, members, frameworks, posture. A client is a permission boundary — see *hierarchy & isolation* above.","externalDocs":{"description":"Help: Clients, Projects & Repositories","url":"https://app.leonidus.ai/help#hierarchy-clients-projects-repos"}},{"name":"Projects","description":"v2.4.0 — Projects belong to exactly one client; attach repositories and project-level frameworks here."},{"name":"Hierarchy","description":"v2.4.0 — Whole-tree map, the people view (who can see what), repository team lead / contributors, and per-client / per-project compliance reports.","externalDocs":{"description":"Help: Client-level access isolation","url":"https://app.leonidus.ai/help#client-access-isolation"}},{"name":"MCP","description":"Remote Model Context Protocol endpoint.","externalDocs":{"description":"Full MCP tool reference","url":"https://app.leonidus.ai/mcp-docs"}}],"paths":{"/api/v1/changelog":{"get":{"tags":["Public"],"summary":"Release notes","operationId":"getChangelog","security":[],"responses":{"200":{"description":"Versions newest-first.","content":{"application/json":{"example":{"versions":[{"version":"2.2.0","title":"Chief AI Architect: dynamic tabs, scorecards, portfolio & background bulk generation","releasedAt":"2026-08-28T22:00:00Z"}]}}}}}}},"/api/v1/help":{"get":{"tags":["Public"],"summary":"Help articles","operationId":"getHelp","security":[],"responses":{"200":{"description":"All published help articles."}}}},"/skill/penta-review.md":{"get":{"tags":["Public"],"summary":"Download the Leonidus Claude Code Skill markdown","operationId":"downloadPentaSkill","security":[],"responses":{"200":{"description":"Skill markdown.","content":{"text/markdown":{}}}}}},"/api/v1/scans/repo":{"post":{"tags":["Scans"],"summary":"Scan a linked repository","description":"Leonidus clones the repo server-side and runs Semgrep + Trivy + Gitleaks + Leonidus-secrets. Returns `scanId` immediately; poll `/api/v1/scans/<id>` or `/api/v1/gate/<id>` for status. Idempotent: duplicate within 30 min returns 409 with `conflictingScanId`.","operationId":"scanRepo","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fullName"],"properties":{"fullName":{"type":"string","example":"gitpisteyo/penta-vuln-sandbox"},"branch":{"type":"string","default":"main"},"commitSha":{"type":"string","nullable":true},"repositoryId":{"type":"string","format":"uuid","nullable":true}}},"examples":{"minimal":{"summary":"Scan default branch","value":{"fullName":"owner/repo"}},"pinnedCommit":{"summary":"Scan a specific commit","value":{"fullName":"owner/repo","branch":"feature/abc","commitSha":"a3f2c1b"}}}}}},"responses":{"200":{"description":"Scan queued.","content":{"application/json":{"example":{"id":"a096f305-d48b-465f-9491-044da0dc3ce1","status":"queued","inputType":"repo","repository":"owner/repo","branch":"main"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X POST https://app.leonidus.ai/api/v1/scans/repo \\\n  -H \"Authorization: Bearer $PENTA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"fullName\":\"owner/repo\",\"branch\":\"main\"}'"},{"lang":"JavaScript","label":"fetch","source":"const res = await fetch(\"https://app.leonidus.ai/api/v1/scans/repo\", {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.PENTA_API_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ fullName: \"owner/repo\", branch: \"main\" }),\n});\nconst { id } = await res.json();"},{"lang":"Python","label":"requests","source":"import os, requests\nr = requests.post(\n    \"https://app.leonidus.ai/api/v1/scans/repo\",\n    headers={\"Authorization\": f\"Bearer {os.environ['PENTA_API_KEY']}\"},\n    json={\"fullName\": \"owner/repo\", \"branch\": \"main\"},\n)\nr.raise_for_status()\nprint(\"scan:\", r.json()[\"id\"])"}]}},"/api/v1/scans/paste":{"post":{"tags":["Scans"],"summary":"Ad-hoc scan of pasted file snippets","description":"For workspaces without a linked repo (demo / tire-kicker). Synchronous — runs inline.","operationId":"scanPaste","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string"},"language":{"type":"string","example":"javascript"}}}}}},"responses":{"200":{"description":"Scan completed inline."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/scans/url":{"post":{"tags":["Scans"],"summary":"DAST scan against a URL","description":"Quick or deep URL pen test. Use `depth: 'deep'` for active fuzz (rate-limited 2/day/workspace).","operationId":"scanUrl","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","example":"https://staging.example.com"},"depth":{"type":"string","enum":["quick","deep"],"default":"quick"}}}}}},"responses":{"200":{"description":"Scan queued."},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/scans/{id}":{"get":{"tags":["Scans"],"summary":"Scan detail","operationId":"getScan","parameters":[{"$ref":"#/components/parameters/ScanId"}],"responses":{"200":{"description":"Full scan record incl. findings + AI analysis + relatedSkillSlugs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanRun"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $PENTA_API_KEY\" \\\n  https://app.leonidus.ai/api/v1/scans/a096f305-d48b-465f-9491-044da0dc3ce1"},{"lang":"JavaScript","label":"fetch","source":"const scan = await fetch(`https://app.leonidus.ai/api/v1/scans/${id}`, {\n  headers: { Authorization: `Bearer ${process.env.PENTA_API_KEY}` }\n}).then(r => r.json());"},{"lang":"Python","label":"requests","source":"r = requests.get(f\"https://app.leonidus.ai/api/v1/scans/{id}\",\n    headers={\"Authorization\": f\"Bearer {KEY}\"})\nscan = r.json()"}]},"delete":{"tags":["Scans"],"summary":"Delete a scan + all artifacts","operationId":"deleteScan","security":[{"cookieAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ScanId"}],"responses":{"200":{"description":"Deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/scans/{id}/sarif":{"get":{"tags":["Scans","Reports"],"summary":"Download SARIF for the scan","description":"OASIS SARIF 2.1.0. v1.21.5+: each result has `properties.skills[]` listing CybersecuritySkills slugs relevant to the rule.","operationId":"getScanSarif","parameters":[{"$ref":"#/components/parameters/ScanId"}],"responses":{"200":{"description":"SARIF JSON.","content":{"application/json":{}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $PENTA_API_KEY\" \\\n  https://app.leonidus.ai/api/v1/scans/$ID/sarif > findings.sarif"},{"lang":"JavaScript","label":"fetch","source":"const sarif = await fetch(url, { headers }).then(r => r.text());"},{"lang":"Python","label":"requests","source":"requests.get(url, headers={\"Authorization\":f\"Bearer {KEY}\"}).json()"}]}},"/api/v1/scans/{id}/sbom":{"get":{"tags":["Scans","Reports"],"summary":"Download SBOM","operationId":"getScanSbom","parameters":[{"$ref":"#/components/parameters/ScanId"},{"name":"format","in":"query","schema":{"type":"string","enum":["spdx","cyclonedx"],"default":"spdx"}}],"responses":{"200":{"description":"SPDX-JSON or CycloneDX."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/scans/{id}/history":{"get":{"tags":["Scans"],"summary":"Prior scans for the same target","operationId":"getScanHistory","parameters":[{"$ref":"#/components/parameters/ScanId"}],"responses":{"200":{"description":"Newest-first list of prior scans."}}}},"/api/v1/scans/{id}/compliance-detection":{"get":{"tags":["Scans","Compliance"],"summary":"Auto-detected applicable compliance frameworks","description":"v1.21.2: heuristic engine ranks frameworks as Required / Recommended / Optional based on detected services, dependencies, and finding patterns.","operationId":"getScanComplianceDetection","parameters":[{"$ref":"#/components/parameters/ScanId"}],"responses":{"200":{"description":"Detection result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceDetectionResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/scans/{id}/services":{"get":{"tags":["Scans","Services"],"summary":"Third-party services + dependencies + infra","description":"v1.21.4: 100+ service catalog grouped into 20+ categories (cloud-aws/azure/gcp, databases, queues, observability, AI/ML, etc.).","operationId":"getScanServices","parameters":[{"$ref":"#/components/parameters/ScanId"}],"responses":{"200":{"description":"Service inventory.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceDetectionResult"}}}}}}},"/api/v1/scans/{id}/skills-rollup":{"get":{"tags":["Scans","Skills"],"summary":"Per-scan skill enrichment summary","description":"v1.21.5: returns totalDistinctSkills + byDomain + topSkills. Recomputed on demand for legacy scans.","operationId":"getScanSkillsRollup","parameters":[{"$ref":"#/components/parameters/ScanId"}],"responses":{"200":{"description":"Rollup."}}}},"/api/v1/scans/{id}/reconcile":{"post":{"tags":["Scans","MCP"],"summary":"Engineer reports findings fixed (batch)","description":"Called by `npx @pisteyo/penta-mcp report` or the MCP `penta_report_completed` tool. Marks findings as `fixed` by fingerprint.","operationId":"reconcileFindings","parameters":[{"$ref":"#/components/parameters/ScanId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["completed"],"properties":{"completed":{"type":"array","items":{"type":"string"},"example":["F-A3F2C1B45E20","F-7C2A8E9D1F45"]},"commit":{"type":"string","nullable":true,"description":"Resolving commit SHA."}}}}}},"responses":{"200":{"description":"Result.","content":{"application/json":{"example":{"scanId":"a096f305...","updated":2,"notFound":[]}}}}}}},"/api/v1/findings":{"get":{"tags":["Findings"],"summary":"List findings","description":"Filter by scan / source / severity / status. Project-scoped Bearer keys force-filter to their bound repo. v2.4.0: results are also filtered to the caller's client scope (see *hierarchy & isolation*); `clientId` / `projectId` narrow further but never widen.","operationId":"listFindings","parameters":[{"name":"scanRunId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"fullName","in":"query","schema":{"type":"string"},"description":"Repo `owner/repo`. With `latest=true`, resolves to the latest completed scan."},{"name":"latest","in":"query","schema":{"type":"boolean"}},{"name":"severity","in":"query","schema":{"type":"array","items":{"$ref":"#/components/schemas/Severity"}},"style":"form","explode":true},{"name":"status","in":"query","schema":{"type":"string","enum":["open","in_progress","fixed","accepted_risk","dismissed","all"]}},{"name":"source","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":200}},{"name":"q","in":"query","schema":{"type":"string"},"description":"Search in title / ruleId / filePath / cweId."},{"$ref":"#/components/parameters/ClientScopeQuery"},{"$ref":"#/components/parameters/ProjectScopeQuery"}],"responses":{"200":{"description":"Findings list.","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer"},"findings":{"type":"array","items":{"$ref":"#/components/schemas/Finding"}}}}}}}}}},"/api/v1/findings/{id}":{"get":{"tags":["Findings"],"summary":"Finding detail with related skills","operationId":"getFinding","parameters":[{"$ref":"#/components/parameters/FindingId"}],"responses":{"200":{"description":"Finding.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindingDetail"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Findings"],"summary":"Update finding workflow state","operationId":"updateFinding","security":[{"cookieAuth":[]}],"parameters":[{"$ref":"#/components/parameters/FindingId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"assignedTo":{"type":"string","format":"uuid","nullable":true},"status":{"$ref":"#/components/schemas/FindingStatus"},"dismissedReason":{"type":"string","description":"Required when status=dismissed."},"dueAt":{"type":"string","format":"date-time","nullable":true}}}}}},"responses":{"200":{"description":"Updated finding."}}}},"/api/v1/gate":{"post":{"tags":["Gate"],"summary":"Pre-deployment gate verdict","description":"Returns `verdict: allow | warn | block | pending`. Sync long-poll (up to 600s) or async with `callbackUrl`. **Bearer required** (cookie not accepted).","operationId":"createGateVerdict","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["input"],"properties":{"input":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["repo","url"]},"fullName":{"type":"string","description":"Required when type=repo."},"branch":{"type":"string"},"commitSha":{"type":"string"},"url":{"type":"string","format":"uri","description":"Required when type=url."},"depth":{"type":"string","enum":["quick","deep"]}}},"policy":{"$ref":"#/components/schemas/GatePolicy"},"callbackUrl":{"type":"string","format":"uri","description":"Optional HMAC-signed webhook target. See `webhooks.gate-verdict`."},"wait":{"type":"boolean","default":false},"maxWaitSec":{"type":"integer","minimum":1,"maximum":600,"default":300}}},"examples":{"ci":{"summary":"CI long-poll with policy override","value":{"input":{"type":"repo","fullName":"owner/repo","commitSha":"a3f2c1b"},"policy":{"minSeverity":"high","gateOnNewOnly":true},"wait":true,"maxWaitSec":300}},"async":{"summary":"Async with webhook callback","value":{"input":{"type":"repo","fullName":"owner/repo"},"callbackUrl":"https://ci.example.com/penta-gate-cb"}}}}}},"responses":{"200":{"description":"Verdict envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GateVerdictPayload"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Long-poll until verdict\ncurl -X POST \"https://app.leonidus.ai/api/v1/gate?wait=true&maxWaitSec=300\" \\\n  -H \"Authorization: Bearer $PENTA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"input\":{\"type\":\"repo\",\"fullName\":\"owner/repo\",\"commitSha\":\"'$(git rev-parse HEAD)'\"},\n       \"policy\":{\"minSeverity\":\"high\",\"gateOnNewOnly\":true}}'"},{"lang":"JavaScript","label":"fetch","source":"const verdict = await fetch(\"https://app.leonidus.ai/api/v1/gate?wait=true\", {\n  method: \"POST\",\n  headers: { Authorization: `Bearer ${KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({\n    input: { type: \"repo\", fullName: \"owner/repo\", commitSha: process.env.GIT_SHA },\n    policy: { minSeverity: \"high\", gateOnNewOnly: true },\n    wait: true, maxWaitSec: 300,\n  }),\n}).then(r => r.json());\nif (verdict.verdict === \"block\") process.exit(1);"},{"lang":"Python","label":"requests","source":"r = requests.post(\"https://app.leonidus.ai/api/v1/gate\",\n    params={\"wait\": \"true\", \"maxWaitSec\": \"300\"},\n    headers={\"Authorization\": f\"Bearer {KEY}\"},\n    json={\"input\": {\"type\": \"repo\", \"fullName\": \"owner/repo\", \"commitSha\": sha},\n          \"policy\": {\"minSeverity\": \"high\", \"gateOnNewOnly\": True}})\nimport sys; sys.exit(1) if r.json().get(\"verdict\") == \"block\" else None"}]}},"/api/v1/gate/{scanId}":{"get":{"tags":["Gate"],"summary":"Poll gate verdict by scan id","operationId":"getGateVerdict","security":[{"bearerAuth":[]}],"parameters":[{"name":"scanId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Verdict.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GateVerdictPayload"}}}}}}},"/api/v1/skills/by-slug/{slug}":{"get":{"tags":["Skills"],"summary":"Fetch one CybersecuritySkill","description":"Returns full procedure body (5-15 KB typically) + framework mappings + Apache-2.0 source attribution. 754 skills imported.","operationId":"getSkillBySlug","parameters":[{"$ref":"#/components/parameters/SkillSlug"}],"responses":{"200":{"description":"Skill.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Skill"}}}},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $PENTA_API_KEY\" \\\n  https://app.leonidus.ai/api/v1/skills/by-slug/performing-jwt-none-algorithm-attack | jq ."},{"lang":"JavaScript","label":"fetch","source":"const skill = await fetch(`https://app.leonidus.ai/api/v1/skills/by-slug/${slug}`, {\n  headers: { Authorization: `Bearer ${KEY}` }\n}).then(r => r.json());"},{"lang":"Python","label":"requests","source":"skill = requests.get(f\"https://app.leonidus.ai/api/v1/skills/by-slug/{slug}\",\n    headers={\"Authorization\": f\"Bearer {KEY}\"}).json()"}]}},"/api/v1/compliance/posture":{"get":{"tags":["Compliance","Clients"],"summary":"Compliance posture — workspace, client or project","description":"Pass/fail/pending per framework. Workspace-wide by default (filtered to the caller's client scope). v2.4.0: add `clientId` **or** `projectId` (not both) to scope to one client / project — the response then covers that scope's *effective* frameworks and only scans + evidence attributed to it. Out-of-scope ids return `404`.","operationId":"getCompliancePosture","parameters":[{"name":"framework","in":"query","schema":{"type":"string"},"description":"Filter to one framework slug (e.g. `soc2`, `hipaa`, `mitre-d3fend`)."},{"$ref":"#/components/parameters/ClientScopeQuery"},{"$ref":"#/components/parameters/ProjectScopeQuery"}],"responses":{"200":{"description":"Posture summary.","content":{"application/json":{"example":{"scope":{"clientId":"c1a2b3c4-...","name":"Acme Corp","frameworkSlugs":["soc2"]},"posture":[{"slug":"soc2","name":"SOC 2","totalControls":67,"passing":47,"failing":12,"pending":8,"passingPercent":70,"failingPercent":18,"pendingPercent":12}]}}}},"400":{"description":"Both `clientId` and `projectId` supplied."},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $PENTA_API_KEY\" \\\n  \"https://app.leonidus.ai/api/v1/compliance/posture?clientId=$CLIENT_ID\""},{"lang":"JavaScript","label":"fetch","source":"const posture = await fetch(`https://app.leonidus.ai/api/v1/compliance/posture?clientId=${clientId}`, {\n  headers: { Authorization: `Bearer ${KEY}` }\n}).then(r => r.json());"},{"lang":"Python","label":"requests","source":"posture = requests.get(\"https://app.leonidus.ai/api/v1/compliance/posture\",\n    params={\"clientId\": client_id}, headers={\"Authorization\": f\"Bearer {KEY}\"}).json()"}]}},"/api/v1/clients":{"get":{"tags":["Clients"],"summary":"List clients","description":"Every client the caller may see. Client-scoped users/keys get only the clients they belong to; admin-tier roles and zero-membership users get all of them.","operationId":"listClients","responses":{"200":{"description":"Clients.","content":{"application/json":{"schema":{"type":"object","properties":{"clients":{"type":"array","items":{"$ref":"#/components/schemas/Client"}},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $PENTA_API_KEY\" https://app.leonidus.ai/api/v1/clients"},{"lang":"JavaScript","label":"fetch","source":"const { clients } = await fetch(\"https://app.leonidus.ai/api/v1/clients\", {\n  headers: { Authorization: `Bearer ${KEY}` }\n}).then(r => r.json());"},{"lang":"Python","label":"requests","source":"clients = requests.get(\"https://app.leonidus.ai/api/v1/clients\",\n    headers={\"Authorization\": f\"Bearer {KEY}\"}).json()[\"clients\"]"}]},"post":{"tags":["Clients"],"summary":"Create a client","description":"Admin-tier only (superadmin / admin / cso). The slug is generated from `name` and must be unique per workspace.","operationId":"createClient","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":200,"example":"Acme Corp"},"description":{"type":"string","maxLength":1000,"nullable":true},"industry":{"type":"string","maxLength":100,"nullable":true},"contactName":{"type":"string","maxLength":200,"nullable":true},"contactEmail":{"type":"string","format":"email","nullable":true}}}}}},"responses":{"200":{"description":"Created client.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"A client with this name/slug already exists in the workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/clients/{id}":{"get":{"tags":["Clients"],"summary":"Client detail","description":"Client with its projects, attached frameworks and member count. `404` when the client is outside the caller's scope.","operationId":"getClient","parameters":[{"$ref":"#/components/parameters/ClientId"}],"responses":{"200":{"description":"Client.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Clients"],"summary":"Update a client","description":"Admin-tier only. Renaming keeps `id` and `slug` stable. `status: archived` hides the client from default lists without deleting history.","operationId":"updateClient","parameters":[{"$ref":"#/components/parameters/ClientId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":1000,"nullable":true},"industry":{"type":"string","maxLength":100,"nullable":true},"contactName":{"type":"string","maxLength":200,"nullable":true},"contactEmail":{"type":"string","format":"email","nullable":true},"status":{"type":"string","enum":["active","archived"]}}}}}},"responses":{"200":{"description":"Updated client.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Clients"],"summary":"Delete a client","description":"Admin-tier only. Memberships and framework attachments are removed; projects under the client become unassigned (they are not deleted). Scans keep their snapshotted `clientId`.","operationId":"deleteClient","parameters":[{"$ref":"#/components/parameters/ClientId"}],"responses":{"200":{"description":"Deleted."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/clients/{id}/members":{"get":{"tags":["Clients"],"summary":"List client members","description":"Who belongs to this client. Admin-tier users are not listed but can always see the client.","operationId":"listClientMembers","parameters":[{"$ref":"#/components/parameters/ClientId"}],"responses":{"200":{"description":"Members.","content":{"application/json":{"schema":{"type":"object","properties":{"members":{"type":"array","items":{"$ref":"#/components/schemas/ClientMember"}}}}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Clients"],"summary":"Add a member","description":"Admin-tier only. The user must already exist in the workspace. For a contributor/viewer this may be the membership that flips them from workspace-wide to client-scoped — and every API key they minted narrows on its next request. Idempotent (`409` if already a member).","operationId":"addClientMember","parameters":[{"$ref":"#/components/parameters/ClientId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["member","manager"],"default":"member"}}}}}},"responses":{"200":{"description":"Membership.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientMember"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Already a member."}}},"delete":{"tags":["Clients"],"summary":"Remove a member","description":"Admin-tier only. If this was the user's last membership they become workspace-wide again (back-compat); their keys follow on the next request.","operationId":"removeClientMember","parameters":[{"$ref":"#/components/parameters/ClientId"},{"name":"userId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Removed."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/clients/{id}/frameworks":{"get":{"tags":["Clients","Compliance"],"summary":"Frameworks attached to a client","operationId":"getClientFrameworks","parameters":[{"$ref":"#/components/parameters/ClientId"}],"responses":{"200":{"description":"Attached frameworks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameworkAttachment"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Clients","Compliance"],"summary":"Replace the client's frameworks","description":"Admin-tier only. **Replaces** the whole list — send the complete set. Applies to every project and repository under the client.","operationId":"setClientFrameworks","parameters":[{"$ref":"#/components/parameters/ClientId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["frameworkSlugs"],"properties":{"frameworkSlugs":{"type":"array","items":{"type":"string"},"example":["soc2","hipaa"]}}}}}},"responses":{"200":{"description":"Attached frameworks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameworkAttachment"}}}},"400":{"description":"Unknown framework slug."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/clients/{id}/posture":{"get":{"tags":["Clients","Compliance"],"summary":"Client compliance posture","description":"Shorthand for `GET /api/v1/compliance/posture?clientId={id}` — posture over the client's attached frameworks using only scans + evidence attributed to it.","operationId":"getClientPosture","parameters":[{"$ref":"#/components/parameters/ClientId"},{"name":"framework","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Posture summary."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List projects","description":"Projects the caller may see (client scope applies). Unassigned projects (`clientId: null`) are visible to unrestricted callers only.","operationId":"listProjects","responses":{"200":{"description":"Projects.","content":{"application/json":{"schema":{"type":"object","properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"total":{"type":"integer"}}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","description":"Requires `repos:add`. Pass `clientId` to place it under a client immediately, or `PATCH /api/v1/projects/{id}` later.","operationId":"createProject","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":100},"description":{"type":"string","maxLength":500},"language":{"type":"string","maxLength":50},"clientId":{"type":"string","format":"uuid","nullable":true}}}}}},"responses":{"200":{"description":"Created project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"409":{"description":"Project with this name already exists."}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Project detail","operationId":"getProject","parameters":[{"$ref":"#/components/parameters/ProjectId"}],"responses":{"200":{"description":"Project with repositories + effective frameworks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Projects"],"summary":"Update / move a project","description":"Admin-tier only. Setting `clientId` moves the project (and its repositories) under another client; future scans snapshot the new client, historical scans keep the old one.","operationId":"updateProject","parameters":[{"$ref":"#/components/parameters/ProjectId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":100},"description":{"type":"string","maxLength":500,"nullable":true},"clientId":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["active","archived"]}}}}}},"responses":{"200":{"description":"Updated project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Projects"],"summary":"Delete a project","description":"Admin-tier only. Repositories under it become unassigned (not deleted). `409` while repositories are still attached in deployments that require detaching first.","operationId":"deleteProject","parameters":[{"$ref":"#/components/parameters/ProjectId"}],"responses":{"200":{"description":"Deleted."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/projects/{id}/frameworks":{"get":{"tags":["Projects","Compliance"],"summary":"Project frameworks (own + effective)","description":"`frameworkSlugs` = attached directly to the project; `effectiveFrameworkSlugs` = client frameworks ∪ project frameworks — what posture and reports use.","operationId":"getProjectFrameworks","parameters":[{"$ref":"#/components/parameters/ProjectId"}],"responses":{"200":{"description":"Frameworks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameworkAttachment"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Projects","Compliance"],"summary":"Replace the project's own frameworks","description":"Admin-tier only. Replaces the project-level list; frameworks inherited from the client cannot be removed here.","operationId":"setProjectFrameworks","parameters":[{"$ref":"#/components/parameters/ProjectId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["frameworkSlugs"],"properties":{"frameworkSlugs":{"type":"array","items":{"type":"string"},"example":["pci-dss"]}}}}}},"responses":{"200":{"description":"Frameworks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameworkAttachment"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/projects/{id}/repositories":{"post":{"tags":["Projects"],"summary":"Attach a repository","description":"Admin-tier only. A repository belongs to at most one project — attaching it here moves it from wherever it was (including the unassigned bucket).","operationId":"attachProjectRepository","parameters":[{"$ref":"#/components/parameters/ProjectId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["repositoryId"],"properties":{"repositoryId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Attached."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Already attached to this project."}}},"delete":{"tags":["Projects"],"summary":"Detach a repository","description":"Admin-tier only. The repository becomes unassigned (visible workspace-wide). Historical scans keep their snapshotted `clientId`.","operationId":"detachProjectRepository","parameters":[{"$ref":"#/components/parameters/ProjectId"},{"name":"repositoryId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Detached."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/repositories/{id}/people":{"get":{"tags":["Hierarchy"],"summary":"Repository team lead + contributors","operationId":"getRepositoryPeople","parameters":[{"$ref":"#/components/parameters/RepositoryId"}],"responses":{"200":{"description":"People.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepositoryPeople"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Hierarchy"],"summary":"Set the team lead (owner)","description":"Admin-tier only. Exactly one lead per repository — the previous lead is replaced; `null` clears. If the repository sits under a client, the lead's client membership is auto-created (idempotent), which may make a contributor/viewer client-scoped.","operationId":"setRepositoryTeamLead","parameters":[{"$ref":"#/components/parameters/RepositoryId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["teamLeadId"],"properties":{"teamLeadId":{"type":"string","format":"uuid","nullable":true}}}}}},"responses":{"200":{"description":"People.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepositoryPeople"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/repositories/{id}/contributors":{"post":{"tags":["Hierarchy"],"summary":"Add a contributor","description":"Admin-tier only. Auto-creates the contributor's membership in the repository's client (if any).","operationId":"addRepositoryContributor","parameters":[{"$ref":"#/components/parameters/RepositoryId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"People.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepositoryPeople"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Already a contributor."}}},"delete":{"tags":["Hierarchy"],"summary":"Remove a contributor","description":"Admin-tier only. Removes the repo assignment only — the client membership is intentionally kept; remove it via `DELETE /api/v1/clients/{id}/members` if the person should lose the whole client.","operationId":"removeRepositoryContributor","parameters":[{"$ref":"#/components/parameters/RepositoryId"},{"name":"userId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Removed."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/hierarchy/map":{"get":{"tags":["Hierarchy"],"summary":"Whole-tree map (clients → projects → repos + unassigned)","description":"Filtered to the caller's client scope. Unrestricted callers also get the `unassigned` bucket of projects/repos not yet under a client. Wrapped by MCP `penta_hierarchy({ view: 'map' })`.","operationId":"getHierarchyMap","responses":{"200":{"description":"Tree.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HierarchyMap"},"example":{"clients":[{"id":"c1a2b3c4-...","name":"Acme Corp","slug":"acme-corp","status":"active","frameworkSlugs":["soc2"],"projects":[{"id":"p1...","name":"Payments","slug":"payments","status":"active","frameworkSlugs":["pci-dss"],"effectiveFrameworkSlugs":["soc2","pci-dss"],"repos":[{"id":"r1...","fullName":"acme/payments-api","teamLead":{"id":"u1...","name":"Alice Ng","email":"alice@acme.com"},"contributors":[],"lastScan":{"id":"a096f305-...","status":"completed","createdAt":"2026-08-30T10:12:00Z","riskScore":22}}]}]}],"unassigned":{"projects":[],"repos":[{"id":"r9...","fullName":"acme/legacy-tools","teamLead":null,"contributors":[],"lastScan":null}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $PENTA_API_KEY\" https://app.leonidus.ai/api/v1/hierarchy/map | jq '.unassigned.repos[].fullName'"},{"lang":"JavaScript","label":"fetch","source":"const map = await fetch(\"https://app.leonidus.ai/api/v1/hierarchy/map\", {\n  headers: { Authorization: `Bearer ${KEY}` }\n}).then(r => r.json());"},{"lang":"Python","label":"requests","source":"tree = requests.get(\"https://app.leonidus.ai/api/v1/hierarchy/map\",\n    headers={\"Authorization\": f\"Bearer {KEY}\"}).json()"}]}},"/api/v1/hierarchy/people":{"get":{"tags":["Hierarchy"],"summary":"People view — who can see what","description":"Every workspace user with the clients they belong to, the repositories they lead and the ones they contribute to. Admin-tier users see all clients regardless of memberships. Wrapped by MCP `penta_hierarchy({ view: 'people' })`.","operationId":"getHierarchyPeople","responses":{"200":{"description":"People.","content":{"application/json":{"schema":{"type":"object","properties":{"people":{"type":"array","items":{"$ref":"#/components/schemas/HierarchyPerson"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/reports/hierarchy":{"post":{"tags":["Reports","Hierarchy"],"summary":"Generate a per-client / per-project compliance report","description":"Pass **exactly one** of `clientId` / `projectId`. Returns immediately with `{ id, status: 'queued' }`; poll `GET /api/v1/reports/hierarchy/{id}` until `completed`, then download `…/export?format=html`. Covers the scope's repositories, latest scans, open findings by severity and posture over its effective frameworks — nothing from other clients. Scoped callers get `404` for other clients. Rate limit `reports.generate` 20/min. Wrapped by MCP `penta_client_report`.","operationId":"createHierarchyReport","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"clientId":{"type":"string","format":"uuid"},"projectId":{"type":"string","format":"uuid"}},"minProperties":1,"maxProperties":1},"examples":{"client":{"summary":"Whole client","value":{"clientId":"c1a2b3c4-..."}},"project":{"summary":"One project","value":{"projectId":"p1..."}}}}}},"responses":{"200":{"description":"Queued.","content":{"application/json":{"example":{"id":"7f3e...","status":"queued"}}}},"400":{"description":"Neither or both of `clientId` / `projectId` supplied."},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"ID=$(curl -s -X POST https://app.leonidus.ai/api/v1/reports/hierarchy \\\n  -H \"Authorization: Bearer $PENTA_API_KEY\" -H \"Content-Type: application/json\" \\\n  -d '{\"clientId\":\"'$CLIENT_ID'\"}' | jq -r .id)\nuntil [ \"$(curl -s -H \"Authorization: Bearer $PENTA_API_KEY\" https://app.leonidus.ai/api/v1/reports/hierarchy/$ID | jq -r .status)\" = completed ]; do sleep 2; done\ncurl -H \"Authorization: Bearer $PENTA_API_KEY\" \"https://app.leonidus.ai/api/v1/reports/hierarchy/$ID/export?format=html\" > acme-compliance.html"},{"lang":"JavaScript","label":"fetch","source":"const { id } = await fetch(\"https://app.leonidus.ai/api/v1/reports/hierarchy\", {\n  method: \"POST\",\n  headers: { Authorization: `Bearer ${KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ clientId }),\n}).then(r => r.json());\nlet status = \"queued\";\nwhile (status !== \"completed\" && status !== \"failed\") {\n  await new Promise(r => setTimeout(r, 2000));\n  ({ status } = await fetch(`https://app.leonidus.ai/api/v1/reports/hierarchy/${id}`, { headers: { Authorization: `Bearer ${KEY}` } }).then(r => r.json()));\n}\nconst html = await fetch(`https://app.leonidus.ai/api/v1/reports/hierarchy/${id}/export?format=html`, { headers: { Authorization: `Bearer ${KEY}` } }).then(r => r.text());"},{"lang":"Python","label":"requests","source":"import time\nr = requests.post(\"https://app.leonidus.ai/api/v1/reports/hierarchy\",\n    headers={\"Authorization\": f\"Bearer {KEY}\"}, json={\"clientId\": client_id})\nrid = r.json()[\"id\"]\nwhile requests.get(f\"https://app.leonidus.ai/api/v1/reports/hierarchy/{rid}\",\n        headers={\"Authorization\": f\"Bearer {KEY}\"}).json()[\"status\"] not in (\"completed\", \"failed\"):\n    time.sleep(2)\nhtml = requests.get(f\"https://app.leonidus.ai/api/v1/reports/hierarchy/{rid}/export\",\n    params={\"format\": \"html\"}, headers={\"Authorization\": f\"Bearer {KEY}\"}).text"}]}},"/api/v1/reports/hierarchy/{id}":{"get":{"tags":["Reports","Hierarchy"],"summary":"Hierarchy report status","operationId":"getHierarchyReport","parameters":[{"$ref":"#/components/parameters/HierarchyReportId"}],"responses":{"200":{"description":"Report record. `exportUrl` is set once `status = completed`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HierarchyReport"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/reports/hierarchy/{id}/export":{"get":{"tags":["Reports","Hierarchy"],"summary":"Download the hierarchy report","description":"Self-contained HTML (inline styles; prints cleanly to PDF). Only available once `status = completed` — `409` while still generating.","operationId":"exportHierarchyReport","parameters":[{"$ref":"#/components/parameters/HierarchyReportId"},{"name":"format","in":"query","schema":{"type":"string","enum":["html"],"default":"html"}}],"responses":{"200":{"description":"Report HTML.","content":{"text/html":{}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Report not completed yet."}}}},"/api/v1/apikeys":{"get":{"tags":["API Keys"],"summary":"List workspace API keys","operationId":"listApiKeys","security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Keys (no plaintext)."}}},"post":{"tags":["API Keys"],"summary":"Mint workspace-wide key — plaintext shown ONCE","operationId":"createApiKey","security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","example":"Production CI"},"expiresInDays":{"type":"integer","minimum":1,"maximum":3650,"example":90},"scopes":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Key + plaintext.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyMint"}}}}}}},"/api/v1/repositories/{id}/apikeys":{"get":{"tags":["API Keys"],"summary":"List project-scoped keys","operationId":"listRepoApiKeys","security":[{"cookieAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Keys."}}},"post":{"tags":["API Keys"],"summary":"Mint project-scoped key","operationId":"createRepoApiKey","security":[{"cookieAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Key + plaintext.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyMint"}}}}}}},"/api/v1/reports/generate":{"post":{"tags":["Reports"],"summary":"Generate scan report","description":"Produces HTML, PDF (print-styled HTML), XLSX, or JSON. Each format includes per-finding skill chips citing CybersecuritySkills (Apache-2.0).","operationId":"generateReport","security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scanId","format"],"properties":{"scanId":{"type":"string","format":"uuid"},"format":{"type":"string","enum":["html","pdf","excel","json"]}}}}}},"responses":{"200":{"description":"Report file (Content-Type matches format)."}}}},"/api/mcp":{"get":{"tags":["MCP","Public"],"summary":"MCP discovery","description":"Returns server info + the 19 tools available. No auth required — discovery only.","operationId":"mcpDiscover","security":[],"responses":{"200":{"description":"Server descriptor."}}},"post":{"tags":["MCP"],"summary":"MCP JSON-RPC 2.0 endpoint","description":"Implements `initialize`, `tools/list`, `tools/call`, `ping`. **See dedicated [MCP docs at `/mcp-docs`](https://app.leonidus.ai/mcp-docs) for full tool reference, JSON-RPC examples, and error codes.**","operationId":"mcpInvoke","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"description":"Request id; echo on response.","oneOf":[{"type":"string"},{"type":"integer"},{"type":"null"}]},"method":{"type":"string","enum":["initialize","tools/list","tools/call","ping"]},"params":{"type":"object","additionalProperties":true}}},"examples":{"listTools":{"summary":"tools/list","value":{"jsonrpc":"2.0","id":1,"method":"tools/list"}},"callTool":{"summary":"tools/call → penta_get_skill","value":{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"penta_get_skill","arguments":{"slug":"performing-jwt-none-algorithm-attack"}}}}}}}},"responses":{"200":{"description":"JSON-RPC result."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# List tools\ncurl -X POST https://app.leonidus.ai/api/mcp \\\n  -H \"Authorization: Bearer $PENTA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'"},{"lang":"JavaScript","label":"fetch","source":"const tools = await fetch(\"https://app.leonidus.ai/api/mcp\", {\n  method: \"POST\",\n  headers: { Authorization: `Bearer ${KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ jsonrpc: \"2.0\", id: 1, method: \"tools/list\" }),\n}).then(r => r.json());"},{"lang":"Python","label":"requests","source":"import requests\nr = requests.post(\"https://app.leonidus.ai/api/mcp\",\n    headers={\"Authorization\": f\"Bearer {KEY}\"},\n    json={\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/list\"})\ntools = r.json()[\"result\"][\"tools\"]"}]}}}}