Docs REST API Reference - Security Scanning Endpoints

API Reference

The Guardr API lets you scan any website for security misconfigurations, manage your monitored sites and read results programmatically - perfect for CI/CD pipelines, internal dashboards and automated security checks.

Free tier - no key required REST · JSON responses HTTPS only

Free tier - no key required

Anyone can read a cached scan result with no API key and no signup. This response shape has no issues field - only grade, score and category breakdown. Limited to 20 requests/day per IP.

curl
curl https://api.guardr.io/v1/scan/example.com
200 OK
{
  "domain": "example.com",
  "scanned_at": "2026-04-18T10:00:00.000Z",
  "grade": "B+",
  "score": 83,
  "categories": {
    "tls": 90,
    "headers": 78,
    "cookies": 100,
    "dns": 80,
    "exposure": 100
  }
}

Authentication

Authenticate by passing your API key in the X-API-Key request header. Get your key from Dashboard → Settings → API Access.

Authentication header
X-API-Key: your_key_here
Keep your key secret. Never expose it in client-side JavaScript or commit it to version control. Store it as an environment variable.

Base URL

Base URL
https://api.guardr.io

Quickstart

Scan a domain and read the results in two steps.

1. Read a cached scan result (no key required)

curl
curl https://api.guardr.io/v1/scan/example.com

2. Trigger a fresh scan and read the result (API key required)

curl
curl -X POST https://api.guardr.io/v1/scan \
  -H "X-API-Key: your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'

Bulk scan - scan multiple domains

bash
domains=("example.com" "another.io" "mysite.com")

for domain in "${domains[@]}"; do
  curl -X POST https://api.guardr.io/v1/scan \
    -H "X-API-Key: your_key_here" \
    -H "Content-Type: application/json" \
    -d "{\"domain\": \"$domain\"}"
done

Quota is per-domain - scanning 10 different domains uses 10 quota slots, not 10× a single slot.

GET

/v1/scan/:domain

Returns the most recent cached scan result for a domain. Never triggers a new scan - use POST /v1/scan for that.

Parameters

Parameter Type Description
domain path Bare hostname, e.g. example.com

Example request

curl
curl https://api.guardr.io/v1/scan/example.com \
  -H "X-API-Key: your_key_here"

Response - Solo+ (full results)

200 OK
{
  "domain": "example.com",
  "scanned_at": "2026-04-18T10:00:00.000Z",
  "grade": "C",
  "score": 61,
  "categories": {
    "tls": 90,
    "headers": 40,
    "cookies": 100,
    "dns": 80,
    "exposure": 100
  },
  "issues": [
    {
      "title": "Missing: content-security-policy",
      "severity": "high",
      "category": "Security Headers",
      "description": "...",
      "remediation": {
        "summary": "...",
        "effort": "requires-planning",
        "snippets": [...]
      }
    }
  ],
  "issues_truncated": false,
  "total_issues": 4,
  "secrets_found": []
}

Response - Free tier (truncated)

200 OK
{
  "domain": "example.com",
  "grade": "C",
  "score": 61,
  "categories": { ... },
  "issues": [
    { "title": "...", "severity": "high", "description": "...", "remediation": { ... } },
    { "title": "...", "severity": "high", "description": "...", "remediation": { ... } },
    { "title": "...", "severity": "medium", "description": "..." }
  ],
  "issues_truncated": false,
  "total_issues": 9,
  "upgrade_url": "https://guardr.io/dashboard/billing"
}

Free accounts see every issue, with full remediation steps on the first two. Upgrade to Solo or higher for remediation steps on everything.

Response - 404 Not Found

404 Not Found
{
  "error": "scan_not_found",
  "message": "No scan found. Use POST /v1/scan to trigger one."
}
POST

/v1/scan

Free+

Triggers a fresh security scan for a domain. Runs inline and returns results in ~5–15 seconds. The result is cached for 1 hour and readable via GET /v1/scan/:domain.

