Skip to content

Start here

OverviewConnect your agentClient onboarding

Guides

WorkspacesScan manualFix ReviewScheduled scans

Reference

CLI guideService API

For agents

docs/agents.mdllms.txt
Apex/Docs
⌘K
Connect your agent

Start here

OverviewConnect your agentClient onboarding

Guides

WorkspacesScan manualFix ReviewScheduled scans

Reference

CLI guideService API

For agents

docs/agents.mdllms.txt
Docs/Reference/Service API

Apex service API

Use the Apex REST API from your application, CI pipeline, or scripts. Authenticate with a service key, start a scan, and import findings with cURL, JavaScript, or Python.

20 min readUpdated October 9, 2026Platform engineers
Make your first request

01Choose the right integration

The REST API lets your application start scans, track progress, and read findings over HTTPS. Follow the REST quickstart for your first request, then use the JavaScript or Python examples to collect results. For an agent with an MCP client, see hosted MCP. Use the local CLI or stdio MCP when a developer needs access to a repository checkout.

CLI + local MCPRepository-local developer and coding-agent workflows with device login
Hosted MCPProduction agents over Streamable HTTP at https://ai.cantina.xyz/mcp
Service APIServerless workers, cron jobs, CI, and internal audit hubs
AuthenticationAuthorization: Bearer apex_sk_live_… or X-API-Key
Findings formatGitLab SAST for scan exports; normalized JSON for workspace exports

The machine-readable contract is available at /api/apex/v1/openapi.json.

02Prerequisites

  • For remote scans, connect the GitHub repository to the same Apex organization that owns the service key. The first API scan can create and link its workspace automatically.
  • For remote scans, use a full 40- or 64-character Git commit SHA. Branch names and moving refs are not accepted.
  • For local or non-Git directories, package the source as tar.gz. The API creates the workspace and signed archive upload; no Git remote or website scan is required.
  • Generate or revoke keys as an Apex organization manager or administrator.
  • Store the key in a secret manager. Apex displays the raw secret only once and stores only its hash.

Version-one repository support

The first service API release accepts GitHub repositories already connected to your Apex organization. GitLab service-account access will follow once connection ownership can be made explicit.

03Generate a service key

Where Settings is available

The Settings flow is available on hosted, non-demo company tenants to active, verified company managers and Apex administrators. Active, verified personal-tenant owners use the scripted CLI instead. Demo tenants and on-prem deployments do not support hosted service API key management.

  1. 1
    Open company Settings
    From the company dashboard, open Settings and find Service API keys.
  2. 2
    Generate the key
    Select Generate key, give the key a descriptive name, choose the scopes it needs, and set an expiration.
  3. 3
    Save the one-time secret
    Copy the secret into your secret manager before closing the dialog. Apex cannot display it again.

Prefer the smallest set of scopes the integration needs. Use only scans:read and findings:read when a key polls existing scans and imports findings; add scans:create when it must start or cancel scans. Select Write findings (findings:write) to change finding status or create or link Linear or Jira tickets. Existing keys and the default three scopes do not gain it.

CLI alternative

Managers and administrators can also generate, list, and revoke service keys from their own terminal with Apex CLI.

terminal
$ apex update
$ apex login
# Replace this placeholder with a future ISO-8601 timestamp from your rotation policy:
$ FUTURE_ISO_8601_TIMESTAMP="YYYY-MM-DDTHH:MM:SSZ"
# Read-only key with an explicit expiration:
$ apex service-key create --name "Audit hub" --scope scans:read,findings:read --expires-at "$FUTURE_ISO_8601_TIMESTAMP"
# Omit --scope only when this key must also trigger scans:
$ apex service-key create --name "Audit hub" --expires-at "$FUTURE_ISO_8601_TIMESTAMP"
# Optional machine-readable output:
$ apex service-key create --name "Audit hub" --scope scans:read,findings:read --expires-at "$FUTURE_ISO_8601_TIMESTAMP" --json

Apex CLI 0.1.16 and later automatically uses your company when only one is available. If you belong to multiple companies, it asks you to choose. Use --company <id-or-handle> to skip that prompt, or when a non-interactive command cannot prompt. As in Settings, copy the secret into your secret manager when it is created. It cannot be retrieved later.

Settings defaults new keys to a 90-day expiration. The CLI leaves a key non-expiring when --expires-at is omitted, so prefer an explicit future ISO-8601 timestamp unless your credential-rotation policy intentionally requires no expiration.

When --scope is omitted, the key gets the default three scopes: scans:create, scans:read, and findings:read. List metadata or revoke a key without exposing its secret:

terminal
$ apex service-key list
$ apex service-key revoke <key-id>

04REST quickstart

The base URL is https://ai.cantina.xyz/api/apex/v1. Requests and responses use JSON. Run requests from your server, CI job, or local terminal; keep the service key out of browser JavaScript. Your key selects the organization, so you do not send a company ID.

  1. 1
    Create a service key
    Follow Generate a service key above. For the full scan workflow, select scans:create, scans:read, and findings:read.
  2. 2
    Load the credential
    Inject APEX_SERVICE_KEY using your secret manager or CI secrets. Send it in the Authorization: Bearer header on each request.
  3. 3
    Verify access with a GET
    Check credit availability below before starting a scan. This request does not start a scan or spend scan credits.
