Skip to content

Ingestion API

Browsers post reports to HeaderHawk directly. You never call this endpoint from your own code — you name it in a report-uri directive or a Reporting-Endpoints header, and the browser does the rest. It is documented here because it is worth knowing what the browser is sending and what a given status code means when you are checking whether reporting works.

Everything below is derived from the OpenAPI contract the endpoint is built from, and a test fails if the two drift apart.

POST https://{ingestCode}.ingest.us.headerhawk.com/r

Each site has a host of its own: its ingest code is the first part of the host, and the path is always /r. The us in the host names the region that receives and stores your reports.

ingestCode is the 12-character identifier, lowercase letters and digits, assigned when you create a site. Copy the full URL from the site’s setup panel rather than assembling it; the guides in these docs write it as https://your-ingest-code.ingest.us.headerhawk.com/r.

On the Professional plan and above, a site can also report to a name you choose, from the Report host name field in its setup panel:

POST https://{name}.ingest.us.headerhawk.com/r

It reaches the same site as the generated host, which keeps working beside it, and the setup panel writes its header block with the name once you have one. A name is 3 to 40 lowercase letters, digits and hyphens, starts with a letter, ends with a letter or digit, and has no two hyphens in a row; twelve letters and digits in a row is the shape of a generated code, so a name of that shape needs a hyphen. It only ever goes in the host, never in the path.

Once your organisation has chosen a name, it stays yours, even after you release it: headers that still name it keep sending reports, so it is never given to anyone else, and you can choose it again for any of your sites. For the same reason your organisation can hold at most 25 names, released ones included; choosing one it already holds again never counts twice. Releasing or changing a name stops pages whose headers name it from reporting within a minute, so change the headers first. Rotating the ingest code leaves the name alone. A name keeps working after a downgrade; choosing or changing one needs the plan, releasing one does not.

On the Business plan and above, a site can also receive its reports on a hostname of your own, such as csp.example.com, so your headers name only your domain:

POST https://csp.example.com/r

Add it under Report hosts on your domain in the site’s setup panel, then create the one DNS record the panel shows:

csp.example.com CNAME hosts.ingest.us.headerhawk.com

HeaderHawk requests a certificate for the hostname once the record resolves, and the host starts receiving reports, usually within half an hour. The panel shows where each host is. Once a host is active, the setup panel writes its header block with it, and the generated host and any name you chose keep working beside it.

A few rules decide which hostnames are accepted:

  • It must be under a domain your organisation has verified, in Settings → Authentication. Whoever holds a hostname receives its reports, so a hostname is accepted only under a domain you have proved you own.
  • It must be a subdomain. A domain’s apex, such as example.com, cannot be a CNAME. A wildcard, an IP address and names under headerhawk.com are refused too.
  • A CAA record on the domain must allow amazon.com, or the certificate cannot be issued. This is the most common reason a host fails. Fix the record, then press Retry.
  • The certificate authority waits 72 hours for the record. A host whose record is not in place by then fails, and Retry starts again.
  • A site can have 2 report hosts, which is enough to move from one to another, and an organisation can have 20.

If the record is later removed or pointed elsewhere, reports stop at your DNS and the certificate cannot renew. HeaderHawk checks every active host daily, and emails your owners and admins when a host becomes active, fails, stops resolving or is suspended.

If your plan no longer includes report hosts, each active host keeps receiving reports for 30 days, with a reminder a week before the end, and is then suspended. Upgrading within the 30 days keeps it with nothing to redo. A host is suspended at once, in the same way, if the domain it is under stops being verified. Removing a host stops pages whose headers name it from reporting, so change the headers first. Rotating the ingest code leaves every report host working.

Sites created before the code moved into the host

Section titled “Sites created before the code moved into the host”

A site created before ingest codes moved into the host has a host of its own too, and also keeps the 8-character code it was first given, which goes in the path instead. Its setup panel names that earlier URL under the endpoint:

POST https://ingest.us.headerhawk.com/r/{ingestCode}

That URL keeps working for those codes, so headers already using it need no change. It accepts only an 8-character code: a 12-character code in the path is answered 400 with a message naming the host it belongs on. Rotating the code from the setup panel gives the site a 12-character code and a host of its own, and the old path URL stops accepting reports.

While a site runs policy discovery, its header block carries a second report-only policy, one that allows nothing, so that browsers report every script, style, image, font, frame and connection the site’s pages load. Those reports go to a path of their own on the same host:

POST https://{ingestCode}.ingest.us.headerhawk.com/r/discovery

A chosen name’s host and a report host on your own domain take /r/discovery the same way; the earlier path form has no discovery path. Discovery reports are kept apart from the site’s ordinary ones, and never become issues or alerts. Each browser’s first report of a source in a run is always kept and its repeats are sampled, 5% by default; each kept report counts against the monthly allowance. While the site is not running discovery, the path answers 204 and records nothing.

The endpoint detects the format from the body, so a browser that sends a CSP report as application/json is handled the same as one that uses the registered type.

Format Content-Type Sent by
CSP Level 2 application/csp-report report-uri directive
Reporting API application/reports+json report-to directive
Script hashes application/reports+json report-to, with 'report-sha256' in script-src
Other browser reports application/reports+json The header each type names in Browser Report Types

A single object under a csp-report key. document-uri, violated-directive and original-policy are required; the rest are optional and browser-dependent.

