Skip to navigation

REST API

Overview

Pensar provides a REST API for programmatic access to your workspace data. It covers the same pentest, issue, and fix capabilities as the MCP Server — plus attack-surface and webhook-endpoint routes that MCP does not expose — and is designed for automation, scripting, and building custom integrations.

If you want findings pushed to you rather than polled for, register a webhook endpoint instead of looping over GET /issues.

The REST API uses API key authentication. For AI-assistant integrations, consider using the MCP Server which supports OAuth-based authentication and works natively with tools like Claude, Cursor, and Windsurf.

Base URL

https://api.pensar.dev

Authentication

All requests must include a valid Pensar API key. You can provide it in one of two ways:

MethodHeaderExample
x-api-key headerx-api-key: <api_key>x-api-key: sk-id__abc123...
Bearer tokenAuthorization: Bearer <api_key>Authorization: Bearer sk-id__abc123...

The workspace is resolved automatically from the API key, so you do not need to send a workspace identifier. (The X-Workspace-Id header is only used by browser/WorkOS-JWT callers, not by API-key callers.)

Creating an API Key

  1. Navigate to Settings > Integrations > API Keys in the Pensar Console
  2. Click Create API Key
  3. Give it a descriptive name and copy the key value
  4. Store it securely — the key is only shown once

API keys are scoped to a workspace. All API requests authenticate against the workspace associated with the key.

Endpoints

Pentests

List Pentests

GET /pentests

Returns all pentests (scans) in the workspace.

Response:

[
{
"id": "51111111-...",
"label": "Full Scan",
"status": "completed",
"scanType": "whitebox-pentest",
"branch": "main",
"startedAt": "2026-03-17T12:00:00Z",
"completedAt": "2026-03-17T12:45:00Z"
}
]

Get Pentest

GET /pentests/:pentestId

Returns detailed information about a specific pentest.

ParameterInTypeRequiredDescription
pentestIdpathstringYesPentest (scan) UUID

Response:

{
"id": "51111111-...",
"label": "Full Scan",
"status": "completed",
"workspaceName": "My Workspace",
"scanType": "whitebox-pentest",
"branch": "main",
"startedAt": "2026-03-17T12:00:00Z",
"completedAt": "2026-03-17T12:45:00Z",
"errorMessage": null,
"issuesCount": 4,
"reportReady": true
}

Dispatch Pentest

POST /pentests

Queue a hosted pentest in the authenticated workspace. It runs asynchronously; use the returned scanId with GET /pentests/:pentestId to check its status. An identical active launch may be reused, in which case the response identifies that existing scan.

ParameterInTypeRequiredDescription
endpointIdsbodystring[]NoExact list of 1–1000 discovered endpoint UUIDs in this workspace. Requires full depth; cannot be combined with branch.
scanLevelbodystringNo"priority" or "full". Defaults to "full" when endpointIds is supplied, otherwise "priority". Only "full" is accepted with endpointIds.
branchbodystringNoMust match the connected repository’s configured target branch. Omitting it uses that configured branch; this does not select a different branch for one run. Cannot be combined with endpointIds.
knowledgeEntryIdsbodystring[]NoUp to 20 active knowledge base entry UUIDs to give the run as context, the same as @-mentioning entries at launch in the Console. Works with or without endpointIds; an archived, stale, or unknown entry returns 400 before a scan is created.

Select exact endpoints:

First list applications with GET /apps, then retrieve their discovered endpoints with GET /apps/:appId/endpoints (paginate with limit and offset). Use the endpoint id values, not URLs, application IDs, or the target IDs returned by a previous pentest. Endpoints from multiple applications may be selected when they belong to the same workspace.

curl --fail-with-body https://api.pensar.dev/pentests \
-H "Authorization: Bearer $PENSAR_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"endpointIds": [
"7e4fe67a-61e5-4aa5-b93a-6871b470aaef",
"59e9b6d5-d13b-4912-9e46-a1e801c1d053"
],
"scanLevel": "full"
}'

Replace the example UUIDs with IDs from your workspace. scanLevel may be omitted for this request because exact endpoint launches default to full depth.

