> ## Documentation Index
> Fetch the complete documentation index at: https://docs.perfai.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Embed automated API security testing directly into your own tools — trigger tests, poll for results, and gate deployments on vulnerability findings.

The Perfai API gives any partner integration or automation — CI/CD pipelines, internal tooling, or custom scripts — full programmatic control of Perfai Security: register apps, trigger security tests and vision runs, poll run status, and retrieve vulnerability findings, all over a REST API secured with JWT bearer tokens.

<CardGroup cols={2}>
  <Card title="Register app" icon="square-plus" href="/docs/api/api-reference#register-app">
    Onboard a new app programmatically.
  </Card>

  <Card title="List apps" icon="list" href="/docs/api/api-reference#list-apps">
    Resolve the app ID for your target service.
  </Card>

  <Card title="Run Vision Agent" icon="eye" href="/docs/api/api-reference#run-vision-agent">
    Re-map your app or re-validate credentials with browser automation.
  </Card>

  <Card title="Run Security Agent" icon="play" href="/docs/api/api-reference#run-security-agent">
    Schedule an asynchronous security test.
  </Card>

  <Card title="Get task status" icon="radar" href="/docs/api/api-reference#get-task-status">
    Poll for test completion with exponential back-off.
  </Card>

  <Card title="Get vulnerabilities" icon="shield-exclamation" href="/docs/api/api-reference#get-vulnerabilities">
    Retrieve findings and gate your deployment.
  </Card>
</CardGroup>

***

## Quickstart

