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:
# 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:
# 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.
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.
{
"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 UUIDspersonaCategories: [...]— 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
coreaccessibilitysecurityform-edge-casesmobile-devicenetwork-performanceinternationalisationsession-authjavascript-renderinge-commercepower-usersDefaults
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:
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:
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
doneTypical 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:
# 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 response400Bad URL (localhost, non-http, unparseable) or invalid JSON body401Missing or invalid API key403Plan 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 key500Server error — check dashboard, may be transientRate 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/.
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
doneOther Languages
Node.js
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
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
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))
}