How scope is selected:

  • With endpointIds, only those discovered endpoints become scan targets. The server derives their applications, removes duplicate IDs, and does not narrow the list using a repository commit or priority depth. Even a list containing every current endpoint remains an explicit list.
  • Without endpointIds, the server resolves eligible workspace applications. These launches, including Apex pensar pentests dispatch, are not attributed to a repository and do not narrow targets against a previous pentest, even if a commit SHA is recorded. scanLevel defaults to "priority"; depth is separate from diff selection. For repository-baseline comparisons, use the CI dispatch integration. To test the changes between two specific commits, send baseSha and commitSha to POST /ci/dispatch (compare two explicit commits); this endpoint does not accept them.
  • Selected applications must be ready to test: they need a linked domain with pentesting enabled, domain verification when required, and discovered endpoints. A rejected selected application does not silently reduce the requested scope.

Launch without an explicit endpoint list:

{
"scanLevel": "full"
}

To include branch, supply the configured target branch, for example {"branch":"main","scanLevel":"full"} when that repository is configured to analyze main.

Response (201):

{
"scanId": "51111111-1111-4111-8111-111111111111",
"label": "PEN-000123",
"status": "queued",
"message": "Pentest queued for My Workspace"
}

Validation: Malformed JSON, unknown fields, an empty or oversized endpoint list, invalid UUIDs, missing or deleted endpoints, endpoints outside the workspace, and incompatible branch or scanLevel values return 400 before a scan is created. The endpoint list is never replaced with workspace-wide scope after validation fails. For example:

{
"error": "One or more endpoint IDs were not found in this workspace"
}

Send only the documented launch fields. Application IDs, custom objectives, model overrides, and credential selections are not accepted by this endpoint.

For command-line examples, including reading endpoint IDs from a file or stdin, see Apex CLI: launch selected endpoints. AI assistants can use MCP dispatch_pentest.

List Pentest Targets

GET /pentests/:pentestId/targets

Returns the targets tested during a pentest. A target is a single endpoint of the attack surface that the pentest exercised. Targets are the entry point for querying the pentest’s execution logs, which are persisted per target rather than per issue (see Target Logs).

ParameterInTypeRequiredDescription
pentestIdpathstringYesPentest (scan) UUID

Response:

[
{
"id": "a1b2c3d4-...",
"url": "/api/login",
"applicationId": "55555555-...",
"applicationName": "web-frontend",
"status": "completed",
"attempt": 1,
"startedAt": "2026-03-17T12:01:00Z",
"completedAt": "2026-03-17T12:09:00Z",
"error": null
}
]

Issues

List Issues

GET /issues

Returns security issues in the workspace with optional filtering.

ParameterInTypeRequiredDescription
scanIdquerystringNoFilter by pentest ID
statusquerystringNoFilter by status: open, closed, false-positive, in-review
severityquerystringNoFilter by severity: critical, high, medium, low
branchquerystringNoFilter by git branch

Response:

[
{
"id": "11111111-...",
"issueLabel": "VULN-000123",
"title": "SQL Injection in login handler",
"severity": "critical",
"status": "open",
"location": "src/auth/login.ts",
"url": "https://console.pensar.dev/acme/VULN-000123",
"closedAt": null,
"closedDisposition": null,
"duplicateOf": null
}
]

issueLabel is the human-facing reference shown in the console and url links straight to the issue. Every :issueId path below accepts either form.

closedAt, closedDisposition and duplicateOf are null on an open finding. The list carries the disposition because without it every closed finding reads as resolved; the free-text close fields are on Get Issue only, since this list is unpaginated.

Get Issue

GET /issues/:issueId

Returns full details of a specific security issue, including description, CWE classification, proof-of-concept, and affected code location.

ParameterInTypeRequiredDescription
issueIdpathstringYesIssue UUID or label (e.g. VULN-000123)

title is the fix pull request’s title when one exists; name is always the finding’s own name. startLineNumber and endLineNumber are the affected lines in location, and are null when the finding has no line range.