Check credits · requires scans:create
# APEX_SERVICE_KEY is already set by your secret manager.
export APEX_API_BASE="https://ai.cantina.xyz/api/apex/v1"
curl --fail-with-body --silent --show-error --max-time 30 \
  --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  "$APEX_API_BASE/credits"

A 200 response includes companyId, scansEnabled, balances.standard.remaining, balances.lite.remaining, and scanTypes. Check the chosen scan type's available, ledger, and creditsRequired. This is a snapshot; the scan-creation response is authoritative if balances change.

Only importing results?

Use a key with scans:read and findings:read. Use workspace and scan listing to discover IDs. Credits and legacy repository-only discovery require scans:create. Verify a results-only key with a scan list:

terminal
curl --fail-with-body --silent --show-error --max-time 30 \
  --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  "https://ai.cantina.xyz/api/apex/v1/scans?limit=20"

Next, start a scan and save its scanId, then collect findings in JavaScript or Python. Both examples also work with a scan already started in the web app.

05Endpoints and scopes

All paths below are relative to /api/apex/v1 and require a service key with the listed scope. Use the UUID returned by Apex for scan and finding IDs; workspaceRef also accepts a workspace prefix such as BEDR9.

REST endpoints, required scopes, and purpose
EndpointScopeUse
GET /creditsscans:createCredit balances and scan availability
GET /workspacesscans:readList workspaces, associated scans, and finding counts
GET /scansscans:readDiscover scan IDs; filter by workspace, status, or type
POST /scansscans:createStart a scan; requires Idempotency-Key
POST /apex-black/scansscans:createStart an Apex Black scan; requires Idempotency-Key and 200 Standard credits
GET /scans/{scanId}scans:readRead status and progress
POST /scans/{scanId}/cancelscans:createCancel an active scan
GET /scans/{scanId}/findingsfindings:readExport a GitLab SAST report
GET /workspaces/{workspaceRef}/findingsfindings:readExport workspace findings as normalized JSON
PATCH /findings/{findingId}/statusfindings:writeClose, reopen, or update remediation status
POST /source-uploadsscans:createCreate archive upload sessions
POST /source-uploads/{uploadId}/completescans:createVerify a source archive upload
GET /findings/{findingId}/ticketsfindings:readList linked tickets
POST /findings/{findingId}/ticketsfindings:writeLink an existing Linear or Jira ticket
POST /findings/{findingId}/issue-trackers/{provider}findings:writeSend a finding to Linear or Jira

Listing counts also requires findings:read. The legacy GET /workspaces?repository=owner/repo summary response keeps its scans:create scope; use include=scans for expanded, read-only inventory with a repository filter.

Download the public OpenAPI 3.1 specification for complete request and response schemas, or import it into Postman or an OpenAPI-compatible client generator. The specification itself does not require authentication.

06Discover workspaces and scans

Use GET /workspaces to inventory your organization and GET /scans to find existing scan IDs. Both require scans:read. Add findings:read to receive finding counts; without it, count fields are null. These requests do not start scans or consume scan credits.

Workspaces with scans and counts
curl --fail-with-body --silent --show-error --max-time 30 \
  --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  "https://ai.cantina.xyz/api/apex/v1/workspaces?limit=20&scanLimit=20"

Each workspace includes its workspaceId, prefix, name, repositories, timestamps, scanCount, findingsCount, a scans array, and scansNextCursor. Empty workspaces and workspaces created from local source archives are included. A repository filter selects matching workspaces; their inventory still includes all linked repositories and scans.

List scan IDs directly

Scan inventory · requires jq to print only IDs
curl --fail-with-body --silent --show-error --max-time 30 \
  --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  "https://ai.cantina.xyz/api/apex/v1/scans?limit=20" \
  | jq -r '.scans[].scanId'

Remove the jq step to keep the full JSON response, including nextCursor. Scan summaries include their workspace ID, display name, sequence number, stored scan type, status, timestamps, archive state, finding count, and last findings sync time. PR and fix-review scans are included; unfinished setup rows have isSetupDraft: true. A preparing API request may not have a stored scan row yet, so retain the scanId returned by POST /scans as well.

Filter and retrieve every page

Completed scans in a workspace
export APEX_WORKSPACE_ID="<workspace-uuid>"
curl --fail-with-body --silent --show-error --max-time 30 --get \
  --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  --data-urlencode "workspaceId=$APEX_WORKSPACE_ID" \
  --data-urlencode "status=completed" \
  --data-urlencode "limit=20" \
  "https://ai.cantina.xyz/api/apex/v1/scans"

Both lists default to 20 records and accept limit=1 through 50. Results are ordered by UUID, not creation time. Request the same endpoint and filters with cursor=nextCursor until nextCursor is null. Each request reads the current state; pagination is not a frozen snapshot of a changing inventory.

Workspace scans have independent pagination. If a workspace has a non-null scansNextCursor, continue at GET /scans?workspaceId=…&cursor=… using that cursor and the same includeArchived setting. The workspace's totals include scans beyond this nested page. scanLimit controls the nested page size, also from 1 to 50.

