Skip to content

Public API

The public API reads your HeaderHawk data from your own code: your sites, the issues browsers have reported on them and the reports behind each one, network errors and crashes, the deprecated APIs your pages use, the pages you monitor with their scans, the alerts HeaderHawk sent, your script inventory, and each site’s PCI DSS 4.0 readiness. It is read-only: nothing it does changes your account.

Every operation, with its parameters and responses, is in the reference. This page walks through a first session with it.

API access is on the Professional plan and above. An owner or admin creates keys under Settings › API keys. A key looks like hh_live_ followed by 49 more characters, and is shown once, when you create it: store it as a secret, as you would a database password.

A key reads what a viewer reads, across every site in your account, and works until it expires or someone revokes it. Revoking a key takes effect within 60 seconds.

Every request goes to https://api.us.headerhawk.com and carries the key as a bearer token. GET /v1/me is the call that always works with a valid key, so it is the one to try first:

Terminal window
curl https://api.us.headerhawk.com/v1/me \
-H "Authorization: Bearer $HEADERHAWK_API_KEY"
{
"tenant": {
"id": "…",
"name": "Example Ltd",
"plan": "professional",
"retentionDays": 90
},
"key": {
"id": "…",
"name": "CI",
"prefix": "a1b2c3d4",
"createdAt": "2026-10-01T09:00:00.000Z",
"expiresAt": "2027-10-01T09:00:00.000Z"
},
"rateLimits": {
"key": { "limit": 600, "windowSeconds": 60 },
"tenant": { "limit": 60, "windowSeconds": 60 }
}
}

A 401 with the code invalid_api_key means the key is missing, malformed, unknown, revoked or expired. The answer is the same for all five, so it cannot be used to find out which keys exist.

An issue groups the reports about the same thing, such as one CSP directive blocking one host, whichever page sent them. GET /v1/issues lists them across every site, most recently seen first:

Terminal window
curl "https://api.us.headerhawk.com/v1/issues?limit=20" \
-H "Authorization: Bearer $HEADERHAWK_API_KEY"
{
"data": [
{
"id": "…",
"siteId": "…",
"reportType": "csp-violation",
"subject": "script-src-elem",
"targetHost": "cdn.example.net",
"reportCount": 412,
"lastSeen": "2026-10-01T08:41:07.000Z"
}
],
"nextCursor": "eyJ2IjoxLCJvIjoyMH0"
}

The example leaves out most of each issue’s fields; the reference lists them all.

Operations that read reported data cover a window of time. Without from and to, the issue lists cover the last 7 days, and you can ask for up to 31. Each operation’s reference page gives its default and its limit, and a longer window is refused with 400 rather than cut short. For a longer period, ask for consecutive windows.

A list takes limit (from 1 to 200, 50 by default) and answers with nextCursor. Pass it back as cursor, with the same other parameters, for the next page. On the last page it is null.

async function allIssues(key) {
const issues = [];
let cursor = null;
do {
const url = new URL("https://api.us.headerhawk.com/v1/issues");
url.searchParams.set("limit", "200");
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${key}` },
});
if (!response.ok) throw new Error(`HeaderHawk answered ${response.status}`);
const page = await response.json();
issues.push(...page.data);
cursor = page.nextCursor;
} while (cursor);
return issues;
}

A cursor is opaque and belongs to the query that produced it: sent with different parameters, it is refused with 400.

Each key may make 600 requests a minute. The operations that read reported data (each one’s reference page says so) also share a budget of 60 requests a minute across your whole account, whichever key spends it (an Enterprise contract can set a higher one). GET /v1/me shows both limits.

A request that reaches a budget gets three headers back for whichever budget is nearest to running out:

Header Meaning
RateLimit-Limit Requests allowed in the current window
RateLimit-Remaining Requests left in it
RateLimit-Reset Seconds until it starts again

Over a budget, the answer is 429 with the code rate_limited and a Retry-After header, in seconds. Wait that long and send the request again:

async function get(url, key) {
for (let attempt = 0; attempt < 5; attempt++) {
const response = await fetch(url, {
headers: { Authorization: `Bearer ${key}` },
});
if (response.status !== 429 && response.status !== 503) return response;
const seconds = Number(response.headers.get("Retry-After") ?? "5");
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
throw new Error("HeaderHawk is still busy; try again later");
}

A 503 with Retry-After means HeaderHawk could not answer just then, and is never a sign that the request was wrong: retry it the same way. Errors lists every code and what to do about each.