A closed issue also carries the full close record: closedAt, closedDisposition, duplicateOf, closedMethod (how it was closed — manual, retest, mcp, api), closedReason, and closedComments. All six are null while the issue is open, and duplicateOf is set only on a duplicate close.

Update Issue

PATCH /issues/:issueId

Update the status of a security issue — close it, mark it as a false positive, or reopen it.

ParameterInTypeRequiredDescription
issueIdpathstringYesIssue UUID or label (e.g. VULN-000123)
statusbodystringNoNew status: open, closed, false-positive, in-review
userFlaggedFalsePositivebodybooleanNoFlag the issue as a false positive
userFlaggedFalsePositiveReasonbodystringNoReason for the false positive flag
closedDispositionbodystringNoStructured close verdict: resolved, duplicate, wont-fix, out-of-scope, risk-accepted
duplicateOfbodystringWith duplicateThe issue this one duplicates, as a UUID or label (e.g. VULN-000123)
closedReasonbodystringNoFree-text reason for closing
closedCommentsbodystringNoAdditional comments on closure
endpointIdbodystring or nullNoEndpoint UUID in this workspace. Omit to leave unchanged; null clears both endpoint and application.

Request Body:

{
"status": "closed",
"closedDisposition": "risk-accepted",
"closedReason": "Compensating control in place",
"closedComments": "Signed off by security"
}

Response (200): the updated issue, including closedAt, closedReason, closedDisposition, duplicateOf, endpointId, and applicationId.

Setting endpointId also updates applicationId to the endpoint’s application. A malformed UUID returns 400; an endpoint outside this workspace or missing returns 404. Another finding with the same name and endpoint in the same pentest returns 409.

A status change, or a change to duplicateOf, is also applied to the finding’s linked GitHub, Linear and Notion tickets, the same as a change made in the console.

closedDisposition is the structured verdict; closedReason is the free text beside it. A finding closed without a disposition reads as Resolved everywhere, so send one whenever the close is not a plain fix. Reopening clears the disposition along with the rest of the close record, and false-positive is its own status rather than a disposition, so it never carries one.

To close a finding as a duplicate, send closedDisposition: "duplicate" with duplicateOf naming the original, which must be another issue in the same workspace:

{
"status": "closed",
"closedDisposition": "duplicate",
"duplicateOf": "VULN-000045",
"closedReason": "Same missing guard on the invoice controller"
}

duplicateOf is required with duplicate and refused with any other disposition, and the issue must end up closed. The original cannot itself be closed as a duplicate, so point at the original it names instead. An original outside the workspace is a 404; the other cases are a 400. Reopening the issue, or changing its disposition, clears the link.

Closing an issue that has duplicates of its own as a duplicate moves those duplicates onto the new original, so every link points at an issue that is not itself a duplicate. Deleting an original reopens its duplicates, since nothing would be left tracking the vulnerability.

other is not accepted — it is being retired from the vocabulary. Any value outside the five is a 400, including null, so omit the field rather than sending one back from a read:

{
"error": "closedDisposition must be one of: resolved, duplicate, wont-fix, out-of-scope, risk-accepted"
}

Retest Issue

POST /issues/:issueId/retest

Queues an asynchronous retest of a security issue against its original target.

ParameterInTypeRequiredDescription
issueIdpathstringYesIssue UUID or label (e.g. VULN-000123)

Response (201):

{
"issueId": "11111111-...",
"sessionId": "22222222-...",
"status": "queued",
"message": "Issue retest queued"
}

The retest runs asynchronously. Rather than polling for the verdict, subscribe to the retest.completed webhook — it carries the verdict and the full issue.

List Issue Retests

GET /issues/:issueId/retests

Returns the issue’s retest history, most recent first. An issue that has never been retested returns an empty array, not a 404.

ParameterInTypeRequiredDescription
issueIdpathstringYesIssue UUID

Response:

