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 |
| path | query, optional | Monitored path on that domain, e.g. ?path=/checkout. Defaults to / (the root). |
Example request
curl https://api.guardr.io/v1/scan/example.com \ -H "X-API-Key: your_key_here"
Example request - non-root path
curl https://api.guardr.io/v1/scan/example.com?path=/checkout \ -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 |
| path | string, optional | Path on that domain to scan, e.g. /checkout. Defaults to / (the root). |
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"}' Example request - non-root path
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", "path": "/checkout"}' 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": "/",
"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. |
| path | string, optional | Specific path to monitor on that domain, e.g. /checkout. Defaults to / (the root). Each path consumes its own site slot against your plan limit. |
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"}' Example request - monitoring a specific path
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", "path": "/checkout"}' {
"site": {
"id": "uuid",
"domain": "example.com",
"path": "/",
"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. |
| path | string, optional | Which monitored path on this domain to remove, e.g. /checkout. Defaults to / - deleting the root never affects a sibling path's own monitoring. |
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"}' Example request - removing a non-root path
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", "path": "/checkout"}' Response - 200 OK
{
"success": true,
"domain": "example.com",
"path": "/"
} 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
Every new account starts with 14 days on Pro. No credit card required. When the trial ends, the account settles onto the Free plan and the API scopes shown below for Free apply.
| 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 →