Optional filters
# Add these query parameters to the corresponding endpoint:
/scans?workspaceId=<workspace-uuid>&status=completed&scanType=standard
/scans?includeArchived=true
/workspaces?includeArchived=true
/workspaces?repository=owner%2Frepo&include=scans

status accepts queued, running, completed, failed, or cancelled. The scanType filter matches the stored type, such as standard, ultra (Audit), lite, pr, or fix_review. Archived records are excluded by default; includeArchived=true includes them. On workspace inventory, that setting applies to both workspaces and nested scans.

What the counts mean

findingsCount is the last persisted scan total, which can include draft and associated findings. It can lag a running scan; use findingsSyncedAt when available to inspect freshness. Workspace finding totals sum matching scan counts, so a finding associated with multiple scans can count more than once. They are not unique workspace-finding counts or severity/validation-filtered totals. A count is null when unavailable or outside the key's scopes. A zero before the first findings sync is also unknown; 0 means a synchronized zero. Imported nonzero totals can be available without a sync timestamp.

Existing repository discovery stays compatible

A request containing only repository retains the existing unpaginated workspace summaries and requires scans:create. Add include=scans or a pagination option to receive the expanded inventory using scans:read.

07Trigger a scan

Send a unique, stable Idempotency-Key for each intended scan. Persist it with the request body in your job state and reuse both when retrying. Replace the example repository and full commit SHA below with your connected repository and the revision to scan. Starting a scan consumes the selected scan credits.

Threat model first, scan second

By default, the API accepts the request with HTTP 202, generates a fresh unattended threat model in the background, and starts the scan automatically when generation completes. The initial status is preparing; poll the returned statusUrl through preparation and scanning. Scan credits are not reserved until threat-model generation succeeds. When you provide workspaceId, useExistingThreatModel defaults to true. Apex validates that the completed model exists before reserving credits. Requests without workspaceId still default to fresh generation for first-scan workspace creation. Set generateThreatModel to false only when you intentionally want to start without generation or validated reuse.

Apex reuses the active workspace already linked to the repository. If none exists, the same request creates a workspace named after the canonical GitHub repository and links it before starting the scan—no website scan is required. If more than one workspace matches, include its workspaceId in the body. The conflict response includes each matching workspace ID, prefix, and name so the caller can present a choice instead of failing with an undiscoverable ID.

Explicit workspaces stay explicit

When workspaceId is present, Apex will not rewrite that workspace to point at another repository. A mismatch returns a conflict so integrations cannot silently change an existing workspace.

Reuse an existing threat model

For a subsequent scan, provide the existing workspace's UUID;useExistingThreatModel is true by default. Set generateThreatModel to true when you want a fresh threat model instead; an explicit fresh generation request overrides the implicit reuse default. Apex fails closed with THREAT_MODEL_NOT_AVAILABLE if the workspace has no completed model; after choosing a different workspace or switching back to generation, submit the corrected request with a new Idempotency-Key.

POST /api/apex/v1/scans
# Set once for this intended scan; save it before making the request.
export APEX_IDEMPOTENCY_KEY="<unique-job-id>"
curl --fail-with-body --silent --show-error --max-time 60 \
  --request POST https://ai.cantina.xyz/api/apex/v1/scans \
  --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  --header "Idempotency-Key: $APEX_IDEMPOTENCY_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "repoUrl": "https://github.com/acme/payments",
    "commit": "0123456789abcdef0123456789abcdef01234567",
    "paths": ["src", "packages/auth"],
    "scanType": "standard",
    "generateThreatModel": true
  }'

The key accepts 1–255 letters, numbers, periods, underscores, colons, or hyphens. Generate a UUID or use your durable job ID. After an HTTP 202, save scanId and poll instead of submitting another POST. The returned statusUrl and findingsUrl are relative to https://ai.cantina.xyz.

standard (default)Consumes the configured standard scan-credit cost (200 credits by default).
auditConsumes 1,000 standard credits.
liteConsumes Lite credits when the Lite-credit ledger is enabled; otherwise consumes the configured standard-credit cost.
local archivesCurrently support standard scans and use the same standard credit reservation path.

Paths are focus hints

The optional paths array prioritizes those files in Apex prompts, but it is not a hard repository boundary. The scan may inspect or report code outside those paths. Omit it if you do not need focus hints; version one does not yet provide strict path isolation.

Scan a local or non-Git directory

Raw filesystem paths cannot cross an HTTP boundary. Create a tar.gz archive, calculate its SHA-256 plus compressed and uncompressed byte counts, then use the upload → complete → scan flow below. A directory does not need to be a Git repository; omit the optional git object for non-Git source.

upload → complete → scan
# 1. Package the source without embedding the output archive:
tar --exclude="./apex-source.tar.gz" -czf apex-source.tar.gz .
# Calculate SHA256, COMPRESSED_BYTES, and UNCOMPRESSED_BYTES in your uploader.

