Skip to main content
RedBlueQARedBlueQA
API & CLI Docs
Upgrade
API & CLI REFERENCE

RedBlueQA CLI Documentation

Trigger scans from your terminal, CI/CD pipeline, or any HTTP-capable program. Every endpoint here returns JSON and uses standard Bearer-token authentication.

Quick Start

Three commands to test that everything works:

bash
# 1. Set up your shell (key never enters shell history)
read -s -p "API key: " RBQA_KEY && echo && export RBQA_KEY
export BASE="https://redblueqa.com"

# 2. Trigger a scan — returns 202 with a scanId
curl -X POST $BASE/api/v1/scans \
  -H "Authorization: Bearer $RBQA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","pages":["https://example.com"]}'

# 3. Open the result in your dashboard
echo "Visit: $BASE/scans/<scanId-from-response>"

Authentication

Two header formats are accepted — pick whichever your tooling prefers:

bash
# Authorization Bearer (standard)
curl -H "Authorization: Bearer rbqa_xxx..." $BASE/api/v1/scans

# x-api-key (some tools — Postman, Hoppscotch — default to this)
curl -H "x-api-key: rbqa_xxx..." $BASE/api/v1/scans

Get your key at /dashboard/api-keys. Keys start with rbqa_ followed by 32 hex characters. We only store the SHA-256 hash — the raw key is shown once at creation time and never again.

Trigger a Scan

The minimum body is just url. Everything else is optional with sensible defaults.

json
POST $BASE/api/v1/scans

{
  "url": "https://example.com",          // required: target site
  "pages": ["https://example.com/path"], // optional: skip crawler, scan these
  "personaIds": ["angry-clicker"],       // optional: specific personas
  "personaCategories": ["security"],     // optional: whole categories
  "auth": {                              // optional: log in first
    "loginUrl": "https://example.com/login",
    "email": "test@example.com",
    "password": "..."
  }
}

Response is 202 Accepted immediately. The scan runs asynchronously — you poll for status and fetch bugs once complete.

json
{
  "scanId": "b94d8770-ea52-43a4-9106-6774f02108df",
  "liveScanId": "f048698f-5fb1-4587-b5a5-7f9d08ba258c",
  "status": "queued",
  "url": "https://example.com/",
  "personas": 2,
  "pages": 9,
  "batches": 2
}

Personas + Categories

You can pass either or both:

  • personaIds: [...] — list specific persona slugs (built-ins) and/or custom persona UUIDs
  • personaCategories: [...] — pass a category name and the API expands it server-side

The two are merged and deduplicated. So personaCategories: ["security"] gets you all 6 security personas without typing them.

Available categories

core
angry-clicker, slow-mobile, form-attacker, lost-user, keyboard-only
accessibility
keyboard-only, screen-reader, high-contrast, zoom-200, motor-impaired
security
xss-hunter, sql-injector, path-traverser, csrf-tester, cookie-mangler, url-fuzzer
form-edge-cases
form-attacker, empty-submitter, max-length-blaster, emoji-spammer, special-chars, negative-numbers
mobile-device
slow-mobile, tiny-screen, tablet-user, foldable-phone, large-desktop, old-android
network-performance
flaky-connection, offline-first, throttled-3g, high-latency
internationalisation
rtl-user, cjk-user, german-user, hindi-user
session-auth
session-expirer, multi-tab, back-button-abuser, incognito-user, bookmark-jumper
javascript-rendering
js-disabled, ad-blocker, slow-cpu, memory-hog
e-commerce
cart-abandoner, coupon-hacker, quantity-extremes
power-users
rapid-fire, scroll-maniac, drag-dropper, right-clicker, copy-paster, print-user, devtools-open

Defaults

If you pass neither personaIds nor personaCategories, the API runs 1 default persona (Angry Clicker). Plan caps:

  • Free: 1 persona
  • Pro: 8 personas
  • Pro Plus: 52 + 3 custom personas
  • Enterprise: 52 + 10 custom personas

Scanning Logged-In Pages

If your target sits behind a login, pass auth:

bash
curl -X POST $BASE/api/v1/scans \
  -H "Authorization: Bearer $RBQA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/dashboard",
    "personaCategories": ["security"],
    "auth": {
      "loginUrl": "https://your-app.com/login",
      "email": "qa-sandbox@example.com",
      "password": "test-password-here"
    }
  }'

⚠️ Use a sandbox account. The scanner is adversarial — it will fill forms with garbage, click everything, possibly break session state. Don't aim it at a real user account.

The scanner uses generic selectors (input[type=email], input[type=password], submit button) that work with most apps. If your login uses Google OAuth or another flow that doesn't accept email+password, this won't work — you'd need a separate sandbox login that does.

Polling for Status

After triggering, poll /api/v1/scans/{scanId} until status is completed:

bash
SCAN_ID="paste-scanId-here"

