Reporting-Endpoints, Report-To and CSP report-to vs report-uri (tested in Chromium)
CSP can send violation reports two ways: the old report-uri directive, or report-to plus a Reporting-Endpoints header. They use different payloads, different rules and different headers. We set up both in Chromium 153 and logged what actually reached the server.
1. The headers
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy: script-src 'self'; report-to csp
Reporting-Endpoints maps a name (csp) to a URL. The CSP report-to directive names the endpoint to use. The older way puts the URL right in the policy:
Content-Security-Policy: script-src 'self'; report-uri https://example.com/csp-reports
2. What arrived, measured
| Setup | Result in Chromium 153 |
|---|---|
report-uri only | POST in about 0.2 s, Content-Type: application/csp-report |
Reporting-Endpoints + report-to, HTTPS endpoint | POST in about 0.2 s, Content-Type: application/reports+json (with and without --short-reporting-delay in our headless run) |
Both report-uri and report-to in one policy | Only the report-to endpoint got a report. The report-uri URL got nothing (waited 30 s). |
| Endpoint on another origin | OPTIONS preflight first (Access-Control-Request-Headers: content-type), then the POST |
report-to with an http://127.0.0.1 endpoint | No report in 60 s |
Old Report-To JSON header (no Reporting-Endpoints) + report-to | No report in 30 s |
3. The two payloads
report-uri sends one object per violation, with hyphenated keys:
Content-Type: application/csp-report
{"csp-report": {"document-uri": "https://localhost:8947/", "referrer": "",
"violated-directive": "script-src-elem", "effective-directive": "script-src-elem",
"original-policy": "script-src 'none'; report-uri https://localhost:8947/r",
"disposition": "enforce", "blocked-uri": "inline", "line-number": 1,
"source-file": "https://localhost:8947/", "status-code": 200, "script-sample": ""}}
report-to sends a JSON array (reports can be batched), with camelCase keys inside body:
Content-Type: application/reports+json
[{"age": 0, "type": "csp-violation", "url": "https://localhost:8946/",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) ... HeadlessChrome/153.0.0.0 ...",
"body": {"blockedURL": "inline", "disposition": "enforce",
"documentURL": "https://localhost:8946/", "effectiveDirective": "script-src-elem",
"lineNumber": 1, "originalPolicy": "script-src 'none'; report-to csp",
"referrer": "", "sample": "", "sourceFile": "https://localhost:8946/", "statusCode": 200}}]
So a collector written for report-uri needs changes: accept the new content type, loop over the array, check type, and read the camelCase fields. Note there is no violatedDirective; use effectiveDirective.
4. Rules that bit us
- The endpoint must be HTTPS. An
http://127.0.0.1endpoint got nothing, even thoughreport-urihappily posts to plain http. In an earlier run with an http page and http endpoint, reports were queued (visible over the DevTools protocol) but never sent in 80 s. - Cross-origin endpoints get a preflight.
application/reports+jsonisn't a "simple" content type, so the browser sendsOPTIONSfirst. Answer it withAccess-Control-Allow-Origin,Access-Control-Allow-Methods: POSTandAccess-Control-Allow-Headers: content-type, or the reports are lost.report-urihad no preflight. - Both directives: report-to wins. When the browser understands
report-toit ignoresreport-uri, which matches the CSP spec. Keepreport-uriin the policy as a fallback for browsers without the Reporting API, but don't expect two copies in Chrome. - Report-To is not Reporting-Endpoints. The older
Report-To: {"group":"csp","max_age":...,"endpoints":[...]}header alone produced no CSP report in Chromium 153. UseReporting-Endpoints. - Reports may be delayed. The Reporting API batches; our headless runs delivered in under a second, but a real browser may wait about a minute. Don't conclude it's broken after 5 seconds.
5. A working setup
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy: script-src 'self'; report-uri https://example.com/csp-reports; report-to csp
Make the endpoint accept both application/csp-report and application/reports+json, return 204, and handle OPTIONS if it is on another origin. To test a policy without blocking anything, send it as Content-Security-Policy-Report-Only; for a starting policy see CSP header examples. Related: Sec-Fetch-* headers.
Frequently asked questions
What is the difference between CSP report-to and report-uri?
report-uri puts a URL in the policy and POSTs one application/csp-report object per violation. report-to names an endpoint from the Reporting-Endpoints header and POSTs an application/reports+json array of reports with type csp-violation. In Chromium 153 both arrived in well under a second in our headless test.
Should I use both report-uri and report-to?
Yes, as a fallback, but in Chromium 153 only the report-to endpoint received the report; report-uri was ignored when report-to was present.
Why are my report-to reports not arriving?
In our test an http:// endpoint got nothing; it must be HTTPS. A cross-origin endpoint also has to answer the CORS preflight. Real browsers can batch reports for about a minute.
What is the difference between Report-To and Reporting-Endpoints?
Report-To is the older JSON header with group and max_age; Reporting-Endpoints is the current name="url" header. With only Report-To, Chromium 153 sent no CSP report in our test.
What Content-Type do CSP reports use?
application/csp-report for report-uri and application/reports+json for report-to.
Written with AI assistance. Measured on 2026-10-01 in headless Chromium 153.0.8010.12 (Playwright) against Node.js servers on localhost with a self-signed certificate (HTTPS page, HTTPS endpoint, certificate errors ignored for the test). The page had one inline script blocked by script-src 'none'; the server logged every request to the report endpoint. Delivery timing was measured with and without Chromium's --short-reporting-delay switch. A normal desktop browser may batch reports for up to about a minute, so don't expect the instant delivery we saw. Other browsers were not tested. PlainHeaders is our extension; DevTools → Network → Headers or curl -I shows the same response headers, free.