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.
The endpoint
Section titled “The endpoint”POST https://{ingestCode}.ingest.us.headerhawk.com/rEach 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.
A report host name you choose
Section titled “A report host name you choose”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/rIt 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.
A report host on your own domain
Section titled “A report host on your own domain”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/rAdd 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.comHeaderHawk 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 underheaderhawk.comare 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.
Policy discovery reports
Section titled “Policy discovery 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/discoveryA 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.
Request formats
Section titled “Request formats”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 |
CSP Level 2
Section titled “CSP Level 2”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 }}Reporting API
Section titled “Reporting API”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.
The types it stores
Section titled “The types it stores”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.
Responses
Section titled “Responses”| 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"}204 does not mean “stored”
Section titled “204 does not mean “stored””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.
Limits
Section titled “Limits”- 100 reports per request. A request carrying more is truncated, not
rejected — a
2xxmeans “accepted”, not “all of it stored”, and a browser that cannot tell a400from a204would 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
429witherror: "rate_limited". - Monthly tenant quota. Past the hard limit, reports are not stored and
ingestion answers
204, as described above.
Checking it by hand
Section titled “Checking it by hand”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.
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.jsonA 204 with an X-Correlation-ID header means the code resolved to a site.
The report appears in the dashboard once processing completes.
Health check
Section titled “Health check”GET https://ingest.us.headerhawk.com/healthReturns 200 with {"status":"healthy","timestamp":"..."}. It touches no
database and is there for uptime monitoring.
Next steps
Section titled “Next steps”- Quick Start — get reporting working
- Troubleshooting — when reports are not arriving
- Script Integrity Monitoring — the
csp-hashpipeline