Request body

Field Type Description
domain * string Domain to scan, e.g. example.com

Example request

curl
curl -X POST https://api.guardr.io/v1/scan \
  -H "X-API-Key: your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'

Response - 200 OK

Same scan-result shape as GET /v1/scan/:domain, tier-gated the same way.

200 OK
{
  "domain": "example.com",
  "scanned_at": "2026-04-18T10:00:00.000Z",
  "grade": "C",
  "score": 61,
  "categories": {
    "tls": 90,
    "headers": 40,
    "cookies": 100,
    "dns": 80,
    "exposure": 100
  },
  "issues": [
    {
      "title": "Missing: content-security-policy",
      "severity": "high",
      "category": "Security Headers",
      "description": "...",
      "remediation": {
        "summary": "...",
        "effort": "requires-planning",
        "snippets": [...]
      }
    }
  ],
  "issues_truncated": false,
  "total_issues": 4,
  "secrets_found": []
}

Response - 429 Too Many Requests

429 Too Many Requests
{
  "error": "quota_exceeded",
  "message": "Scan quota reached for example.com. Resets in ~18hr.",
  "retry_after": 64800,
  "upgrade_url": "https://guardr.io/dashboard/billing"
}
Quota: One scan per domain per quota window. The window matches your plan's scan frequency - daily for Free/Solo/Starter, every 6 hours for Pro and hourly for Agency. Quota is per-domain, so scanning 10 different domains uses 10 independent quota slots.
GET

/v1/account

Returns your current plan, quota configuration and list of active API keys.

curl
curl https://api.guardr.io/v1/account \
  -H "X-API-Key: your_key_here"
200 OK
{
  "plan": "starter",
  "quota": {
    "scan_window": 86400,
    "burst_per_minute": 30
  },
  "keys": [
    {
      "key_id": "...",
      "display": "••••••••••••••••f630",
      "label": "CI pipeline",
      "created_at": "18.04.2026",
      "last_used_at": "18.04.2026",
      "revoked": false
    }
  ]
}

Response - 401 Unauthorized

401 Unauthorized
{
  "error": "invalid_key",
  "message": "API key not recognized. Check your key at guardr.io/dashboard/settings."
}
GET

/v1/sites

Free+

Returns all active monitored sites for your account, including grade, uptime percentage and average response time. A monitoring object is included per site - see data_confidence below for how to read it honestly on a newly added site.

curl
curl https://api.guardr.io/v1/sites \
  -H "X-API-Key: your_key_here"
200 OK
{
  "sites": [
    {
      "id": "uuid",
      "domain": "example.com",
      "path": null,
      "created_at": "2026-04-18T10:00:00Z",
      "latest_grade": "B+",
      "latest_score": 83,
      "last_scanned_at": "2026-04-18T10:00:00Z",
      "is_up": true,
      "last_checked_at": "2026-04-18T10:05:00Z",
      "monitoring": {
        "window_days": 7,
        "monitoring_since": "2026-07-11T10:00:00Z",
        "data_confidence": "building",
        "estimated_full_confidence_at": "2026-07-21T18:00:00Z",
        "checks_count": 8,
        "uptime_percentage": 87.5,
        "avg_response_ms": 214,
        "latest_response_ms": 198
      }
    }
  ],
  "total": 1
}

Response - 401 Unauthorized

401 Unauthorized
{
  "error": "invalid_key",
  "message": "API key not recognized. Check your key at guardr.io/dashboard/settings."
}

Reading data_confidence honestly

Uptime stats need a run-in period before they mean anything. The monitoring object's data_confidence field tells you how far along a site is:

Value Meaning
pending No uptime checks recorded yet
building Fewer than 100 checks recorded - uptime_percentage is real but based on a small sample
established 100 or more checks recorded - the full picture

