Barrion Docs
Public API

Pentests

Start an AI pentest, follow it, read its findings and see what it cost.

An API pentest is the same run you would start from the dashboard: same levels, same prices, same report.

Before the first run:

  1. Verify the target in the dashboard, the same way as for any pentest.
  2. Turn on API runs for it under your API keys in Settings. See Targets for API pentests.
  3. Copy the target's verification id from the same targets table, in the Verification id column. You pass it as verificationId when you start a run.

Types

Every pentest endpoint answers with these shapes.

type PentestStatus =
  | "QUEUED"
  | "VERIFYING" | "RECON" | "ENUMERATION" | "WEB_TEST" | "API_TEST" | "ANALYSIS" | "REPORTING"
  | "COMPLETED" | "FAILED" | "CANCELLED"  // only these three mean the run is over
  // Only on runs started from the dashboard; the API never creates them:
  | "AWAITING_PAYMENT"  // waiting for a checkout to be paid
  | "PAUSED";           // ran out of credits partway; can be resumed from the dashboard

type Pentest = {
  id: string;
  targetUrl: string;
  status: PentestStatus;
  depth: "LEAD" | "STANDARD" | "DEEP" | "EXTENDED" | "MAXIMUM"; // a run started from the API is one of the first three
  apiKeyId: string | null;      // the key that started it, null for a dashboard run
  createdAt: string;            // ISO 8601
  startedAt: string | null;
  completedAt: string | null;
  reportReleased: boolean;      // whether you may read its findings yet, see "Read the findings"
  credits: PentestCredits;
};

type PentestCredits = {
  state: "RESERVED" | "CHARGED" | "RETURNED" | "NOT_CREDIT_FUNDED" | "AWAITING_PAYMENT";
  reserved: number | null;      // credits held at start
  charged: number | null;       // null until the run is settled
  returned: number | null;      // null until the run is settled
};

type PentestFinding = {
  id: string;
  title: string;
  description: string | null;
  severity: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO";
  confidence: "CONFIRMED" | "FIRM" | "TENTATIVE";
  affectedUrl: string | null;
  remediation: string | null;
  cvssScore: string | null;
  cweId: string | null;
};

Start a pentest

POST /v1/pentests · permission pentests:write · requires an Idempotency-Key header

Starts a run and holds its level's credits. Answers 201 with the run's id, which is how you follow it.

Request body

Prop

Type

Any other field is refused rather than ignored, so a typo fails loudly. The run's budget, duration and model come from the level and cannot be set.

Example

curl -X POST https://api.barrion.io/v1/pentests \
  -H "Authorization: Bearer $BARRION_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: release-2026-09-28" \
  -d '{
    "targetUrl": "https://staging.example.com",
    "verificationId": "6aca3f16-78aa-4e04-a77a-425a8b3d67ad",
    "depth": "LEAD"
  }'
const res = await fetch("https://api.barrion.io/v1/pentests", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BARRION_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "release-2026-09-28",
  },
  body: JSON.stringify({
    targetUrl: "https://staging.example.com",
    verificationId: "6aca3f16-78aa-4e04-a77a-425a8b3d67ad",
    depth: "LEAD",
  }),
});
if (!res.ok) throw new Error(`${res.status} ${(await res.json()).code}`);
const { id } = (await res.json()) as { id: string; status: string };
import os, requests

res = requests.post(
    "https://api.barrion.io/v1/pentests",
    headers={
        "Authorization": f"Bearer {os.environ['BARRION_API_KEY']}",
        "Idempotency-Key": "release-2026-09-28",
    },
    json={
        "targetUrl": "https://staging.example.com",
        "verificationId": "6aca3f16-78aa-4e04-a77a-425a8b3d67ad",
        "depth": "LEAD",
    },
)
res.raise_for_status()
pentest_id = res.json()["id"]

Response 201

type StartPentestResponse = { id: string; status: PentestStatus };
{ "id": "3f9e2c7a-1b4d-4e8f-9a2c-5d6e7f8a9b0c", "status": "QUEUED" }

A retry with the same Idempotency-Key and body answers the same run, with the header Idempotent-Replayed: true, and starts nothing. See Idempotency.

Errors

