Documentation

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

POSThttps://shield-api.3stoneai.com/api/v1/shield/scan

Scan 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 }
}
GEThttps://shield-api.3stoneai.com/api/v1/shield/scans

List 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 }
}
GEThttps://shield-api.3stoneai.com/api/v1/shield/scans/:id

A 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": [...] }
}
GEThttps://shield-api.3stoneai.com/api/v1/shield/findings/:id

A 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[] */ ]
}
POSThttps://shield-api.3stoneai.com/api/v1/shield/findings/:id/remediation

Request 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
}
POSThttps://shield-api.3stoneai.com/api/v1/shield/findings/:id/verify

Real, 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..."
}
GEThttps://shield-api.3stoneai.com/api/v1/shield/regressions

Compares 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": {...} } ]
}
GEThttps://shield-api.3stoneai.com/api/v1/shield/usage

Check 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
}
POSThttps://shield-api.3stoneai.com/api/v1/shield/projects

Register 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 } }
GEThttps://shield-api.3stoneai.com/api/v1/shield/projects

List your registered projects.

Response

{ "ok": true, "projects": [ { "id": "...", "name": "...", "productionUrl": "...", "monitoringEnabled": true, "scanFrequency": "weekly", "lastScanAt": "...", "lastScanStatus": "completed" } ] }
GEThttps://shield-api.3stoneai.com/api/v1/shield/projects/:id

A project's detail, plus real open/critical issue counts.

Response

{ "ok": true, "project": { /* ... */ }, "openIssueCount": 4, "criticalOpenCount": 1 }
PATCHhttps://shield-api.3stoneai.com/api/v1/shield/projects/:id

Rename, 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 */ } }
DELETEhttps://shield-api.3stoneai.com/api/v1/shield/projects/:id

Delete a project. Its scans and issue history are removed with it.

Response

{ "ok": true }
POSThttps://shield-api.3stoneai.com/api/v1/shield/projects/:id/scan

Scan 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)." }
}
GEThttps://shield-api.3stoneai.com/api/v1/shield/projects/:id/issues

Real 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.

FieldTypeMeaning
findingId / idstring (uuid)Stable identifier for this finding.
ruleIdstringThe real axe-core rule that fired, e.g. "image-alt".
impact"critical"|"serious"|"moderate"|"minor"|nullNull for needs_review findings - axe-core didn't reach a severity judgment either.
wcagCriteriastring[]Raw axe-core tags, e.g. ["wcag2a","wcag111"].
wcagLevel"A"|"AA"|"AAA"|nullDerived from wcagCriteria - null if the finding has no numbered WCAG tag at all (a best-practice-only finding).
targetSelectorstringThe real DOM selector axe-core matched.
htmlSnippetstring|nullReal HTML captured from the page, truncated to 500 characters.
occurrencesnumberHow 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.
manualReviewRequiredbooleanTrue only for needs_review findings.

Errors

StatusError messageCause
401Missing Authorization header. Use: Authorization: Bearer sk_shield_...No Authorization header sent.
401Invalid API key.The key doesn't match any active key on record.
403This API key has been deactivated.Key exists but is_active is false.
429Monthly call limit reached for this API key.callsUsedThisPeriod >= callsLimitPerPeriod for your current plan.
400A 'url' field is required in the request body.Missing/empty required field.
404Scan 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.
502Could 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.

PolicyBehavior
fail_on_any_newAny new or recurring issue fails CI.
fail_on_criticalOnly new/recurring critical-impact issues fail CI (the default the GitHub Action uses).
fail_on_critical_and_seriousCritical or serious-impact issues fail CI.
report_onlyNever 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_critical

Fails 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.