01Choose the right integration
Use the local CLI or stdio MCP when a person or coding agent needs direct access to a repository checkout. Use hosted MCP when an agent needs a production HTTPS endpoint with a static organization credential. Use the REST API when an application needs explicit HTTP and JSON control, including local archive uploads.
| CLI + local MCP | Repository-local developer and coding-agent workflows with device login |
| Hosted MCP | Production agents over Streamable HTTP at https://ai.cantina.xyz/mcp |
| Service API | Serverless workers, cron jobs, CI, and internal audit hubs |
| Authentication | Authorization: Bearer apex_sk_live_… or X-API-Key |
| Findings format | GitLab 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.
- Open company SettingsFrom the company dashboard, open Settings and find Service API keys.
- Generate the keySelect Generate key, give the key a descriptive name, choose the scopes it needs, and set an expiration.
- Save the one-time secretCopy 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.
CLI alternative
Managers and administrators can also generate, list, and revoke service keys from their own terminal with Apex CLI.
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" --jsonApex 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 all three scopes: scans:create, scans:read, and findings:read. List metadata or revoke a key without exposing its secret:
apex service-key list
apex service-key revoke <key-id>04Connect hosted MCP
Point any Streamable HTTP MCP client at https://ai.cantina.xyz/mcp and send the organization service key as Authorization: Bearer <TOKEN>. No local apex-mcp process, device login, browser, or TTY is required.
Keep the service key out of config files
Store the secret in APEX_SERVICE_KEYand use your MCP client's environment-variable expansion. Do not paste the raw key into committed configuration, command history, logs, or prompts.
Codex
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 apexClaude Code
Add a user- or project-scoped .mcp.json entry. Claude Code expands ${APEX_SERVICE_KEY} from the process environment when it connects.
{
"mcpServers": {
"apex": {
"type": "http",
"url": "https://ai.cantina.xyz/mcp",
"headers": {
"Authorization": "Bearer ${APEX_SERVICE_KEY}"
}
}
}
}Other clients use the same URL and Bearer header. A successful connection advertises apex-credits, apex-workspaces, apex-scan, apex-status, apex-cancel-scan, apex-findings, and apex-workspace-findings. 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.
Hosted and local MCP have different jobs
Hosted MCP operates on repositories already connected to the service key's Apex organization and scans an exact full commit SHA. Use local apex-mcpwhen a tool needs the caller's filesystem, local archive preparation, device login, provider connection, or setup workflows.
05Trigger a scan
Send a unique, stable Idempotency-Key for each intended scan. Reuse that same value when retrying the same request.
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.
curl --request POST https://ai.cantina.xyz/api/apex/v1/scans \
--header "Authorization: Bearer $APEX_SERVICE_KEY" \
--header "Idempotency-Key: 8c8d9e5d-4fa6-4b65-bd42-14ddb5874c83" \
--header "Content-Type: application/json" \
--data '{
"repoUrl": "https://github.com/acme/payments",
"commit": "0123456789abcdef0123456789abcdef01234567",
"paths": ["src", "packages/auth"],
"scanType": "standard",
"generateThreatModel": true
}'| standard (default) | Consumes the configured standard scan-credit cost (200 credits by default). |
| audit | Consumes 2,000 standard credits. |
| lite | Consumes Lite credits when the Lite-credit ledger is enabled; otherwise consumes the configured standard-credit cost. |
| local archives | Currently 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.
# 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:
{
"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"
}06Poll scan status
curl --header "Authorization: Bearer $APEX_SERVICE_KEY" \
https://ai.cantina.xyz/api/apex/v1/scans/<scan-id>- StartTreat the HTTP 202 response as accepted and retain its scanId and statusUrl.
- PollUse bounded exponential backoff while status is preparing, queued, or running.
- StopStop polling on completed, failed, or cancelled.
- CollectFetch findings after the scan reaches completed.
07Cancel a scan
curl --request POST \
--header "Authorization: Bearer $APEX_SERVICE_KEY" \
https://ai.cantina.xyz/api/apex/v1/scans/<scan-id>/cancelCancellation 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.
08Fetch findings
curl --header "Authorization: Bearer $APEX_SERVICE_KEY" \
https://ai.cantina.xyz/api/apex/v1/scans/<scan-id>/findingsThe 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.
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.
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=opento 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.
09Errors and retries
| 400 | Invalid repository URL, commit SHA, path hint, or request body |
| 401 | Missing, malformed, expired, or revoked service key |
| 402 | The selected scan credit or entitlement is exhausted |
| 403 | The key lacks the required scope |
| 404 | The scan does not exist in the key's organization |
| 409 | Idempotency conflict, repository workspace mismatch, active scan, or a scan that cannot be cancelled |
| 422 | Unsupported repository or scan-source input |
| 429 | Rate limit reached; respect Retry-After |
| 500 | Internal persistence or lookup failure; retry with backoff |
| 502 | Transient provider or Apex scan failure; retry with the identical request when appropriate |
| 503 | Workspace 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. A definitive 4xx response 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.
10Claude 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.