StatusCodeWhyWhat to do
400VALIDATION_ERRORA field is missing or invalid; field names itFix the request
402INSUFFICIENT_CREDITSYour balance does not cover the whole levelTop up, or pick a lighter level
403TARGET_API_RUNS_DISABLEDAPI runs are off for this targetTurn them on in Settings
403API_KEY_CAP_EXCEEDEDThe run would take this key past its budgetRaise the key's budget
403ORG_CREDIT_CAP_EXCEEDEDThe run would take you past your allowance in the organizationAsk an organization admin
403FORBIDDENThe target's verification is not complete, has expired, or does not cover targetUrlRe-verify the target in the dashboard
403FORBIDDENIn an organization, targetUrl is not one of the pentest targets assigned to youAsk an organization admin for access
404NOT_FOUNDNo target verification with this verificationId in your accountCopy the id from the targets table under API keys
409IDEMPOTENCY_IN_PROGRESSThe first request with this key is still runningRetry the same request shortly
422IDEMPOTENCY_KEY_REUSEDThis key was already used with a different bodyUse a new key for a new run
429PENTEST_CONCURRENCY_EXCEEDEDYou already have as many pentests running as your account allowsWait for one to finish
429RATE_LIMITEDToo many starts in a minuteWait for Retry-After
500INTERNAL_ERRORThe run could not be dispatchedRetry with the same Idempotency-Key

A pentest is only started when your balance covers its whole level. The dashboard can start a run on part of a level and let you top up to continue; the API never does, because a pipeline cannot top up halfway through.

Authenticated runs

To test behind a login, pass test accounts in credentials, taken from your CI secret store on every run.

type Credential = {
  name: string;                 // unique within the run; letters, digits, _ and -
  kind?: "FORM_LOGIN" | "BEARER_TOKEN" | "BASIC_AUTH" | "COOKIE" | "API_KEY"; // default FORM_LOGIN
  user: string;                 // username, or the header or parameter name for API_KEY
  pass: string;                 // password, token or key value
  totpSecret?: string;          // authenticator-app secret, if the login needs one
  instructions?: string;        // non-secret guidance, e.g. "after login, open /profile"
  mode?: "auto" | "json" | "form" | "browser"; // FORM_LOGIN: how the login is submitted; default auto
  location?: "header" | "query";               // API_KEY: where the key is sent; default header
  tokenUrl?: string;            // BEARER_TOKEN: OAuth2 token URL for the client-credentials flow
  scope?: string;               // BEARER_TOKEN with tokenUrl: scope to request
  audience?: string;            // BEARER_TOKEN with tokenUrl: audience to request
};

For BEARER_TOKEN without tokenUrl, pass is the token itself. With tokenUrl, user is the client id and pass the client secret, and Barrion requests the token and refreshes it during the run.

Credentials are encrypted as they arrive and are never shown back to you or written to logs. They stay stored, encrypted, so Barrion's reviewers can verify findings behind the login and a retest of the same run can reuse them. A daily cleanup deletes them once they are 30 days old. Use dedicated test accounts, not real users' logins.

Get a pentest

GET /v1/pentests/{id} · permission pentests:read

The run's status and what it has cost so far. This is what a pipeline polls.

curl https://api.barrion.io/v1/pentests/3f9e2c7a-1b4d-4e8f-9a2c-5d6e7f8a9b0c \
  -H "Authorization: Bearer $BARRION_API_KEY"
const res = await fetch(`https://api.barrion.io/v1/pentests/${id}`, {
  headers: { Authorization: `Bearer ${process.env.BARRION_API_KEY}` },
});
const pentest = (await res.json()) as Pentest;
res = requests.get(
    f"https://api.barrion.io/v1/pentests/{pentest_id}",
    headers={"Authorization": f"Bearer {os.environ['BARRION_API_KEY']}"},
)
pentest = res.json()

Response 200: Pentest

{
  "id": "3f9e2c7a-1b4d-4e8f-9a2c-5d6e7f8a9b0c",
  "targetUrl": "https://staging.example.com",
  "status": "WEB_TEST",
  "depth": "LEAD",
  "apiKeyId": "b81d4f0e-2c3a-4b5d-8e9f-0a1b2c3d4e5f",
  "createdAt": "2026-09-28T10:02:11.000Z",
  "startedAt": "2026-09-28T10:04:40.000Z",
  "completedAt": null,
  "reportReleased": true,
  "credits": { "state": "RESERVED", "reserved": 400, "charged": null, "returned": null }
}

A run can wait in QUEUED for a while before it starts: it is waiting for a free slot. FAILED runs are not charged, and CANCELLED ones only for the work they got through.