[
{
"id": "a8e2c451-...",
"status": "fixed",
"stillExists": false,
"confidence": "high",
"evidence": "The union-select payload now returns 400 and the query is parameterized at src/db/query.ts:142.",
"recommendation": "Close the finding.",
"error": null,
"queuedAt": "2026-08-27T08:58:02.000Z",
"startedAt": "2026-08-27T08:58:41.000Z",
"completedAt": "2026-08-27T09:06:18.000Z"
}
]

status is derived from the retest’s timestamps, error, and result rather than stored, so it cannot drift out of sync with them: queued, in-progress, fixed, still-vulnerable, or error. confidence is high, medium, or low, and is the field to build an escalation rule on.


Fixes

List Fixes

GET /issues/:issueId/fixes

Returns all available auto-remediation fixes for an issue.

ParameterInTypeRequiredDescription
issueIdpathstringYesIssue UUID or label (e.g. VULN-000123)

Get Fix

GET /fixes/:fixId

Returns the full fix details including the code diff, explanation, and affected file path.

ParameterInTypeRequiredDescription
fixIdpathstringYesFix UUID

Agent Logs

List Agent Logs

GET /issues/:issueId/logs

Returns agent execution logs for a specific issue.

ParameterInTypeRequiredDescription
issueIdpathstringYesIssue UUID or label (e.g. VULN-000123)
levelquerystringNoFilter by log level: debug, info, warn, error
rolequerystringNoFilter by role: assistant, user, system, tool-call, tool-result
limitqueryintegerNoMaximum number of logs to return

Search Agent Logs

POST /issues/:issueId/logs/search

Search agent logs by text pattern with configurable context.

ParameterInTypeRequiredDescription
issueIdpathstringYesIssue UUID or label (e.g. VULN-000123)
querybodystringYesSearch text pattern
levelbodystringNoFilter by log level
rolebodystringNoFilter by role
contextLinesbodyintegerNoNumber of surrounding context lines to include

Request Body:

{
"query": "SQL injection",
"level": "info",
"contextLines": 3
}

Target Logs

A pentest’s execution logs are persisted against the individual targets it tested (see List Pentest Targets), not against the issues it discovered. These endpoints query those logs directly by target id, so you can inspect a target’s full pentest activity even when it produced no issue. The level, role, and contextLines filters behave identically to the issue Agent Logs endpoints.

List Target Logs

GET /targets/:targetId/logs

Returns agent execution logs for a specific pentest target.

ParameterInTypeRequiredDescription
targetIdpathstringYesTarget UUID (from List Pentest Targets)
levelquerystringNoFilter by log level: debug, info, warn, error
rolequerystringNoFilter by role: assistant, user, system, tool-call, tool-result
limitqueryintegerNoMaximum number of logs to return

Search Target Logs

POST /targets/:targetId/logs/search

Search a target’s agent logs by text pattern with configurable context.

ParameterInTypeRequiredDescription
targetIdpathstringYesTarget UUID
querybodystringYesSearch text pattern
levelbodystringNoFilter by log level
rolebodystringNoFilter by role
contextLinesbodyintegerNoNumber of surrounding context lines to include

Attack Surface

These endpoints expose the discovered attack surface of the workspace — the applications (apps) Pensar tracks and the individual endpoints within them. They have no MCP equivalent.

List Apps

GET /apps

Returns the applications in the workspace.

ParameterInTypeRequiredDescription
limitqueryintegerNoMaximum number of apps to return
offsetqueryintegerNoNumber of apps to skip (pagination)

Create App

POST /apps

Creates a new application in the workspace.

ParameterInTypeRequiredDescription
namebodystringYesApplication name
descriptionbodystringYesDescription of the application
typebodystringNoApplication type
frameworkbodystringNoFramework the app is built with
domainIdbodystringNoAssociated domain ID
disallowedActionsbodystringNoActions the testing agent must not perform

Get App

GET /apps/:appId

Returns full details of a specific application.

ParameterInTypeRequiredDescription
appIdpathstringYesApplication UUID

Update App

PATCH /apps/:appId

Updates an application. All body fields are optional.