# 2. Create the workspace and signed upload session:
curl --request POST https://ai.cantina.xyz/api/apex/v1/source-uploads \
  --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "workspaceName": "Payments local",
    "uploads": [{
      "displayName": "payments",
      "relativePath": ".",
      "archiveFormat": "tar.gz",
      "sha256": "<sha256>",
      "compressedBytes": <compressed-bytes>,
      "uncompressedBytes": <uncompressed-bytes>
    }]
  }'

# 3. PUT the archive to uploads[0].upload.putUrl using its returned headers:
curl --request PUT --header "Content-Type: application/gzip" \
  --upload-file apex-source.tar.gz "<put-url>"

# 4. Verify the upload; this returns archiveId and sha256:
curl --request POST \
  --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  --header "Content-Type: application/json" \
  --data '{"workspaceId":"<workspace-id>"}' \
  https://ai.cantina.xyz/api/apex/v1/source-uploads/<upload-id>/complete

# 5. Start the first scan with a fresh threat model:
curl --request POST https://ai.cantina.xyz/api/apex/v1/scans \
  --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  --header "Idempotency-Key: local-payments-2026-07-22" \
  --header "Content-Type: application/json" \
  --data '{
    "workspaceId": "<workspace-id>",
    "scanType": "standard",
    "generateThreatModel": true,
    "scanSources": [{
      "sourceKind": "local_archive",
      "displayName": "payments",
      "relativePath": ".",
      "archiveId": "<archive-id>",
      "sha256": "<sha256>"
    }]
  }'

Generate a model for a new local workspace

A newly created local workspace has no completed threat model to reuse. Keep generateThreatModel: true on its first scan. Omitting it makes a request with workspaceIddefault to existing-model reuse and can return THREAT_MODEL_NOT_AVAILABLE.

Archive limits

Each archive must be at most 500 MiB compressed and 2 GiB uncompressed, and one scan may include at most 20 archives. Signed upload URLs are credentials: do not log or persist them beyond the upload.

A successful request returns HTTP 202:

202 Accepted
{
  "scanId": "3e559b86-f524-4b27-a175-fb4944c53f1b",
  "workspaceId": "2abf03ae-768b-4717-9d14-43e1a8d8d9a1",
  "status": "preparing",
  "statusUrl": "/api/apex/v1/scans/3e559b86-f524-4b27-a175-fb4944c53f1b",
  "findingsUrl": "/api/apex/v1/scans/3e559b86-f524-4b27-a175-fb4944c53f1b/findings"
}

08Poll scan status

An HTTP 200 means the status lookup succeeded; check the JSON status to determine whether the scan succeeded. Status responses include scanId, status, progress (0–100), createdAt, and updatedAt. Preparation failures also include error and code.

GET /api/apex/v1/scans/{scanId}
$ curl --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  https://ai.cantina.xyz/api/apex/v1/scans/<scan-id>
  1. 1
    Start
    Treat the HTTP 202 response as accepted and retain its scanId and statusUrl.
  2. 2
    Poll
    Use bounded exponential backoff while status is preparing, queued, or running.
  3. 3
    Stop
    Stop polling on completed, failed, or cancelled.
  4. 4
    Collect
    Fetch findings after the scan reaches completed.

09JavaScript and Python examples

Save either program below, set APEX_SCAN_ID to the UUID from the creation response or an existing scan, and inject APEX_SERVICE_KEY from your secret manager. The key needs scans:read and findings:read.

Each program waits for completion and writes the GitLab SAST JSON report to standard output. It stops on failed or cancelled scans and other non-retryable errors, retries GETs on transport failures, 429, and 5xx, and honors Retry-After while findings finish synchronizing. The budget is 120 requests with a 30-second request timeout and backoff capped at 30 seconds unless the server asks for a longer wait.

Node.js 22 or later

No packages to install. Save as apex-findings.mjs.

apex-findings.mjs
const { APEX_SERVICE_KEY, APEX_SCAN_ID } = process.env;
if (!APEX_SERVICE_KEY || !APEX_SCAN_ID) {
  throw new Error("Set APEX_SERVICE_KEY and APEX_SCAN_ID first.");
}

const url = "https://ai.cantina.xyz/api/apex/v1/scans/" +
  encodeURIComponent(APEX_SCAN_ID);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let readingFindings = false;

async function collectFindings() {
  for (let attempt = 0; attempt < 120; attempt++) {
    let delay = Math.min(2000 * 2 ** Math.min(attempt, 4), 30000);
    let response;
    try {
      response = await fetch(url + (readingFindings ? "/findings" : ""), {
        headers: { Authorization: "Bearer " + APEX_SERVICE_KEY },
        signal: AbortSignal.timeout(30000),
        redirect: "error",
      });
    } catch {
      // Transport errors are safe to retry: every request here is a GET.
      await sleep(delay);
      continue;
    }

    if (response.ok) {
      let data;
      try {
        data = await response.json();
      } catch {
        // Retry an incomplete or malformed GET response within the same budget.
        await sleep(delay);
        continue;
      }
      if (readingFindings) return data;
      if (data.status === "completed") {
        readingFindings = true;
        continue;
      }
      if (!["preparing", "queued", "running"].includes(data.status)) {
        throw new Error("Scan stopped: " + data.status +
          (data.code ? " (" + data.code + ")" : ""));
      }
    } else {
      if (response.status !== 429 && response.status < 500) {
        const error = await response.json().catch(() => ({}));
        throw new Error("HTTP " + response.status + ": " +
          (error.code ?? "request_failed"));
      }
      // Apex returns Retry-After in seconds. Never retry sooner.
      const retryAfter = Number(response.headers.get("Retry-After"));
      if (Number.isFinite(retryAfter) && retryAfter > 0) {
        delay = Math.max(delay, retryAfter * 1000);
      }
      await response.body?.cancel();
    }
    await sleep(delay);
  }
  throw new Error("Polling budget exhausted. Resume with the same APEX_SCAN_ID.");
}

