3Stone Shield API reference
Every endpoint below is live and real - nothing described here is planned or partial. See the quickstart for pricing and a first request.
Authentication
Every request (except the free scan tool) needs a real API key in the Authorization header:
Authorization: Bearer sk_shield_...Keys are issued automatically by email immediately after checkout - see pricing. Manage or cancel at https://shield-api.3stoneai.com/shield/manage.
Endpoints
https://shield-api.3stoneai.com/api/v1/shield/scanScan a URL. Runs synchronously (typically 5-30s) - a single-page scan comfortably finishes inside a normal request, so there's no job-polling step to build against.
Request body
{ "url": "https://example.com" }Response
{
"ok": true,
"scanId": "9a4e0667-...",
"url": "https://example.com",
"pageTitle": "Example Domain",
"violations": [ /* RawFinding[] - see Finding object below */ ],
"needsReview": [ /* RawFinding[] - axe-core "incomplete" results, see below */ ],
"report": {
"summary": "...",
"prioritizedActions": [ /* AI-generated, plain-language */ ],
"manualReviewCount": 1
},
"usage": { "callsUsedThisPeriod": 12, "callsLimitPerPeriod": 500 }
}https://shield-api.3stoneai.com/api/v1/shield/scansList your own scan history. Query params: url (filter), limit (default 20, max 100), offset.
Response
{
"ok": true,
"scans": [
{ "scanId": "...", "url": "...", "status": "completed", "requestedAt": "...", "completedAt": "...", "costUsd": 0.014 }
],
"pagination": { "limit": 20, "offset": 0, "total": 3 }
}https://shield-api.3stoneai.com/api/v1/shield/scans/:idA specific scan's full status, findings, and report. 404 if the scan doesn't exist or isn't yours.
Response
{
"ok": true, "scanId": "...", "url": "...", "status": "completed",
"requestedAt": "...", "completedAt": "...",
"violations": [ /* Finding[] */ ], "needsReview": [ /* Finding[] */ ],
"report": { "summary": "...", "prioritizedActions": [...] }
}https://shield-api.3stoneai.com/api/v1/shield/findings/:idA single finding, plus any remediations and verifications already generated for it.
Response
{
"ok": true, "findingId": "...", "scanId": "...", "ruleId": "image-alt",
"impact": "critical", "wcagCriteria": ["wcag2a","wcag111"], "wcagLevel": "A",
"detectionMethod": "automated", "manualReviewRequired": false,
"verificationStatus": "unverified",
"remediations": [ /* Remediation[] */ ], "verifications": [ /* Verification[] */ ]
}https://shield-api.3stoneai.com/api/v1/shield/findings/:id/remediationRequest an AI-proposed fix for one finding, grounded in the real HTML snippet captured at scan time. Not generated automatically for every finding - opt in per finding, real AI cost per call.
Response
{
"ok": true, "remediationId": "...", "findingId": "...",
"explanation": "...", "wcagReference": "WCAG 2.1 SC 1.1.1, Level A",
"affectedCode": "<img src=\"...\">", "proposedCode": "<img src=\"...\" alt=\"\">",
"diff": "- <img src=\"...\">\n+ <img src=\"...\" alt=\"\">",
"changeExplanation": "...", "confidence": 0.7,
"potentialRisks": "...", "humanReviewRecommended": true,
"verified": false
}https://shield-api.3stoneai.com/api/v1/shield/findings/:id/verifyReal, rescan-based verification. Body: { rescanUrl } (required - the URL where the fix is actually deployed, production or a preview/staging URL), { remediationId } (optional). Costs a real scan.
Request body
{ "rescanUrl": "https://example.com", "remediationId": "..." }Response
{
"ok": true, "findingId": "...", "status": "verified_resolved",
"originalFindingPresent": false, "newFindingsIntroduced": 0,
"notes": "...", "rescanId": "...",
"disclaimer": "This verifies that this specific finding was not detected on rescan. It does not certify overall WCAG conformance..."
}https://shield-api.3stoneai.com/api/v1/shield/regressionsCompares your two most recent completed scans of a URL. Query param: url (required). For a registered project's full issue history instead, use GET /projects/:id/issues below.
Response
{
"ok": true, "url": "...",
"latestScan": { "id": "...", "requestedAt": "..." },
"previousScan": { "id": "...", "requestedAt": "..." },
"newFindings": [ /* Finding[] */ ], "resolvedFindings": [ /* Finding[] */ ],
"recurringFindings": [ /* Finding[] */ ], "severityChanges": [ { "before": {...}, "after": {...} } ]
}https://shield-api.3stoneai.com/api/v1/shield/usageCheck your current usage without spending a scan to see it.
Response
{
"ok": true, "email": "you@company.com", "tierKey": "3Stone Shield API — Starter",
"callsUsedThisPeriod": 12, "callsLimitPerPeriod": 500,
"periodStart": "2026-08-01T00:00:00Z", "isActive": true
}https://shield-api.3stoneai.com/api/v1/shield/projectsRegister a site to monitor. Scans run "for" a project compare against its own history instead of being one-off - this is what powers regression monitoring and CI.
Request body
{ "name": "My Site", "productionUrl": "https://example.com", "monitoringEnabled": false, "scanFrequency": "manual" }Response
{ "ok": true, "project": { "id": "...", "name": "My Site", "productionUrl": "https://example.com", "monitoringEnabled": false, "scanFrequency": "manual", "lastScanAt": null, "nextScanAt": null } }https://shield-api.3stoneai.com/api/v1/shield/projectsList your registered projects.
Response
{ "ok": true, "projects": [ { "id": "...", "name": "...", "productionUrl": "...", "monitoringEnabled": true, "scanFrequency": "weekly", "lastScanAt": "...", "lastScanStatus": "completed" } ] }https://shield-api.3stoneai.com/api/v1/shield/projects/:idA project's detail, plus real open/critical issue counts.
Response
{ "ok": true, "project": { /* ... */ }, "openIssueCount": 4, "criticalOpenCount": 1 }https://shield-api.3stoneai.com/api/v1/shield/projects/:idRename, change the URL, or toggle monitoring/scan frequency. Any field omitted is left unchanged.
Request body
{ "monitoringEnabled": true, "scanFrequency": "weekly" }Response
{ "ok": true, "project": { /* updated */ } }https://shield-api.3stoneai.com/api/v1/shield/projects/:idDelete a project. Its scans and issue history are removed with it.
Response
{ "ok": true }https://shield-api.3stoneai.com/api/v1/shield/projects/:id/scanScan a project's real URL and classify every finding against its own history: NEW, RESOLVED, UNCHANGED, or RECURRING (an issue that was resolved and came back). A project's first scan establishes a baseline - everything it finds is real pre-existing state, not a CI regression, so adopting Shield on a site with 200 existing issues doesn't fail CI on issue #1. Optional failurePolicy returns a real CI verdict - this is the same endpoint the GitHub Action calls.
Request body
{ "failurePolicy": "fail_on_critical" }Response
{
"ok": true, "scanId": "...", "projectId": "...",
"violations": [ /* Finding[] */ ], "needsReview": [ /* Finding[] */ ],
"report": { "summary": "...", "prioritizedActions": [...] },
"baselineScan": false,
"regressions": { "new": 1, "resolved": 2, "unchanged": 8, "recurring": 0 },
"ci": { "status": "passed", "policy": "fail_on_critical", "reason": "No new or recurring issues at critical severity (1 lower-severity issue reported but not blocking)." }
}https://shield-api.3stoneai.com/api/v1/shield/projects/:id/issuesReal issue history for a project - first detected, last detected, times detected, current status. Optional ?status=open|resolved filter.
Response
{
"ok": true, "projectId": "...",
"issues": [
{ "id": "...", "ruleId": "image-alt", "targetSelector": "img.hero", "impact": "critical",
"status": "open", "firstDetectedAt": "2026-08-01T...", "lastDetectedAt": "2026-08-10T...",
"timesDetected": 6, "timesResolved": 0 }
]
}The Finding object
Every item in violations and needsReview shares this shape.
| Field | Type | Meaning |
|---|---|---|
| findingId / id | string (uuid) | Stable identifier for this finding. |
| ruleId | string | The real axe-core rule that fired, e.g. "image-alt". |
| impact | "critical"|"serious"|"moderate"|"minor"|null | Null for needs_review findings - axe-core didn't reach a severity judgment either. |
| wcagCriteria | string[] | Raw axe-core tags, e.g. ["wcag2a","wcag111"]. |
| wcagLevel | "A"|"AA"|"AAA"|null | Derived from wcagCriteria - null if the finding has no numbered WCAG tag at all (a best-practice-only finding). |
| targetSelector | string | The real DOM selector axe-core matched. |
| htmlSnippet | string|null | Real HTML captured from the page, truncated to 500 characters. |
| occurrences | number | How many elements this exact violation matched. |
| detectionMethod | "automated"|"needs_review" | automated = definite violation. needs_review = axe-core's own "incomplete" result - could not auto-resolve pass/fail. |
| manualReviewRequired | boolean | True only for needs_review findings. |
Errors
| Status | Error message | Cause |
|---|---|---|
| 401 | Missing Authorization header. Use: Authorization: Bearer sk_shield_... | No Authorization header sent. |
| 401 | Invalid API key. | The key doesn't match any active key on record. |
| 403 | This API key has been deactivated. | Key exists but is_active is false. |
| 429 | Monthly call limit reached for this API key. | callsUsedThisPeriod >= callsLimitPerPeriod for your current plan. |
| 400 | A 'url' field is required in the request body. | Missing/empty required field. |
| 404 | Scan not found. / Finding not found. | Doesn't exist, or belongs to a different API key. |
| 422 | (scan-specific error message) | The scan itself failed - page didn't load, SSRF-blocked target, timeout, etc. |
| 502 | Could not generate a remediation. / Verification failed to run. | The AI call or rescan itself failed - safe to retry. |
Rate limits & usage
Each plan has a monthly call cap (Starter: 500, Growth: 5,000 - see pricing). Every response includes your current usage:
"usage": { "callsUsedThisPeriod": 12, "callsLimitPerPeriod": 500 }The free scan tool (no API key) is separately capped at 3 scans per email per 24 hours and 30 scans globally per 10 minutes, to bound abuse cost - not meant for integration, see the API instead.
Webhooks / events
Not implemented yet. Every response above is synchronous and complete - there's currently no async event/webhook system to document, and none is described anywhere else in these docs. This section will be filled in if and when that ships.
Examples
cURL
curl -X POST https://shield-api.3stoneai.com/api/v1/shield/scan \
-H "Authorization: Bearer sk_shield_..." \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'JavaScript / TypeScript
const res = await fetch("https://shield-api.3stoneai.com/api/v1/shield/scan", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SHIELD_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://example.com" }),
});
const data = await res.json();
console.log(data.violations.length, "violations,", data.needsReview.length, "need review");Python
import os, requests
res = requests.post(
"https://shield-api.3stoneai.com/api/v1/shield/scan",
headers={"Authorization": f"Bearer {os.environ['SHIELD_API_KEY']}"},
json={"url": "https://example.com"},
)
data = res.json()
print(len(data["violations"]), "violations,", len(data["needsReview"]), "need review")Failure policies
Passed to POST /projects/:id/scan as failurePolicy - controls what counts as a CI failure. A project's first scan always establishes a baseline regardless of policy: adopting Shield on a site with 200 existing issues doesn't fail CI on issue #1, only on what's genuinely new afterward.
| Policy | Behavior |
|---|---|
| fail_on_any_new | Any new or recurring issue fails CI. |
| fail_on_critical | Only new/recurring critical-impact issues fail CI (the default the GitHub Action uses). |
| fail_on_critical_and_serious | Critical or serious-impact issues fail CI. |
| report_only | Never fails CI - numbers are still returned for visibility. |
CI/CD & GitHub Action
No dedicated GitHub App - building one that isn't a hollow badge requires its own registered credentials and review process this product doesn't have, and wouldn't add anything a real API key can't already do. What exists instead: a real, workingcomposite GitHub Actionauthenticated with your own Shield API key (stored as a GitHub Secret, never in the workflow file), calling the exact endpoint documented above.
# .github/workflows/accessibility.yml
name: Accessibility check
on: [pull_request]
jobs:
shield-scan:
runs-on: ubuntu-latest
steps:
- name: Deploy preview
id: deploy
run: echo "url=https://your-preview-url.example.com" >> "$GITHUB_OUTPUT"
# Replace with your real preview-deployment step
- name: 3Stone Shield scan
uses: jathanks3/3stone/integrations/github-action@main
with:
api-key: ${{ secrets.SHIELD_API_KEY }}
project-id: "<your project id - see POST /projects above>"
url: ${{ steps.deploy.outputs.url }}
failure-policy: fail_on_criticalFails the job on a real regression per the chosen policy and posts a real PR comment (new/ resolved/unchanged counts, using GitHub's own built-in token - no extra credential). Full setup instructions in the Action's own README.