estimated_full_confidence_at is a fixed projection - monitoring_since plus 100 × your plan's probe interval - not a live recalculation. If checks are missed (probe outages, downtime), this timestamp can pass while data_confidence is still building; only checks_count reaching 100 flips it to established.

POST

/v1/sites

Free+

Adds a domain to your monitored sites and triggers an immediate first scan. Respects your plan's site limit (1 on Free).

Request body

Field Type Description
domain * string Domain to add, e.g. example.com. www prefix and protocol are stripped automatically.
curl
curl -X POST https://api.guardr.io/v1/sites \
  -H "X-API-Key: your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'
201 Created
{
  "site": {
    "id": "uuid",
    "domain": "example.com",
    "status": "active"
  }
}

Response - 409 Conflict

409 Conflict
{
  "error": "site_exists",
  "message": "example.com is already being monitored."
}
Returns 200 if the domain was previously deleted and has been reactivated. Returns 409 if already active.
DELETE

/v1/sites/:domain

Free+

Removes a site from monitoring. The domain must be echoed back in the request body to confirm - see below. The site, and its full history, can be restored at any time within 30 days.

Request body

Field Type Description
confirm_domain * string Must exactly match the domain in the URL path, or the request is rejected with 400 confirmation_required.
curl
curl -X DELETE https://api.guardr.io/v1/sites/example.com \
  -H "X-API-Key: your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"confirm_domain": "example.com"}'

Response - 200 OK

200 OK
{
  "success": true,
  "domain": "example.com"
}

Response - 400 Bad Request

400 Bad Request
{
  "error": "confirmation_required",
  "message": "Body must include {\"confirm_domain\": \"example.com\"} to confirm deletion."
}
This is a recycle-bin delete, not an immediate purge. The site and its full history (scans, uptime) stay recoverable - re-add the same domain within 30 days to restore everything exactly as it was. After 30 days it is permanently removed.

Sites can also be restored from Dashboard → Recently Deleted, which offers the same 30-day window with a per-site restore button - there's no bulk-restore option.

POST

/v1/enroll

No key required

Redeems an enrollment token for a permanent, single-site-bound API key and binds it to the given domain in one step. This is what the official WordPress plugin calls during setup - no existing API key is needed, because the token itself is the credential.

Request body

Field Type Description
enrollment_token * string Generated from Dashboard → Settings → Enrollment Tokens. Bounded-use and time-limited - see below.
domain * string Domain to bind this redemption to, e.g. example.com. www prefix and protocol are stripped automatically.
curl
curl -X POST https://api.guardr.io/v1/enroll \
  -H "Content-Type: application/json" \
  -d '{"enrollment_token": "your_token_here", "domain": "example.com"}'

Response - 201 Created

201 Created
{
  "key": "grd_live_...",
  "site": {
    "id": "a1b2c3d4-...",
    "domain": "example.com",
    "status": "active"
  },
  "shown_once": true,
  "message": "Copy this key now - it will not be shown again."
}
The key is shown exactly once, in this response. Store it immediately - Guardr never displays it again, the same as any other API key.

What happens to the domain

Every redemption spends one use from the token regardless of which case below applies - a token's uses_remaining only ever goes down.

Domain state on the token's account HTTP What happens
Not monitored yet 201 New site created and scanned immediately, new key issued bound to it
Already active 200 No new site - a new key is issued bound to the existing site (e.g. re-enrolling the same site with a fresh key)
In the 30-day recycle bin 200 Site is restored (subject to the plan's site limit - 403 site_limit_reached if there's no room) and a new key is issued bound to it

Response - 401 Unauthorized

401 Unauthorized
{
  "error": "invalid_token",
  "message": "Enrollment token not recognized."
}

Response - 409 Conflict

409 Conflict
{
  "error": "token_exhausted",
  "message": "This enrollment token has no redemptions remaining."
}

A token's max_uses is set once at creation time (capped at the account's remaining site capacity that moment) and only ever decreases - generate a new token rather than waiting for one to refill.