console.log(JSON.stringify(await collectFindings(), null, 2));
terminal
$ node apex-findings.mjs > findings.json

Python 3.10 or later

Uses only the standard library. Save as apex_findings.py.

apex_findings.py
import json
import os
import time
from urllib.error import HTTPError, URLError
from urllib.parse import quote
from urllib.request import HTTPRedirectHandler, Request, build_opener

key = os.environ["APEX_SERVICE_KEY"]
scan_id = os.environ["APEX_SCAN_ID"]
url = "https://ai.cantina.xyz/api/apex/v1/scans/" + quote(scan_id, safe="")

# Keep the bearer credential on the configured API origin.
class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

opener = build_opener(NoRedirect)

def collect_findings():
    reading_findings = False
    for attempt in range(120):
        delay = min(2 * 2 ** min(attempt, 4), 30)
        request = Request(
            url + ("/findings" if reading_findings else ""),
            headers={"Authorization": "Bearer " + key},
        )
        try:
            with opener.open(request, timeout=30) as response:
                data = json.load(response)
        except HTTPError as error:
            with error:
                if error.code != 429 and error.code < 500:
                    raise RuntimeError("HTTP " + str(error.code) +
                                       ": check the API error and scopes") from None
                try:
                    retry_after = int(error.headers.get("Retry-After", "0"))
                except ValueError:
                    retry_after = 0
                delay = max(delay, retry_after)
        except (URLError, TimeoutError, json.JSONDecodeError, UnicodeDecodeError):
            pass  # These GET requests are safe to retry.
        else:
            if reading_findings:
                return data
            if data["status"] == "completed":
                reading_findings = True
                continue
            if data["status"] not in ("preparing", "queued", "running"):
                raise RuntimeError("Scan stopped: " + data["status"] +
                                   " (" + data.get("code", "terminal") + ")")
        time.sleep(delay)
    raise TimeoutError("Polling budget exhausted. Resume with the same APEX_SCAN_ID.")

print(json.dumps(collect_findings(), indent=2))
terminal
$ python3 apex_findings.py > findings.json

Resume the same scan

A local timeout does not cancel the scan. Save its ID in your job state and rerun with the same APEX_SCAN_ID; these programs never create a scan. Run long polling in a background worker or scheduled job when it exceeds your hosting platform's request duration. Treat the exported report as confidential; findings can include unpublished drafts.

10Cancel a scan

POST /api/apex/v1/scans/{scanId}/cancel
$ curl --request POST \
  --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  https://ai.cantina.xyz/api/apex/v1/scans/<scan-id>/cancel

Cancellation requires scans:create and stops the remaining Apex work for a queued or running scan. A successful response returns HTTP 200 with status: cancelled. Repeating the request after cancellation is safe and returns the same terminal status.

Terminal scans cannot be cancelled

Apex returns 409 when a scan is already completed or failed, or when it has not started in Apex yet. Poll the status URL to confirm the current terminal state.

11Fetch findings

GET /api/apex/v1/scans/{scanId}/findings
$ curl --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  https://ai.cantina.xyz/api/apex/v1/scans/<scan-id>/findings

The response is a GitLab SAST report. Each vulnerability includes the persisted Apex finding ID, title, description, severity, and available file and line location. IDs remain stable when you fetch the same scan again, so downstream systems can deduplicate safely.

Findings retrieval also works for scans started in the Apex web app, so an integration can begin with this endpoint before automating scan creation.

Discover workspace IDs

A key with scans:create can list active workspaces before creating a scan. Provide a repository URL or GitHub owner/name to find every linked workspace without exposing unrelated workspace metadata. If more than one is returned, let the user choose one and send its workspaceId in a new scan request with a new Idempotency-Key.

GET /api/apex/v1/workspaces
curl --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  'https://ai.cantina.xyz/api/apex/v1/workspaces?repository=Uniswap%2Fliquidity-launcher'

Fetch a whole workspace by prefix

To fetch the same finding set as the workspace findings browser, use its human-readable prefix such as BEDR9. The export uses non-archived scans and excludes PR scans, fix-review scans, and unfinished advanced-scan setup drafts. The service key still limits the lookup to its own organization, so a prefix from a different organization returns 404. Use the workspace UUID instead if a company has an ambiguous prefix.

GET /api/apex/v1/workspaces/{workspaceRef}/findings
curl --header "Authorization: Bearer $APEX_SERVICE_KEY" \
  'https://ai.cantina.xyz/api/apex/v1/workspaces/BEDR9/findings?state=open'

