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
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 https://api.guardr.io/v1/scan/example.com
{
"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.
X-API-Key: your_key_here
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 https://api.guardr.io/v1/scan/example.com
2. Trigger a fresh scan and read the result (API key required)
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
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.
/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 https://api.guardr.io/v1/scan/example.com \ -H "X-API-Key: your_key_here"
Response - Solo+ (full results)
{
"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)
{
"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
{
"error": "scan_not_found",
"message": "No scan found. Use POST /v1/scan to trigger one."
} /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 -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.
{
"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
{
"error": "quota_exceeded",
"message": "Scan quota reached for example.com. Resets in ~18hr.",
"retry_after": 64800,
"upgrade_url": "https://guardr.io/dashboard/billing"
} /v1/account
Returns your current plan, quota configuration and list of active API keys.
curl https://api.guardr.io/v1/account \ -H "X-API-Key: your_key_here"
{
"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
{
"error": "invalid_key",
"message": "API key not recognized. Check your key at guardr.io/dashboard/settings."
} /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 https://api.guardr.io/v1/sites \ -H "X-API-Key: your_key_here"
{
"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
{
"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.
/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 -X POST https://api.guardr.io/v1/sites \
-H "X-API-Key: your_key_here" \
-H "Content-Type: application/json" \
-d '{"domain": "example.com"}' {
"site": {
"id": "uuid",
"domain": "example.com",
"status": "active"
}
} Response - 409 Conflict
{
"error": "site_exists",
"message": "example.com is already being monitored."
} 200 if the domain was previously deleted and has been reactivated. Returns 409 if already active.
/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 -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
{
"success": true,
"domain": "example.com"
} Response - 400 Bad Request
{
"error": "confirmation_required",
"message": "Body must include {\"confirm_domain\": \"example.com\"} to confirm deletion."
} 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.
/v1/enroll
No key requiredRedeems 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 -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
{
"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."
} 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
{
"error": "invalid_token",
"message": "Enrollment token not recognized."
} Response - 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.
/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 -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
{
"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
{
"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"
} /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 https://guardr.io/api/enrollment-tokens \ -b "session=your_dashboard_session_cookie"
Response - 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
}
]
} /api/enrollment-tokens/:id/redemptions
Dashboard session requiredLists 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 https://guardr.io/api/enrollment-tokens/b7e2c1a0-.../redemptions \ -b "session=your_dashboard_session_cookie"
Response - 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
{
"error": "not_found",
"message": "Enrollment token not found."
} /api/enrollment-tokens/:id/revoke-all
Dashboard session requiredRevokes 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.
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 -X POST https://guardr.io/api/enrollment-tokens/b7e2c1a0-.../revoke-all \ -b "session=your_dashboard_session_cookie"
Response - 200 OK
{
"success": true,
"revoked_count": 3
} Response - 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 |
|---|---|---|
| 400 | invalid_domain | Domain is malformed, private or blocked |
| 400 | invalid_body | Request body is not valid JSON |
| 400 | confirmation_required | DELETE /v1/sites/:domain body is missing or doesn't match confirm_domain |
| 400 | missing_token | POST /v1/enroll body is missing enrollment_token |
| 401 | invalid_key | Key not found or revoked |
| 401 | missing_key | Endpoint requires a key, none was provided |
| 401 | invalid_token | Enrollment token not recognized |
| 403 | site_limit_reached | Plan site limit reached - upgrade to add more |
| 404 | site_not_found | No active site found for that domain |
| 404 | scan_not_found | No cached scan exists - use POST /v1/scan |
| 404 | not_found | Unknown API endpoint |
| 409 | site_exists | Domain is already being monitored |
| 409 | token_exhausted | Enrollment token has no redemptions remaining |
| 410 | token_revoked | Enrollment token has been revoked |
| 410 | token_expired | Enrollment token has expired |
| 422 | scan_failed | Scanner could not reach the domain |
| 429 | rate_limit_exceeded | Per-minute burst limit hit |
| 429 | quota_exceeded | Per-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 |
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 →