Enrollment token vs. API key - what's the difference?

An enrollment token is not an API key - it's a short-lived, bounded-use ticket, generated from a logged-in dashboard session, that a not-yet-authenticated caller (like a fresh WordPress install) redeems for one. Creating a token requires being logged in; redeeming one does not - the token itself is the credential, the same pattern as a password-reset or invite link.

Use this when... Use
You're connecting one site and already have (or can create) a key directly A plugin-scoped API key, pasted directly - create one from Dashboard → Settings with "Plugin-scoped key" checked
You're rolling out to many sites (e.g. an agency onboarding a fleet) and don't want to create and track a separate key per site One enrollment token, redeemed independently by each site - every redemption mints its own unique key automatically

Every key issued from the same token is linked back to it for auditing, and can be mass-revoked in one action from Dashboard → Settings → Enrollment Tokens - individually-created keys have no such grouping.

Enrollment token management

These four endpoints are how a token gets created, listed and revoked from Dashboard → Settings → Enrollment Tokens. They're called by the dashboard itself using your logged-in browser session, not an API key - shown here for reference on the full lifecycle, not as endpoints a third-party integration calls directly.

POST

/api/enrollment-tokens

Dashboard session required

Mints a new enrollment token. max_uses is capped at the lower of 50 and your plan's remaining site capacity at that moment - a token can never mint more keys than you currently have room for. Expiry is optional (24-72 hours, defaults to 48).

curl
curl -X POST https://guardr.io/api/enrollment-tokens \
  -b "session=your_dashboard_session_cookie" \
  -H "Content-Type: application/json" \
  -d '{"label": "Agency rollout - batch 1"}'

Response - 201 Created

201 Created
{
  "token": "grd_enroll_...",
  "token_id": "b7e2c1a0-...",
  "label": "Agency rollout - batch 1",
  "expires_at": "2026-07-25T10:00:00.000Z",
  "max_uses": 12,
  "shown_once": true,
  "message": "Copy this token now - it will not be shown again. Each redemption mints one permanent, site-bound plugin key."
}

Response - 403 Forbidden

403 Forbidden
{
  "error": "no_site_capacity",
  "message": "Your free plan has no remaining site slots (1 limit). Remove a site or upgrade before generating an enrollment token.",
  "upgrade_url": "/dashboard/billing"
}
GET

/api/enrollment-tokens

Dashboard session required

Lists every token on the account with its redemption counts - the data behind the "N keys issued" row in the dashboard. redemption_count includes revoked keys; active_redemption_count does not.

curl
curl https://guardr.io/api/enrollment-tokens \
  -b "session=your_dashboard_session_cookie"

Response - 200 OK

200 OK
{
  "tokens": [
    {
      "id": "b7e2c1a0-...",
      "label": "Agency rollout - batch 1",
      "expires_at": "2026-07-25T10:00:00.000Z",
      "max_uses": 12,
      "uses_remaining": 9,
      "created_at": "2026-07-23T10:00:00.000Z",
      "revoked": false,
      "expired": false,
      "redemption_count": 3,
      "active_redemption_count": 3
    }
  ]
}
GET

/api/enrollment-tokens/:id/redemptions

Dashboard session required

Lists every key ever issued from one token, each with the domain it's bound to and a masked key display - never the plaintext key itself, which was only ever shown once at redemption time.

curl
curl https://guardr.io/api/enrollment-tokens/b7e2c1a0-.../redemptions \
  -b "session=your_dashboard_session_cookie"

Response - 200 OK

200 OK
{
  "redemptions": [
    {
      "key_id": "f3a9...",
      "display": "grd_live_••••••••••••••••4f2a",
      "domain": "client-one.com",
      "created_at": "2026-07-23T10:04:00.000Z",
      "last_used_at": "2026-07-23T12:30:00.000Z",
      "revoked": false
    }
  ]
}

Response - 404 Not Found

404 Not Found
{
  "error": "not_found",
  "message": "Enrollment token not found."
}
POST