The response includes the resolved workspace, its active scans, and full finding records rather than only finding IDs. Set state=open to exclude invalid findings, duplicates, and findings marked fixed, false positive, won't fix, or skipped. The response includes each finding's reviewState, isDuplicate, and duplicateOfId. Repeat filter to combine facets. Supported forms include severity:high, status:valid, review:accepted, duplicate:unique, repo:example-app, source:lib/auth.ts, impact:high, likelihood:medium, visibility:draft, scan:<scan-id>, and !validation:invalid. Finding statuses are proposed, partially_valid, valid, invalid, or duplicate; open is a finding state, not a status value. Review state and validity include the current shared client feedback used by the workspace UI. Validation filters use the same client/viewer feedback precedence as the workspace UI, and each finding exposes the resulting validity as valid, invalid, or unknown. Invalid or unknown filter strings are ignored; the response's filters array shows the filters that were applied.

Findings access includes drafts

A key with findings:read can retrieve unpublished draft findings for scans in its organization so an external hub can perform triage. Treat the service key as a privileged credential and do not expose its findings payload to users who cannot review drafts in Apex.

The first request after a scan completes durably synchronizes the terminal finding set. If that sync is unavailable or has more pages, Apex returns 503 with Retry-After. Retry the same GET; after readiness is persisted, later requests serve the cached report without depending on a live scan process.

12Errors and retries

API errors have a human-readable error and a stable code; branch on the code instead of matching the message. Some errors include additional fields, such as matching workspaces. For example, a missing scope returns HTTP 403:

403 Forbidden
{
  "error": "Service API key is missing the scans:read scope",
  "code": "insufficient_scope"
}

Rate limits apply per service key. On 429, wait at least the number of seconds in Retry-After before trying again. Share a polling budget across workers using the same key, and add jitter to backoff in concurrent integrations.

400Invalid repository URL, commit SHA, path hint, or request body
401Missing, malformed, expired, or revoked service key
402The selected scan credit or entitlement is exhausted
403The key lacks the required scope
404The scan does not exist in the key's organization
409Idempotency conflict, repository workspace mismatch, active scan, or a scan that cannot be cancelled
422Unsupported repository or scan-source input
429Rate limit reached; respect Retry-After
500Internal persistence or lookup failure; retry with backoff
502Transient provider or Apex scan failure; retry with the identical request when appropriate
503Workspace provisioning is in progress, an Apex operation timed out, scan finalization is incomplete, or the first terminal findings sync is incomplete; respect Retry-After

Retry safely

Threat-model generation and scan startup continue server-side after the creation request returns 202. Preparation failures appear as status: failed with an error code at the returned status URL; callers do not need to replay the creation POST merely because generation takes longer than one HTTP request.

GET requests and scan cancellation requests are safe to retry. After a transport error, timeout, or 5xx, retry a scan creation POST only with the original body and the same Idempotency-Key. Idempotency is organization-scoped, so replay remains safe if the service key is rotated between attempts. For 429 or a 409with IDEMPOTENCY_REQUEST_IN_PROGRESS, wait for Retry-After and retry the same body and key. A stored, definitive 4xx failure is retained: after correcting its precondition, submit it as a new intended scan with a new Idempotency-Key. Never log the Authorization or X-API-Key header.

13Connect hosted MCP

Point a Streamable HTTP MCP client at https://ai.cantina.xyz/mcp. ChatGPT and supported Codex loopback clients link an Apex organization through OAuth. Service-key clients send Authorization: Bearer <TOKEN> using their organization’s key. No local apex-mcp process is required.

Connect once and continue your request

The Apex Black plugin uses browser sign-in in ChatGPT and Codex. Use its Connect or Authenticate action, sign in, and select your organization. Your agent retains the requested targets and continues preparation after connecting. Connecting is free and does not start a scan.

ChatGPT

Connect https://ai.cantina.xyz/mcp using OAuth in your host’s developer connection flow, or import the Apex Black plugin. Leave client credentials blank; Apex supports dynamic public-client registration and S256 PKCE. Sign in with your existing Apex account, select an organization you manage, and review the scan and finding permissions before connecting. This connection does not start a scan.

Connections expire after 30 days. Revoke the MCP OAuth: ChatGPT entry in organization Settings → Service API keys to disconnect. Access tokens expire after one hour and refresh tokens rotate automatically. The OAuth server migration must be deployed before account linking works.

Codex

The Apex Black plugin uses OAuth by default and needs no service key or environment-variable setup. For a standalone connection, add the hosted server without a bearer-token setting, then log in. Complete the browser consent flow with an organization you manage; Codex receives the callback through its local loopback listener and manages the tokens. Connecting does not start a scan.

terminal
$ codex mcp add apex --url https://ai.cantina.xyz/mcp
$ codex mcp login apex
$ codex mcp get apex

Alternatively, configure an existing organization service key using an environment variable. Use only one of these configurations for the apex server entry.

Keep service keys out of config files

Use an environment variable or a secret manager for unattended integrations. Do not paste the raw key into committed configuration, command history, logs, or prompts.