credits.state:

StateMeaning
RESERVEDSome of the run's credits are still held. charged and returned are null until all of them are settled
CHARGEDSettled. charged credits were spent and returned went back to your balance
RETURNEDFailed, or cancelled before it started. Everything reserved went back
NOT_CREDIT_FUNDEDPaid for another way, for example included in your plan
AWAITING_PAYMENTA dashboard run waiting for its checkout. Nothing is reserved yet

A run can be COMPLETED and still read RESERVED, for two reasons. Settling a finished run takes up to a minute or so. And on Standard and Deep, the part of the credits that pays for the expert review stays held until a reviewer signs the report off. See Credits.

Errors

404 NOT_FOUND when there is no such run in this account. Retests are not in the API, so a retest's id is also a 404.

List pentests

GET /v1/pentests · permission pentests:read

Your runs, newest first.

Prop

Type

curl "https://api.barrion.io/v1/pentests?limit=20" \
  -H "Authorization: Bearer $BARRION_API_KEY"

Response 200

type ListPentestsResponse = {
  pentests: Pentest[];
  nextOffset: number | null;    // null on the last page
};

Read the findings

GET /v1/pentests/{id}/findings · permission pentests:read

Prop

Type

curl "https://api.barrion.io/v1/pentests/3f9e2c7a-1b4d-4e8f-9a2c-5d6e7f8a9b0c/findings?severity=critical,high" \
  -H "Authorization: Bearer $BARRION_API_KEY"
const res = await fetch(
  `https://api.barrion.io/v1/pentests/${id}/findings?severity=critical,high`,
  { headers: { Authorization: `Bearer ${process.env.BARRION_API_KEY}` } }
);
const { reportReleased, total, findings } = await res.json();
res = requests.get(
    f"https://api.barrion.io/v1/pentests/{pentest_id}/findings",
    params={"severity": "critical,high"},
    headers={"Authorization": f"Bearer {os.environ['BARRION_API_KEY']}"},
)
data = res.json()

Response 200

type PentestFindingsResponse = {
  id: string;
  reportReleased: boolean;
  total: number | null;         // null while the report is not released
  nextOffset: number | null;
  findings: PentestFinding[];   // worst first; empty while the report is not released
};
{
  "id": "3f9e2c7a-1b4d-4e8f-9a2c-5d6e7f8a9b0c",
  "reportReleased": true,
  "total": 1,
  "nextOffset": null,
  "findings": [
    {
      "id": "c1d0e2f3-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
      "title": "SQL injection in the search parameter",
      "description": "...",
      "severity": "CRITICAL",
      "confidence": "CONFIRMED",
      "affectedUrl": "https://staging.example.com/search?q=",
      "remediation": "...",
      "cvssScore": "9.8",
      "cweId": "CWE-89"
    }
  ]
}

Findings follow the same release rule as the dashboard, and reportReleased says whether you may see them yet. It is not the same as the run being finished:

  • Light includes no expert review, so its findings are released as soon as the run is paid for, and fill in while it runs. reportReleased is true before the run is COMPLETED.
  • Standard and Deep include an expert review. Nothing is released until a Barrion reviewer has signed the report off, which can be some time after the run reaches COMPLETED.
  • A run that ended FAILED or CANCELLED shows whatever it found, without review.

So to judge a run, wait for both: status is COMPLETED and reportReleased is true. Only findings that made it into the report are listed.

If a reviewer rejects a Standard or Deep report, reportReleased stays false, and the API cannot yet tell that apart from a review still in progress. If a report is not released within a few days of the run completing, check the run in the dashboard.

Cancel a pentest

POST /v1/pentests/{id}/cancel · permission pentests:write

curl -X POST https://api.barrion.io/v1/pentests/3f9e2c7a-1b4d-4e8f-9a2c-5d6e7f8a9b0c/cancel \
  -H "Authorization: Bearer $BARRION_API_KEY" \
  -d ''

The request has no body, but it must still send Content-Length: 0, which -d '' does. Without it the load balancer answers 411 Length Required.

Response 200

type CancelPentestResponse = {
  success: true;
  status: "COMPLETED" | "FAILED" | "CANCELLED"; // the run's status after the call
};

A cancelled run is charged only for the work it got through, and nothing if it had not started. Cancelling a run that has already ended is not an error: it answers { "success": true, "status": "COMPLETED" } and changes nothing, so a pipeline's cleanup step can always call it.