ParameterInTypeRequiredDescription
appIdpathstringYesApplication UUID
namebodystringNoApplication name
descriptionbodystringNoDescription of the application
typebodystringNoApplication type
frameworkbodystringNoFramework the app is built with
domainIdbodystringNoAssociated domain ID
disallowedActionsbodystringNoActions the testing agent must not perform

Delete App

DELETE /apps/:appId

Deletes an application.

ParameterInTypeRequiredDescription
appIdpathstringYesApplication UUID

List Endpoints

GET /apps/:appId/endpoints

Returns the endpoints discovered for a specific application.

ParameterInTypeRequiredDescription
appIdpathstringYesApplication UUID
typequerystringNoFilter by endpoint type
limitqueryintegerNoMaximum number of endpoints to return
offsetqueryintegerNoNumber of endpoints to skip (pagination)

Create Endpoint

POST /apps/:appId/endpoints

Adds an endpoint to an application.

ParameterInTypeRequiredDescription
appIdpathstringYesApplication UUID
endpointbodystringYesThe endpoint path or identifier
descriptionbodystringYesDescription of the endpoint
typebodystringNoEndpoint type

Get Endpoint

GET /endpoints/:endpointId

Returns full details of a specific endpoint.

ParameterInTypeRequiredDescription
endpointIdpathstringYesEndpoint UUID

Update Endpoint

PATCH /endpoints/:endpointId

Updates an endpoint. All body fields are optional.

ParameterInTypeRequiredDescription
endpointIdpathstringYesEndpoint UUID

Delete Endpoint

DELETE /endpoints/:endpointId

Deletes an endpoint.

ParameterInTypeRequiredDescription
endpointIdpathstringYesEndpoint UUID

Search Apps

GET /search/apps

Searches applications in the workspace.

ParameterInTypeRequiredDescription
qquerystringYesSearch query
typequerystringNoFilter by application type
limitqueryintegerNoMaximum number of results to return
offsetqueryintegerNoNumber of results to skip (pagination)

Search Endpoints

GET /search/endpoints

Searches endpoints across all applications in the workspace.

ParameterInTypeRequiredDescription
qquerystringYesSearch query
applicationIdquerystringNoRestrict the search to a single application
typequerystringNoFilter by endpoint type
authRequiredquerybooleanNoFilter by whether authentication is required
limitqueryintegerNoMaximum number of results to return
offsetqueryintegerNoNumber of results to skip (pagination)

Knowledge Base

The workspace knowledge base is the context Pensar agents use during pentests: technology notes, testing history, and false positive patterns. A false_positive_pattern entry suppresses matching findings.

Any API key with knowledge:read can list and read entries. Creating, updating, and archiving require a named user (a signed-in user credential): a workspace API key has no author to attribute the change to and receives 403. Every entry records the user who created it in createdBy.

An entry has this shape:

{
"id": "33333333-3333-4333-8333-333333333333",
"title": "Auth stack",
"content": "Sessions are issued by the /auth service as signed cookies.",
"category": "technology_context",
"status": "active",
"source": "user",
"tags": ["auth"],
"confidence": null,
"createdBy": "44444444-4444-4444-8444-444444444444",
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:00:00.000Z"
}

category is one of false_positive_pattern, technology_context, testing_history, user_note, or agent_learning. status is active, stale, or archived; only active entries are given to agents.

List Knowledge Entries

GET /knowledge

Returns a JSON array of entries, newest first.

ParameterInTypeRequiredDescription
categoryquerystringNoFilter by category
statusquerystringNoFilter by status
tagsquerystringNoComma-separated tags; matches entries carrying every listed tag

Create Knowledge Entry

POST /knowledge

Requires a named user. Returns the created entry with 201; source is always "user".

ParameterInTypeRequiredDescription
titlebodystringYesEntry title
contentbodystringYesEntry body
categorybodystringYesEntry category
tagsbodystring[]NoTags for filtering

Get Knowledge Entry

GET /knowledge/:id
ParameterInTypeRequiredDescription
idpathstringYesEntry UUID

Update Knowledge Entry

PATCH /knowledge/:id

Requires a named user. Send at least one body field; omitted fields are unchanged and tags replaces the existing list. Returns the updated entry.