terminal
$ export APEX_SERVICE_KEY='<service-key>'
$ codex mcp add apex --url https://ai.cantina.xyz/mcp --bearer-token-env-var APEX_SERVICE_KEY
$ codex mcp get apex

Claude Code

Add a user- or project-scoped .mcp.json entry. Claude Code expands ${APEX_SERVICE_KEY} from the process environment when it connects.

.mcp.json
{
  "mcpServers": {
    "apex": {
      "type": "http",
      "url": "https://ai.cantina.xyz/mcp",
      "headers": {
        "Authorization": "Bearer ${APEX_SERVICE_KEY}"
      }
    }
  }
}

Service-key clients use the same URL and Bearer header. A successful connection advertises apex-credits, apex-workspaces, apex-scan, apex-black-prepare, apex-black-start, apex-status, apex-cancel-scan, apex-findings, apex-workspace-findings, apex-finding-tickets, apex-finding-link-ticket, apex-finding-send, and apex-finding-status. Tool calls enforce the same scopes, organization isolation, rate limits, usage attribution, and idempotency rules as the REST API. When a REST operation supplies a Retry-After header, the MCP tool error preserves it as retryAfter and a normalized retryAfterSeconds value in structured content.

Run an Apex Black scan

Existing Apex customers with Apex Black beta enabled can run active web security scans through apex-black-start. The connection needs scans:create; include scans:read and findings:read to follow the scan and read its results. Each new scan costs 200 Standard credits. Use apex-black-prepare with the exact targets to check access and credits before starting. It returns readiness, the cost, and support or billing links for missing prerequisites, without creating a workspace or reserving credits. Older deployments can use apex-credits to check the balance; scan creation checks the current entitlement and balance again. Retain the returned organization ID as expectedCompanyId for starts and replays. When the discovered input schema advertises this field, OAuth starts require it and a changed connection is rejected before launch. Older servers omit the field; keep the original ID locally and verify the same organization after reconnecting. The connected deployment must advertise apex-black-start; installing the plugin alone does not enable the tool or the beta.

Supply targetUrls with 1–16 exact HTTPS origins, such as https://app.example.com. Credentials, paths, query strings, and fragments are rejected. The listed origins define the target scope. Add testing instructions and exclusions in optional scopeText (up to 100,000 characters), application context in threatModelContext (up to 4,000 characters), and a workspaceName (up to 242 characters, leaving room for the Apex Black prefix) if desired. Set authorizationAccepted: true when authority to test every target and acceptance of the credit cost are present. Use authorization already given in the session without asking again.

apex-black-start arguments
{
  "idempotencyKey": "black-customer-portal-2026-09-30",
  "expectedCompanyId": "11111111-1111-4111-8111-111111111111",
  "targetUrls": ["https://app.example.com", "https://api.example.com"],
  "scopeText": "Exclude billing and destructive actions.",
  "workspaceName": "Customer portal",
  "authorizationAccepted": true
}

The result includes a durable scanId, workspaceId, status, statusUrl, findingsUrl, creditCost: 200, scanType: "standard", and operationType: "apex_black". The scan type matches the shared inventory; the operation type identifies the web scan. Keep the IDs and use apex-status to check progress, then apex-findings after completion. Reuse the same idempotencyKey and identical input after transport or retryable errors, respecting retryAfterSeconds. An identical retry returns the same scan without another charge; changed input with the same key returns 409. A stored remote-start failure, including APEX_BLACK_START_FAILED, is replayed without launching again. Keep the returned request and workspace IDs and contact support before attempting another request.

In the example, replace expectedCompanyId with the organization ID returned by preparation or credits. REST clients send the same body without the MCP-only expectedCompanyId and idempotencyKeyto POST /api/apex/v1/apex-black/scans, passing the key in the Idempotency-Key header. The endpoint returns 202 and the same scan result. The existing scan status and findings endpoints read Apex Black results. See the OpenAPI specification for the request, response, and error schemas.

Close and reopen findings

Use apex-finding-status to close a resolved issue with status: "fixed" or reopen it with status: "open". This records your remediation decision and an audit entry while preserving the scanner verdict, severity, and validity feedback.

Hosted MCP takes findingId (the finding UUID), status, and an optional note. Your connection must explicitly grant findings:write. Read-only keys cannot change statuses.

apex-finding-status arguments
{
  "findingId": "<finding-uuid>",
  "status": "fixed",
  "note": "Resolved in PR #123"
}
REST · requires findings:write
curl -X PATCH "https://ai.cantina.xyz/api/apex/v1/findings/$FINDING_ID/status" \
  -H "Authorization: Bearer $APEX_SERVICE_KEY" -H "Content-Type: application/json" \
  -d '{"status":"fixed","note":"Resolved in PR #123"}'

Other statuses are acknowledged, in_review, pending_merge, false_positive, wont_fix, and skipped. Reopening retains the reviewed state as accepted. Invalid or duplicate findings require a validity or duplicate-classification review before they can enter the open queue.

