All posts

How to set up the browser Reporting API

CentralCSP Team ·

Last update:

The browser already knows when your security policies fire, when network requests fail, and when a page uses an API that is about to be removed. Most of the time that knowledge stays in the user's browser and you never see it. The Reporting API is how you get it out: a small amount of configuration tells the browser to package those events as JSON and send them to a server you control. This walkthrough takes you from nothing to a working endpoint that receives real reports, the modern way, with Reporting-Endpoints first and the deprecated Report-To only where you still need it.

The short version: declare an endpoint with one header, name it from a policy, and the browser batches and delivers JSON reports to your URL out of band. The rest of this guide is the detail behind those three steps, and how to read what arrives.

What the Reporting API actually does

It helps to separate two things. A policy or platform feature is the producer: Content Security Policy (CSP), Cross-Origin-Opener-Policy (COOP), Network Error Logging, a deprecation warning, a crash. The Reporting API is the transport: a shared queue in the browser that collects those reports and delivers them. The producer decides what is worth reporting; the API decides how it travels.

When a producer flags an event, the browser does not send it immediately. It collects the report, batches it with others, and POSTs the batch to your endpoint on its own schedule, independent of the page. That decoupling is the point: a report can still arrive after the page has navigated away or even crashed, because delivery does not depend on the page being alive.

One caveat to set expectations: the spec calls delivery best-effort, not a guaranteed channel. Reports can be dropped, deduplicated, or delayed, so treat the stream as a high-value signal, not an audit log you can prove is complete. For the full model, see how the Reporting API works.

Step 1, declare a reporting endpoint

Everything starts with one response header. Reporting-Endpoints is a list of named endpoints, each a name mapped to an HTTPS URL.

Reporting-Endpoints: main-endpoint="https://<Endpoint-ID>.report.centralcsp.com"

Two rules matter here. The URL must be HTTPS, because the browser silently ignores a non-secure endpoint, so a typo or an http:// URL means reports vanish with no error. And the header only affects the response it is served on, so you must send it on every page that should be able to report, not just the home page.

You can declare more than one endpoint and route different policies to different URLs, and you can declare a special endpoint named default that catches report types which have nowhere else to go (deprecations and interventions, crashes).

Reporting-Endpoints: default="https://<Endpoint-ID>.report.centralcsp.com",
                     csp-endpoint="https://<Endpoint-ID>.report.centralcsp.com"

Full syntax is on the Reporting-Endpoints reference.

Step 2, point a policy at the endpoint

Declaring an endpoint does nothing on its own. A policy has to reference it by name. For CSP, that is the report-to directive. Start in Report-Only so a mistake in the policy reports instead of blocking, and cannot break the page while you tune it.

Reporting-Endpoints: main-endpoint="https://<Endpoint-ID>.report.centralcsp.com"
Content-Security-Policy-Report-Only: default-src 'self'; report-to main-endpoint

Other policies attach to an endpoint the same way, with small syntax differences: COOP and COEP use a report-to="..." parameter on their header, Integrity-Policy uses an endpoints=(...) directive, and the implicit report types (deprecation, intervention, crash) go to the endpoint named default with no wiring at all.

Step 3, read what arrives

When the browser delivers, it sends an HTTP POST with Content-Type: application/reports+json and a JSON array. Every entry shares the same envelope, the fields type, url, user_agent, age, and body, and only the body changes from one report type to the next.

[
  {
    "type": "csp-violation",
    "age": 53,
    "url": "https://api-next.centralcsp.com/",
    "user_agent": "Mozilla/5.0 ...",
    "body": {
      "documentURL": "https://api-next.centralcsp.com/",
      "blockedURL": "https://evil.example/script.js",
      "effectiveDirective": "script-src-elem",
      "disposition": "report",
      "statusCode": 200
    }
  }
]

Do not be surprised if the first report takes a minute to show up. Chromium batches deliveries and delays them to save battery and bandwidth, so reporting is not instant. The envelope, and how it differs from the legacy single-object application/csp-report format that report-uri uses, is covered in the report delivery format. If you would rather confirm a live site is wired correctly before you build a receiver, the Reporting API configuration checker does that for you.

A generated header block declaring the endpoint and pointing every policy at it

Where the reports should go

For the full picture of where browser reports go and how to receive them, the trade-offs are below. You can stand up your own collector, and for a single policy on a low-traffic site that is fine. The honest trade-off shows up at scale: real traffic produces a lot of reports, much of it duplicate noise, and the value is not in storing them but in grouping, de-duplicating, and alerting on the ones that matter. That is a backend to build and maintain.

CentralCSP is that backend as a service. Point the Reporting-Endpoints URL at it and it collects every report type, groups them, maps CSP violations and script hashes to real causes, and turns the stream into alerts and exportable evidence. You skip the collector and start with the part that is actually useful.

Next steps

Ready to collect from real traffic instead of a throwaway endpoint? Start a free trial.

Sources