/api/enrollment-tokens/:id/revoke-all

Dashboard session required

Revokes every active key ever issued from this token, and the token itself, in one action - the same "N keys issued" row's mass-revoke button.

This revokes authentication only. The sites those keys were bound to are not deleted or paused - they stay active and keep being scanned and probed on schedule. Revoking a key just means the WordPress plugin (or whatever held it) can no longer read or rescan that site until it's reconnected with a new key.
curl
curl -X POST https://guardr.io/api/enrollment-tokens/b7e2c1a0-.../revoke-all \
  -b "session=your_dashboard_session_cookie"

Response - 200 OK

200 OK
{
  "success": true,
  "revoked_count": 3
}

Response - 404 Not Found

404 Not Found
{
  "error": "not_found",
  "message": "Enrollment token not found."
}

Rate limits

Two independent limits apply to every request: a per-minute burst limit and a per-domain scan quota.

Plan Burst limit Scan quota Quota window
Public (no key) 20 req/day per IP Read only -
Free 5 req/min 1/domain 7 days
Solo 15 req/min 1/domain 24 hours
Starter 30 req/min 1/domain 24 hours
Pro 60 req/min 1/domain 6 hours
Agency 120 req/min 1/domain 1 hour

X-RateLimit-* and Retry-After headers are scoped to GET /v1/scan/:domain and POST /v1/scan only, and only when called with an API key - they are not present on /v1/sites*, /v1/account, or the keyless public tier. Within that scope they're included on every response, success included - not just on 429.

Example headers on a rate-limited response:

X-RateLimit-Limit:     60
X-RateLimit-Remaining: 0
X-RateLimit-Reset:     1713441600
Retry-After:           47

Error codes

All errors return JSON with an error field and a human-readable message.

HTTP error Meaning
400invalid_domainDomain is malformed, private or blocked
400invalid_bodyRequest body is not valid JSON
400confirmation_requiredDELETE /v1/sites/:domain body is missing or doesn't match confirm_domain
400missing_tokenPOST /v1/enroll body is missing enrollment_token
401invalid_keyKey not found or revoked
401missing_keyEndpoint requires a key, none was provided
401invalid_tokenEnrollment token not recognized
403site_limit_reachedPlan site limit reached - upgrade to add more
404site_not_foundNo active site found for that domain
404scan_not_foundNo cached scan exists - use POST /v1/scan
404not_foundUnknown API endpoint
409site_existsDomain is already being monitored
409token_exhaustedEnrollment token has no redemptions remaining
410token_revokedEnrollment token has been revoked
410token_expiredEnrollment token has expired
422scan_failedScanner could not reach the domain
429rate_limit_exceededPer-minute burst limit hit
429quota_exceededPer-domain scan quota hit for this window

Plan comparison

Plan GET results POST scan Site API Keys
Public Grade + score only - - -
Free Top 2 issues 1
Solo $7/mo Full results 1
Starter $19/mo Full results 1
Pro $69/mo Full results 2
Agency $179/mo Full results 5

Get your API key →

Migrating from SecurityHeaders.com

SecurityHeaders.com shut down its free API in April 2026. Here's how to migrate to Guardr.

Before (SecurityHeaders.com)

curl "https://api.securityheaders.com/?q=example.com&hide=on&followRedirects=on" \
  -H "x-api-key: your_key"

After (Guardr)

curl -X POST https://api.guardr.io/v1/scan \
  -H "X-API-Key: your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'

What Guardr adds

Feature SecurityHeaders Guardr
Security headers scan
A–F grade
Remediation instructions-
SSL/TLS + HSTS check-
DNS security (DNSSEC and CAA)-
Exposed file detection (.env and .git)-
JS bundle secret scanning-
Site management via API-
Free tier-

Try it on your site - free

Free scan, no signup required. Upgrade when you need alerts, history, or PDF reports.

Scan your site →