Six calls take you from an app URL to security findings — each step feeds the next through the `$TOKEN`, `$APP_ID`, and `$TASK_ID` variables. You need a Perfai account with a **non-Viewer** role (writes are role-gated) and **available credits** (tests draw down your organization's balance). Swap `api.perfai.ai` for your own host if you run Perfai on-premise.

```bash theme={null}
# 1. Get a token — exchange your Perfai email + password
TOKEN=$(curl -s -X POST https://api.perfai.ai/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{"username":"you@example.com","password":"<your-password>"}' | jq -r .id_token)

# 2. Register your app by URL — Perfai onboards it for you (returns appId)
APP_ID=$(curl -s -X POST https://api.perfai.ai/v1/apps \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"appUrl":"https://api.example.com"}' | jq -r .appId)

# 3. Poll until registrationStatus is COMPLETED (repeat — first onboarding is app-dependent and can take a while)
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.perfai.ai/v1/apps/$APP_ID" | jq -r .registrationStatus

# 4. Start a security test (returns taskId)
TASK_ID=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  "https://api.perfai.ai/v1/apps/$APP_ID/security-agent" | jq -r .taskId)

# 5. Poll until state is COMPLETED (repeat)
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.perfai.ai/v1/tasks/$TASK_ID" | jq -r .state

# 6. Read the findings
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.perfai.ai/v1/apps/$APP_ID/vulnerabilities" | jq
```

<Note>
  Step 3 is the slow one — Perfai onboards your app (sign-up, role discovery, RBAC, mapping) before it's testable, and the default auto sign-up path is the slowest, so poll with a generous timeout rather than assuming a fixed duration. The sections below explain each piece — [base path](#base-path), [authentication](#authentication), and the [test state machine](#test-state-machine) — and the [complete integration example](#complete-integration-example) further down is a production-ready CI gate with error handling and back-off.
</Note>

***

## How it works

<Frame caption="The lifecycle of an app in the Perfai API: you call the magenta steps, poll the green ones, and Perfai handles the amber one on its own.">
  <img className="docs-frame-img" src="https://mintcdn.com/perfai/RMk7-pViyTWXap6x/docs/images/api-lifecycle.svg?fit=max&auto=format&n=RMk7-pViyTWXap6x&q=85&s=6d040a7adc545d3de51234266c1e7a29" alt="Perfai API lifecycle — authenticate, register a new app or reuse an existing one, Perfai onboards it without testing, poll until ready, trigger the test yourself, poll it, read findings, and re-map after a release." width="680" height="856" data-path="docs/images/api-lifecycle.svg" />
</Frame>

<Steps>
  <Step title="Authenticate">
    Call `POST /api/v1/auth/token` with your Perfai email and password (see [Authentication](#authentication)), then send the returned `id_token` as `Authorization: Bearer <token>` on every request.
  </Step>

  <Step title="Register or resolve your app">
    For a new app, call `POST /apps` with just an `appUrl` (everything else is optional). By default Perfai runs full autonomous onboarding — **creating test accounts for you (auto sign-up)**, discovering roles, and mapping your API surface — then drives it to a testable state. Supply `tenantOneCredentials` to use your own accounts, or `createTestAccounts: false` for an unauthenticated test. If your app has endpoints that must never be exercised — account deletion, payment capture — list them in [`skipEndpoints`](/docs/api/api-reference#excluding-endpoints-from-tests) at this point; the exclusion applies to every test that follows. Poll `GET /apps/{appId}` until `registrationStatus` is `COMPLETED`. The default auto sign-up onboarding runs an initial security test as its final step, so first findings and a report are available as soon as registration completes; supplying your own `tenantOneCredentials` or `createTestAccounts: false` skips that initial test. Either way, call `POST /apps/{appId}/security-agent` to run a test on demand once the app is registered. For an existing app, call `GET /apps` and use the `search` parameter to find its `appId`.
  </Step>

  <Step title="Trigger a test">
    Once `registrationStatus` is `COMPLETED`, call `POST /apps/{appId}/security-agent`. You receive a `taskId` and a `statusUrl` immediately — the test runs asynchronously.
  </Step>

  <Step title="Poll for completion">
    Call `GET /tasks/{taskId}` repeatedly (with exponential back-off) until `state` reaches `COMPLETED`, `FAILED`, or `CANCELLED`.
  </Step>

  <Step title="Gate on findings">
    Call `GET /apps/{appId}/vulnerabilities` and parse the results. Fail the build, or trigger any downstream workflow, if unfixed Critical or High findings exist.
  </Step>

  <Step title="Re-run vision when your app changes">
    After a release that changes your UI or API surface, call `POST /apps/{appId}/vision-agent` to re-map the app (`operation: "map"`) or re-validate stored credentials (`operation: "validate-login"`). Poll `GET /tasks/{taskId}` until a terminal state.
  </Step>
</Steps>

<Warning>
  **You can't test an app until registration is ready.** Every write call returns immediately with an ID (`201` or `202`); the real work runs in the background. After `POST /apps`, **poll `GET /apps/{appId}` until `registrationStatus` is `COMPLETED`** — how long that takes depends entirely on your app, and the default auto-signup path can run well beyond the test-polling window below, so use a generous timeout rather than assuming a fixed duration (see [Test state machine](#test-state-machine)). The default onboarding runs an initial security test itself, so findings may already be present when registration completes; to run another test call `POST /apps/{appId}/security-agent`, which only succeeds once `registrationStatus` is `COMPLETED` — triggering it earlier fails with `400 App is not registered yet`. Tests and vision runs follow the same pattern: send the request, get an ID, then poll its `statusUrl` until a terminal state.
</Warning>

***

## Base path

All endpoints share a common base URL:

```text theme={null}
https://api.perfai.ai/v1
```

The path is always `/v1`; only the host changes. On the cloud platform the host is `api.perfai.ai`; on-premise, use your own Perfai API address (`https://<your-perfai-host>/v1`). Wherever an example shows `https://<host>/…`, substitute that host.

<Note>
  **Response URLs are absolute.** Write endpoints return a `Location` response header, and the register, security-test and vision-run responses include a `statusUrl` — both are absolute URLs (for example `https://api.perfai.ai/v1/tasks/{taskId}`) that you can poll directly. On-premise, they use your own host. Add `-i` to `curl` if you want to see the response headers.
</Note>

***

## Authentication

Every endpoint requires a bearer token on the `Authorization` header:

```http theme={null}
Authorization: Bearer <token>
```

The token is a standard Perfai login token tied to your user — there is no separate API key. Get one by calling `POST https://api.perfai.ai/api/v1/auth/token` with `{"username","password"}` and using the returned `id_token` (see the [Quickstart](#quickstart)).

The token carries your organization, user identity, and role. All data is automatically scoped to your organization — cross-tenant access returns `404` (not `403`) to prevent enumeration. Tokens are short-lived; on a `401`, request a new one from the same endpoint.

<Note>
  Your token inherits your own role. **Reads are open to any role; writes (register app, trigger test, run vision) require a non-Viewer role** — see [Roles and permissions](#roles-and-permissions). For automation, an org administrator can provision a service account with a role that permits writes — for example **AppSec** or **Full-Permission-Administrator**.
</Note>

***

## Rate limits

| Endpoint                                                   | Limit                     |
| ---------------------------------------------------------- | ------------------------- |
| `POST /apps/:appId/security-agent`                         | 10 requests / 60 s per IP |
| `POST /apps` (register)                                    | 5 requests / 60 s per IP  |
| `POST /apps/:appId/vision-agent`                           | 5 requests / 60 s per IP  |
| `DELETE /apps/:appId`                                      | 5 requests / 60 s per IP  |
| `POST /vulnerabilities/:id/dismiss` \| `/fix` \| `/reopen` | 30 requests / 60 s per IP |
| All read endpoints (`GET`)                                 | No limit                  |

When the rate limit is exceeded, the server responds with `429 Too Many Requests`. Back off and retry after the value in the `Retry-After` response header.

***

## Test state machine

After you trigger a run, it moves through the following states:

```
POST /security-agent
        │
        ▼
     QUEUED ──── engine picks up ──► RUNNING
        │                               │
        │                    ┌──────────┼──────────┐
        │                    ▼          ▼           ▼
        └──── timeout ──► FAILED   COMPLETED   CANCELLED   INTERRUPTED

   RUNNING ⇄ AWAITING_INPUT   (registration only — you supply credentials)
```

| State            | Meaning                                                                                                                                                                                                                                                                                         |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `QUEUED`         | Run accepted, waiting for an engine worker                                                                                                                                                                                                                                                      |
| `RUNNING`        | Test actively in progress                                                                                                                                                                                                                                                                       |
| `AWAITING_INPUT` | Onboarding has stopped and is waiting for **you**. Perfai could not sign itself up to the app — no public sign-up page, or a CAPTCHA, SSO wall or invite-only flow — so it needs test-account credentials. Send them to `POST /v1/apps/{appId}/account-setup` and the task returns to `RUNNING` |
| `COMPLETED`      | Test finished — vulnerability results are available                                                                                                                                                                                                                                             |
| `FAILED`         | Test encountered a fatal error                                                                                                                                                                                                                                                                  |
| `CANCELLED`      | Run was cancelled before completion                                                                                                                                                                                                                                                             |
| `INTERRUPTED`    | The run was destroyed before it could finish (for example, its worker was terminated). Its findings are **unknown** — re-trigger the test. Never read this as "no vulnerabilities found"                                                                                                        |

`COMPLETED`, `FAILED`, `CANCELLED`, and `INTERRUPTED` are terminal states. Stop polling once you reach any of them.

`AWAITING_INPUT` is **not** terminal, but polling through it will not help — nothing moves until you supply the credentials. Treat it as a prompt to act, not a state to wait out.

**Vision runs** use the same state enum, minus `AWAITING_INPUT` and `INTERRUPTED`: `QUEUED → RUNNING → COMPLETED | FAILED | CANCELLED`. Only **registration** can reach `AWAITING_INPUT`, and only **security tests** can reach `INTERRUPTED`.

**Registration** has its own field, `registrationStatus`, returned by `POST /apps` and `GET /apps/{appId}`: `PENDING → IN_PROGRESS → COMPLETED | FAILED | CANCELLED`. Unlike a test, registration time is highly app-dependent (the auto-signup path is the slowest and can run well beyond the run-polling window below) — poll `GET /apps/{appId}` until it's `COMPLETED` with a generous timeout rather than assuming a fixed duration.

### Recommended polling strategy

Start at a 5 second interval and apply exponential back-off, capping at 60 seconds. Allow up to 30 minutes before timing out.

```
interval = 5s
max_wait  = 30m

while elapsed < max_wait:
    response = GET /tasks/{taskId}
    if response.state in [COMPLETED, FAILED, CANCELLED]:
        break
    sleep(interval)
    interval = min(interval * 1.5, 60s)
```

***

## Roles and permissions

Reads (list apps, get app, run status, vulnerabilities, reports) are open to every authenticated role. All writes share one allow-list. Reports are a paid feature — on the free tier the report endpoints return `402` regardless of role.

***

## Pagination

`GET` endpoints that return lists accept the following query parameters:

| Parameter  | Type    | Default | Constraints | Description                                             | Example    |
| ---------- | ------- | ------- | ----------- | ------------------------------------------------------- | ---------- |
| `page`     | integer | `1`     | ≥ 1         | 1-based page number                                     | `2`        |
| `pageSize` | integer | `20`    | 1 – 100     | Items per page                                          | `50`       |
| `search`   | string  | —       | —           | Case-insensitive partial match on app label or API name | `Payments` |

`page` and `pageSize` apply to every list endpoint. `search` applies to [List apps](/docs/api/api-reference#list-apps) and [Get vulnerabilities](/docs/api/api-reference#get-vulnerabilities) only — [Get reports](/docs/api/api-reference#get-reports) does not accept it and ignores it if sent.

All paginated responses include top-level `total`, `page`, and `pageSize` fields. Check `page * pageSize < total` to determine whether more pages exist.

***

## Error format

All error responses follow [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457):

```json theme={null}
{
  "type": "https://perfai.ai/errors/<error-slug>",
  "title": "Human-readable summary",
  "<context-field>": "<optional additional detail>"
}
```

| HTTP status | `type` slug                                         | When                                                                                                                                                                                                                 |
| ----------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`       | *(standard validation message)*                     | Missing required header or invalid request field                                                                                                                                                                     |
| `401`       | —                                                   | Missing or expired JWT                                                                                                                                                                                               |
| `402`       | —                                                   | Organization credit balance exhausted — add credits, then retry                                                                                                                                                      |
| `403`       | —                                                   | Role does not permit the operation                                                                                                                                                                                   |
| `404`       | `not-found`                                         | App or run not found, or belongs to another tenant                                                                                                                                                                   |
| `409`       | `run-already-active`                                | A security test is already in-flight for this app                                                                                                                                                                    |
| `409`       | `app-already-registered`                            | A **usable** app for this URL already exists (`COMPLETED` or still onboarding) — reuse the returned `appId`. A failed or abandoned app from this API is auto-replaced on re-register instead of returning this error |
| `409`       | `app-registered-outside-api`                        | The URL has an app that was created outside this API and is never auto-replaced — remove that app, then retry                                                                                                        |
| `409`       | `vision-run-already-active`                         | A vision run is already in-flight for this app (includes `activeVisionRunId`)                                                                                                                                        |
| `429`       | —                                                   | Rate limit exceeded                                                                                                                                                                                                  |
| `500`       | `vision-task-create-failed` / `registration-failed` | The resource was not created — retry safely                                                                                                                                                                          |

***

## Complete integration example

The following script implements the full five-step workflow — resolve app, trigger test, poll until terminal state, fetch vulnerabilities, and fail the build on Critical findings. It's written for a CI environment (GitHub Actions or any POSIX-compatible CI), but the same calls work from any scripting environment or internal tool.

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

HOST="https://api.perfai.ai"       # Perfai cloud — replace with your own host on-premise
TOKEN="$PERFAI_API_TOKEN"          # a Perfai token — see Authentication for how to get one
APP_LABEL="Payments API"

# 1. Resolve appId by label
APP_ID=$(curl -sf \
  -H "Authorization: Bearer $TOKEN" \
  "$HOST/v1/apps?search=$(python3 -c \
    'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))' "$APP_LABEL")" \
  | jq -r '.data[0].appId')

[ -z "$APP_ID" ] && { echo "App not found: $APP_LABEL"; exit 1; }
echo "App ID: $APP_ID"

# 2. Trigger test
RUN=$(curl -sf -X POST \
  -H "Authorization: Bearer $TOKEN" \
  "$HOST/v1/apps/$APP_ID/security-agent")

TASK_ID=$(echo "$RUN" | jq -r '.taskId')
echo "Security test: $TASK_ID"

# 3. Poll until terminal state
INTERVAL=10
ELAPSED=0
MAX=1800   # 30 min timeout

while true; do
  STATE=$(curl -sf \
    -H "Authorization: Bearer $TOKEN" \
    "$HOST/v1/tasks/$TASK_ID" \
    | jq -r '.state')
  echo "[$ELAPSED s] state=$STATE"

  case "$STATE" in
    COMPLETED|FAILED|CANCELLED) break ;;
  esac

  sleep "$INTERVAL"
  ELAPSED=$((ELAPSED + INTERVAL))
  INTERVAL=$(( INTERVAL < 60 ? INTERVAL + 5 : 60 ))

  if [ "$ELAPSED" -ge "$MAX" ]; then
    echo "Timed out waiting for test"; exit 1
  fi
done

[ "$STATE" != "COMPLETED" ] && { echo "Test did not complete: $STATE"; exit 1; }

# 4. Fetch vulnerabilities and fail on unfixed Critical findings
CRITICAL=$(curl -sf \
  -H "Authorization: Bearer $TOKEN" \
  "$HOST/v1/apps/$APP_ID/vulnerabilities?pageSize=100" \
  | jq '[.data[] | select(.severity == "Critical" and .isFixed == false and .isDismissed == false)] | length')

echo "Critical findings: $CRITICAL"
[ "$CRITICAL" -gt 0 ] && {
  echo "Build blocked: $CRITICAL unfixed critical vulnerability/ies"
  exit 1
}

echo "Security gate passed."
```

<Note>
  Set `PERFAI_API_TOKEN` as a secret in whichever system runs this script. Never hard-code the token in a pipeline definition or commit it to version control.
</Note>