while true; do
  DATA=$(curl -s $BASE/api/v1/scans/$SCAN_ID \
    -H "Authorization: Bearer $RBQA_KEY")
  STATUS=$(echo "$DATA" | jq -r '.status')
  BUGS=$(echo "$DATA" | jq -r '.summary.bugCount')
  echo "[$(date +%H:%M:%S)] $STATUS — $BUGS bugs so far"
  [ "$STATUS" = "completed" ] && break
  [ "$STATUS" = "failed" ] && exit 1
  sleep 15
done

Typical scan times: 2-5 minutes for 1-2 personas on a single page; 30-90 minutes for all 52 personas with auth on a multi-page site.

Fetching Bugs

Once the scan is completed, fetch the deduplicated bug list:

bash
# All bugs
curl $BASE/api/v1/scans/$SCAN_ID/bugs \
  -H "Authorization: Bearer $RBQA_KEY" | jq .

# Filter by severity (server-side)
curl "$BASE/api/v1/scans/$SCAN_ID/bugs?severity=critical" \
  -H "Authorization: Bearer $RBQA_KEY" | jq .

# Include screenshots (large, base64 data: URIs)
curl "$BASE/api/v1/scans/$SCAN_ID/bugs?screenshots=1" \
  -H "Authorization: Bearer $RBQA_KEY" > bugs-with-screenshots.json

Each bug includes: type, severity, message, url, personaName, rootCause, suggestedFix, impact.

Error Reference

202Scan accepted and queued — normal success response
400Bad URL (localhost, non-http, unparseable) or invalid JSON body
401Missing or invalid API key
403Plan quota exceeded, or feature not on your tier (e.g. custom personas without Pro Plus)
404Scan ID not found OR belongs to another user (always 404 either way)
429Rate limit exceeded — 10 scans/min per key
500Server error — check dashboard, may be transient

Rate Limits + Quotas

  • Per-key rate limit: 10 scans per minute per API key (returns 429)
  • Monthly scan quota: enforced per account, depends on plan (Free=3, Pro=30, Pro Plus=100, Enterprise=300)
  • Page caps per scan: Free=3, Pro=10, Pro Plus=50, Enterprise=500
  • Persona caps per scan: Free=1, Pro=8, Pro Plus=52+3 custom, Enterprise=52+10 custom
  • API key cap: max 10 keys per account (revoke unused ones)

GitHub Action Template

Drop this into your repo at .github/workflows/redblueqa-scan.yml. Customer-ready templates with full bug-breakdown PR comments live in ci/customer-templates/.

yaml
name: RedBlueQA Scan
on: pull_request

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger
        id: t
        run: |
          RESP=$(curl -sf -X POST https://redblueqa.com/api/v1/scans \
            -H "Authorization: Bearer ${{ secrets.RBQA_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d '{"url":"https://your-app.com","personaCategories":["security"]}')
          echo "id=$(echo "$RESP" | jq -r .scanId)" >> $GITHUB_OUTPUT

      - name: Wait
        run: |
          while true; do
            S=$(curl -sf https://redblueqa.com/api/v1/scans/${{ steps.t.outputs.id }} \
              -H "Authorization: Bearer ${{ secrets.RBQA_API_KEY }}" | jq -r .status)
            echo "$(date +%H:%M:%S) $S"
            [ "$S" = "completed" ] && break
            [ "$S" = "failed" ] && exit 1
            sleep 15
          done

Other Languages

Node.js

javascript
const RBQA_KEY = process.env.RBQA_API_KEY;
const BASE = 'https://redblueqa.com';

const res = await fetch(`${BASE}/api/v1/scans`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${RBQA_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://example.com',
    personaCategories: ['accessibility', 'security'],
  }),
});
const { scanId } = await res.json();
console.log('Scan queued:', scanId);

Python

python
import os, requests

key = os.environ['RBQA_API_KEY']
base = 'https://redblueqa.com'

resp = requests.post(
    f'{base}/api/v1/scans',
    headers={'Authorization': f'Bearer {key}'},
    json={
        'url': 'https://example.com',
        'personaCategories': ['accessibility', 'security'],
    },
)
resp.raise_for_status()
print('Scan queued:', resp.json()['scanId'])

Go

go
package main

import (
  "bytes"
  "encoding/json"
  "fmt"
  "io"
  "net/http"
  "os"
)

func main() {
  body, _ := json.Marshal(map[string]any{
    "url":               "https://example.com",
    "personaCategories": []string{"accessibility", "security"},
  })
  req, _ := http.NewRequest("POST",
    "https://redblueqa.com/api/v1/scans", bytes.NewReader(body))
  req.Header.Set("Authorization", "Bearer "+os.Getenv("RBQA_API_KEY"))
  req.Header.Set("Content-Type", "application/json")
  resp, _ := http.DefaultClient.Do(req)
  defer resp.Body.Close()
  out, _ := io.ReadAll(resp.Body)
  fmt.Println(string(out))
}
Need an API key? Create one in the dashboard →