Include the finding's updatedAt as expectedUpdatedAt(CLI: --expected-updated-at) to prevent overwriting a newer edit. A conflict returns FINDING_STATUS_CONFLICT; reload and review the latest state before retrying. The response includes the saved reviewState, updatedAt, and changed. Workspace finding lists include remediationStatus so open and acknowledged remain distinct on subsequent reads.

Linear sync currently runs from Linear to Apex: started tickets become acknowledged and completed tickets become fixed. Changing a finding here does not update its Linear or Jira ticket. Use the direct status operation when managing remediation from an MCP client or API integration.

Send findings to Linear or Jira

Create a ticket from a finding with the same title, severity, details, and Apex link as the frontend. Connect the integration and configure its destination in Apex first. Sending uses saved Linear workspace project and labels (or the company default), or the workspace Jira site, project, and issue type.

Call apex-finding-send with findingId (UUID) and provider (linear or jira). The service key must explicitly include findings:write.

REST clients call POST /api/apex/v1/findings/{findingId}/issue-trackers/{provider}with no request body. The response contains created and export, including the ticket's issueKey and issueUrl.

Already sent or linked findings return their existing ticket with created: false; an active export returns a conflict. After an ambiguous timeout or provider failure, inspect the linked tickets and the provider before retrying. Sending is never automatically retried.

Link existing tickets

After connecting Linear or Jira in company settings, call apex-finding-link-ticket with findingId (the finding UUID), provider (linear or jira), and issueRef (ticket key or HTTPS issue URL). The key needs explicit findings:write scope. Call apex-finding-ticketswith findingId to list links using findings:read.

terminal
$ apex service-key create --name "Ticket linking" --scope findings:read,findings:write

REST clients use GET /api/apex/v1/findings/{findingId}/ticketsand POST /api/apex/v1/findings/{findingId}/tickets. POST accepts {"provider":"linear","issueRef":"SEC-123"}and returns linked plus a ticket containing provider, issueId, issueKey, and issueUrl. A repeated link returns linked: false. Apex verifies access through the company integration without creating or modifying a provider ticket. A different existing link or an in-progress export returns 409. Use a full Jira issue URL when the integration can access several sites and no destination is configured.

Hosted and local MCP have different jobs

Hosted MCP operates on repositories already connected to the connection's Apex organization and scans an exact full commit SHA, or runs Apex Black against authorized HTTPS origins. Use local apex-mcp when a tool needs the caller's filesystem, local archive preparation, device login, provider connection, or setup workflows.

14Claude and Codex prompts

These prompts work in Claude Code or Codex. The first two keep key creation in a user-controlled Settings or terminal flow; the others use either the hosted MCP endpoint or the REST API for a headless runtime instead of launching a local stdio process there.

›Walk me through generating an Apex service key from my company Settings page. Help me choose least-privilege scopes, but do not ask me to paste the secret into this chat.
›Give me the exact Apex CLI command I should run in my own terminal to create a service key named Audit hub. Do not run it for me or ask me to paste the secret into this chat.
›Connect this MCP client to https://ai.cantina.xyz/mcp through browser sign-in. Preserve my requested scan through account linking, verify tools/list, and check readiness without starting a scan or asking me to create a service key.
›Using APEX_SERVICE_KEY from the environment, write a serverless TypeScript adapter that starts an Apex scan for this repository and commit. Send a stable Idempotency-Key, poll until terminal, and return the GitLab SAST findings.
›Using APEX_SERVICE_KEY from the environment, package this local directory as tar.gz, create and complete an Apex source upload, then start a standard scan from its local_archive scanSources entry with generateThreatModel: true. A new workspace has no model to reuse. Keep the key and signed upload URL secret.
›Add Apex to our audit hub. Call POST /api/apex/v1/scans, poll GET /api/apex/v1/scans/{scanId} with bounded backoff, then fetch /findings. Never log the bearer token and retry only safe requests.
›Using APEX_SERVICE_KEY from the environment, cancel this active Apex scan with POST /api/apex/v1/scans/{scanId}/cancel. Confirm the returned status is cancelled and never log the credential.
›Given an Apex scanId for a scan started in the web app, fetch its GitLab SAST findings with APEX_SERVICE_KEY, preserve the raw response, and import each vulnerability once by stable ID. Do not trigger another scan or log the credential.
›Using APEX_SERVICE_KEY from the environment, fetch every finding shown in the workspace findings browser for BEDR9 and exclude findings whose client/viewer feedback validity is invalid. Keep the response as JSON and never print the credential.
›Diagnose this Apex API response without printing secrets. Explain whether it is a transport/5xx retry that must preserve the original Idempotency-Key, or a definitive 4xx that must be corrected and submitted as a new intended scan with a new key.
Was this useful?
Last updated October 9, 2026 · v2026.10
← PreviousApex CLI guide

On this page

  1. Choose the right integration
  2. Prerequisites
  3. Generate a service key
  4. REST quickstart
  5. Endpoints and scopes
  6. Discover workspaces and scans
  7. Trigger a scan
  8. Poll scan status
  9. JavaScript and Python examples
  10. Cancel a scan
  11. Fetch findings
  12. Errors and retries
  13. Connect hosted MCP
  14. Claude and Codex prompts

Related

OpenAPI documentApex CLI guideClient onboarding