{
"csp-report": {
"document-uri": "https://example.com/page",
"referrer": "https://google.com/",
"violated-directive": "script-src",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self'; report-uri https://your-ingest-code.ingest.us.headerhawk.com/r",
"disposition": "report",
"blocked-uri": "https://evil.com/script.js",
"status-code": 200
}
}

An array of report envelopes. body and type are required, and type is csp-violation for a policy violation. Inside body, documentURL, effectiveDirective and originalPolicy are required.

[
{
"age": 10,
"type": "csp-violation",
"url": "https://example.com/page",
"user_agent": "Mozilla/5.0...",
"body": {
"documentURL": "https://example.com/page",
"blockedURL": "https://evil.com/script.js",
"disposition": "enforce",
"effectiveDirective": "script-src",
"originalPolicy": "default-src 'self'; script-src 'self'"
}
}
]

Browsers batch every report a page produces into one array, whatever its type. Mixed arrays are split and each type routed independently, so a request carrying both csp-violation and csp-hash reports is handled as two pipelines, not rejected. See Script Integrity Monitoring for what the csp-hash type collects.

Besides csp-violation and csp-hash, the endpoint stores these Reporting API types, each answered 204:

type Shown on
integrity-violation Issues and the Reports Explorer
coep Issues and the Reports Explorer
coop Issues and the Reports Explorer
permissions-policy-violation Issues and the Reports Explorer
potential-permissions-policy-violation Issues and the Reports Explorer
document-policy-violation Issues and the Reports Explorer
connection-allowlist Issues and the Reports Explorer
deprecation Browser changes
intervention Browser changes
network-error Reliability
crash Reliability

Each is kept only while the site has it switched on, and at the site’s sample rate for it. A report of a type switched off, or drawn out by the sample, is discarded before it counts towards your quota. A network-error report of a request that succeeded is discarded the same way: the setup asks for failures only.

A report of any type not listed here is accepted and discarded, and does not count towards your quota either. The answer is still a 204, because a browser treats an error as a failed delivery and retries it.

Status Meaning
204 Violation or other browser reports, or only unstored types, accepted. No body. Also the answer past the monthly hard limit.
202 Script-hash reports accepted. Body carries accepted and correlationId.
400 invalid_path or invalid_format — see below.
404 not_found — no site is accepting reports for this code.
429 rate_limited. Carries Retry-After in whole seconds.
503 service_unavailable — a fault on our side; the report is not recorded.

Every response carries an X-Correlation-ID header. Quote it when asking about a specific request; it is what traces that request through the pipeline.

Error bodies are uniform:

{
"error": "invalid_format",
"message": "Unknown report format"
}

A 204 is returned when the reports were accepted and when every report in the request was discarded because its document-uri did not match the site’s registered domain. The two are deliberately indistinguishable, so the endpoint cannot be used to enumerate the domains a tenant monitors. It is also the answer to a request that carried only report types HeaderHawk does not store.

This is the single most common reason reporting looks broken: a site registered as example.com discards reports from www.example.com, and the browser sees success either way. The site’s setup panel counts anything dropped for this reason — check it there, not in the response.

A 204 is also the answer once your tenant is past its monthly hard limit, when the reports are not stored. A 2xx is the only status a browser does not retry: Chrome sends a report again after any other answer, 410 and 429 included, so refusing with one would multiply the traffic and deliver nothing. Your usage against the limit is on Settings → Billing in the dashboard.

404 does not distinguish unknown from deactivated

Section titled “404 does not distinguish unknown from deactivated”

An unrecognised code and a code belonging to a deactivated site both return not_found, for the same reason: distinct statuses would let anyone spraying the keyspace confirm which codes belong to real customers. POST /r on ingest.us.headerhawk.com itself, with no code in the host, gets the same answer. An owner who deactivated their own site already knows they did.

202 does not mean “every hash was recorded”

Section titled “202 does not mean “every hash was recorded””

accepted counts the reports published for processing, and may be 0 because sampling dropped the report, because the site has script hashes switched off, or because the documentURL did not match the registered domain. The response does not distinguish them.

  • 100 reports per request. A request carrying more is truncated, not rejected — a 2xx means “accepted”, not “all of it stored”, and a browser that cannot tell a 400 from a 204 would simply retry.
  • Per-source rate limiting, applied before the ingest code is looked up, so it bounds a caller probing for valid codes as well as one flooding a known one. Exceeding it returns 429 with error: "rate_limited".
  • Monthly tenant quota. Past the hard limit, reports are not stored and ingestion answers 204, as described above.

The endpoint accepts a hand-rolled report, which is the quickest way to confirm your ingest code reaches a live site. Use a document-uri on your registered domain, or it will be discarded and you will still get a 204.

Terminal window
cat > report.json <<'JSON'
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src",
"original-policy": "default-src 'self'; report-uri https://your-ingest-code.ingest.us.headerhawk.com/r",
"blocked-uri": "https://evil.com/script.js"
}
}
JSON
curl -i -X POST https://your-ingest-code.ingest.us.headerhawk.com/r \
-H "Content-Type: application/csp-report" \
--data-binary @report.json

A 204 with an X-Correlation-ID header means the code resolved to a site. The report appears in the dashboard once processing completes.

GET https://ingest.us.headerhawk.com/health

Returns 200 with {"status":"healthy","timestamp":"..."}. It touches no database and is there for uptime monitoring.