ParameterInTypeRequiredDescription
idpathstringYesEntry UUID
titlebodystringNoEntry title
contentbodystringNoEntry body
categorybodystringNoEntry category
statusbodystringNoEntry status
tagsbodystring[]NoTags for filtering

Archive Knowledge Entry

DELETE /knowledge/:id

Requires a named user. Archives the entry (sets status to "archived") rather than deleting it, and returns the archived entry.

ParameterInTypeRequiredDescription
idpathstringYesEntry UUID

An unknown category or status returns 400; an entry outside the workspace returns 404.


Webhooks

Register HTTPS endpoints that Pensar pushes signed findings to as they are created, source-mapped, retested, and closed. The payload contract, signature-verification snippets, retry semantics, and endpoint requirements are documented on the Webhooks page; this section is the route reference.

The path is /webhook-endpoints, not /webhooks. /webhooks/* is reserved for inbound provider receivers and returns 404.

Subscribable event names are issue.created, issue.updated, issue.status_changed, retest.completed, and pentest.completed.

List Webhook Endpoints

GET /webhook-endpoints

Returns the workspace’s webhook endpoints, newest first. The signing secret is never included.

Response:

{
"endpoints": [
{
"id": "f2a71c98-...",
"url": "https://hooks.acme.dev/pensar",
"events": ["issue.created", "issue.updated", "retest.completed"],
"enabled": true,
"description": "Vulnerability harness",
"health": {
"lastAttemptAt": "2026-08-26T20:00:01.468Z",
"lastStatus": "success",
"lastResponseCode": 200,
"consecutiveFailures": 0
},
"createdAt": "2026-08-26T19:14:02.117Z",
"updatedAt": "2026-08-26T19:14:02.117Z"
}
]
}

The health block is updated on every delivery attempt, so this single call tells you whether a receiver is healthy. At 20 consecutive failures the endpoint is disabled automatically.

Create Webhook Endpoint

POST /webhook-endpoints

Registers an endpoint and mints its signing secret. The URL must be HTTPS, must use a hostname rather than an IP literal, and must resolve exclusively to publicly routable addresses — see Endpoint requirements. Duplicate entries in events are collapsed.

ParameterInTypeRequiredDescription
urlbodystringYesHTTPS URL to deliver to. Unique within the workspace.
eventsbodystring[]YesEvents to subscribe to. Must be non-empty.
descriptionbodystringNoFree-text label shown in the console
enabledbodybooleanNoDefaults to true

Response (201):

{
"endpoint": {
"id": "f2a71c98-...",
"url": "https://hooks.acme.dev/pensar",
"events": ["issue.created", "issue.updated", "retest.completed"],
"enabled": true,
"description": "Vulnerability harness",
"health": {
"lastAttemptAt": null,
"lastStatus": null,
"lastResponseCode": null,
"consecutiveFailures": 0
},
"createdAt": "2026-08-26T19:14:02.117Z",
"updatedAt": "2026-08-26T19:14:02.117Z"
},
"secret": "whsec_2f1c9a7e..."
}

secret is returned by this route and by no other. Store it before you close the response — it cannot be read back, and rotating means deleting and re-registering the endpoint.

A URL Pensar would refuse to fetch is a 400 naming the reason; a URL already registered in the workspace is a 409.

Get Webhook Endpoint

GET /webhook-endpoints/:endpointId

Returns the endpoint plus its 20 most recent delivery attempts.

ParameterInTypeRequiredDescription
endpointIdpathstringYesWebhook endpoint UUID

Response:

{
"endpoint": { "id": "f2a71c98-...", "url": "https://hooks.acme.dev/pensar", "...": "..." },
"recentDeliveries": [
{
"id": "d3f1aa08-...",
"event": "issue.created",
"status": "success",
"attempt": 1,
"responseCode": 200,
"responseBody": "ok",
"error": null,
"durationMs": 184,
"createdAt": "2026-08-26T20:00:01.284Z",
"deliveredAt": "2026-08-26T20:00:01.468Z"
}
]
}

Update Webhook Endpoint

PATCH /webhook-endpoints/:endpointId

Updates an endpoint. All body fields are optional, but at least one must be supplied — an empty body is a 400. A changed url is re-validated. Setting enabled to true resets consecutiveFailures to zero, so a re-enabled endpoint does not trip the auto-disable threshold on its next failure.

ParameterInTypeRequiredDescription
endpointIdpathstringYesWebhook endpoint UUID
urlbodystringNoNew HTTPS delivery URL
eventsbodystring[]NoReplacement subscription list. Must be non-empty.
enabledbodybooleanNoEnable or disable delivery
descriptionbodystring | nullNoFree-text label. null clears it.

Response:

{
"endpoint": { "id": "f2a71c98-...", "enabled": true, "...": "..." }
}

Delete Webhook Endpoint

DELETE /webhook-endpoints/:endpointId

Deletes the endpoint and, by cascade, its delivery history.

ParameterInTypeRequiredDescription
endpointIdpathstringYesWebhook endpoint UUID

Response:

{
"deleted": true,
"id": "f2a71c98-..."
}

Test Webhook Endpoint

POST /webhook-endpoints/:endpointId/test

Queues a real delivery so you can verify the wiring — URL reachability, signature verification, and your 2xx — without waiting for a real finding. It renders the workspace’s most recent finding as an issue.created event; if the workspace has no findings, it sends a clearly-marked synthetic one instead (label VULN-SAMPLE, nil UUID for every id). The event is always issue.created, whatever the endpoint subscribes to.

ParameterInTypeRequiredDescription
endpointIdpathstringYesWebhook endpoint UUID

Response (202):

{
"deliveryId": "7c4e1b09-...",
"endpointId": "f2a71c98-...",
"event": "issue.created",
"sample": false,
"issueId": "1b7e9d40-..."
}

sample is true when the synthetic event was sent. The delivery is queued, not yet sent — read the delivery log for the outcome. Testing a disabled endpoint is a 409.

List Webhook Deliveries

GET /webhook-endpoints/:endpointId/deliveries

Returns delivery attempts for an endpoint, newest first. One record per attempt, including retries.

ParameterInTypeRequiredDescription
endpointIdpathstringYesWebhook endpoint UUID
limitqueryintegerNoMaximum attempts to return. Defaults to 50, capped at 200.
includePayloadquerybooleanNoInclude the rendered body sent on each attempt. Defaults to false.

Response:

{
"deliveries": [
{
"id": "d3f1aa08-...",
"event": "issue.created",
"status": "success",
"attempt": 1,
"responseCode": 200,
"responseBody": "ok",
"error": null,
"durationMs": 184,
"createdAt": "2026-08-26T20:00:01.284Z",
"deliveredAt": "2026-08-26T20:00:01.468Z"
}
],
"limit": 50
}

status is pending, success, or failed. id is the value that travelled as the X-Pensar-Delivery header, and responseBody is truncated to 2 KB.


Auth

Validate API Key

GET /auth/validate

Validates the API key and returns the associated workspace. Useful for CLI tools to resolve workspace context from a stored API key.

Response:

{
"workspace": {
"id": "workspace-uuid",
"name": "My Workspace",
"slug": "my-workspace"
}
}

Error Handling

The API returns standard HTTP status codes. Error responses include a JSON body with an error field:

{
"error": "Scan not found or not in this workspace"
}

Other resource-specific variants include "Issue not found or not in this workspace" and "Fix not found or not in this workspace".

Endpoints taking an :issueId distinguish a malformed reference from an unknown one. A value that is neither a UUID nor a VULN-… label is rejected before the lookup runs:

{
"error": "Issue reference must be a UUID or a VULN-000000 label (issueId=not-an-issue)"
}

A well-formed reference that matches no issue in the workspace returns 404.

Status CodeMeaning
200Success
201Created (e.g., pentest dispatched)
400Bad request (missing required fields, or a malformed issue reference)
401Unauthorized (invalid or missing API key)
403Forbidden (workspace access denied)
404Resource not found
500Internal server error

Next Steps