Error reporting
Your workspace has an Errors page: exceptions reported from your own application, from the Vroxy widget running on your pages, and from any hand-rolled client you point at it. It’s the same place your support conversations live, so “three people complained about checkout” and “here’s the exception that broke checkout” sit next to each other.
Most people never touch the endpoint directly — the server libraries report for you, and the widget reports its own errors automatically. This page is what’s underneath.
The endpoint
Section titled “The endpoint”POST https://vroxy.ai/ingest/errorsContent-Type: application/jsonX-Vroxy-Tenant: <your workspace public key>curl -X POST https://vroxy.ai/ingest/errors \ -H 'Content-Type: application/json' \ -H 'X-Vroxy-Tenant: YOUR_PUBLIC_KEY' \ -d '{ "source": "api", "error_class": "Stripe::CardError", "message": "Your card was declined.", "backtrace": ["app/services/charge.rb:41:in `call'"], "request_path": "/checkout", "request_method": "POST", "environment": "production", "app_version": "2026.08.30", "context": { "order_id": "1234" } }'A successful call answers 202 Accepted:
{ "accepted": 1, "ids": ["a1b2c3d4"] }The endpoint accepts cross-origin requests, because browsers report to it directly.
Authentication is your public key, not a secret
Section titled “Authentication is your public key, not a secret”The X-Vroxy-Tenant header carries the same public key that’s in
your embed snippet — the one visible in the page source of every site
running the widget. Not a bearer token.
That’s deliberate. Error reporting has to work from browsers, and a browser cannot hold a secret: anything shipped to the page to authenticate a report is readable by anyone who views source. A secret in that position would be security theater, and would also mean apps that never configured a server-side token couldn’t report at all.
So the endpoint is treated as untrusted input, and the protections are elsewhere:
- Per-workspace rate limiting — see below.
- Hard caps on every field, so no single report can be large.
- Retention pruning, so a flood can’t grow without bound.
- Nothing in the payload is trusted. Every value is coerced and truncated at write time; an invalid report is dropped rather than raising.
The practical consequence to be aware of: someone who has your public key — which is to say, anyone who has visited your site — can post error reports into your workspace. They can’t read anything, delete anything, or reach any other part of your workspace. Treat the Errors page as a signal, not as evidence.
Batching
Section titled “Batching”Send one report as a plain object, or several under an errors key:
{ "source": "node", "errors": [ { "error_class": "TypeError", "message": "x is not a function" }, { "error_class": "RangeError", "message": "Invalid array length" } ]}At most 10 reports per request are accepted; anything beyond that
is ignored. A top-level source applies to every report in the batch,
and any report can override it with its own source.
Fields
Section titled “Fields”| Field | Notes |
|---|---|
error_class |
Truncated to 200 characters. Blank becomes UnknownError. |
message |
Truncated to 1,000 characters. |
backtrace |
Array of strings, or a newline-separated string. First 30 lines, each capped at 400 characters, 8,000 characters total. |
url |
600 characters. |
request_method |
10 characters. |
request_path |
500 characters. |
environment |
40 characters — e.g. production, staging, browser. |
app_version |
40 characters. Useful for “did the deploy cause this”. |
context |
Object, at most 30 keys. Keys capped at 60 characters; scalar values at 500. Nested objects and arrays are kept as given. |
occurred_at |
Any parseable timestamp. Unparseable, or more than an hour in the future, falls back to now. |
source |
One of ruby, js, api, mobile, node, python, php. Anything else is recorded as api. |
An optional X-Vroxy-Visitor header carrying a visitor token
associates the report with that visitor, so you can jump from an error
to the person who hit it. The widget sends it automatically.
Responses
Section titled “Responses”| Status | Meaning |
|---|---|
202 Accepted |
Stored. Body has accepted (how many were saved) and ids. |
404 Not Found |
The public key doesn’t match a workspace. |
429 Too Many Requests |
Over the rate limit. A Retry-After: 60 header is set. |
503 Service Unavailable |
Error ingestion is turned off on the deployment. |
Note that accepted can be lower than the number you sent: a report
that fails validation is dropped silently rather than failing the whole
batch.
Limits and retention
Section titled “Limits and retention”- 120 reports per minute, per workspace. Past that you get a
429and aRetry-After: 60. This is a workspace-wide budget shared by your server, your browsers and every other source. - 20,000 stored occurrences per workspace. Once you’re over, oldest occurrences are pruned. Pruning is opportunistic — it runs on a fraction of incoming reports, not on every one — so the count hovers around the limit rather than sitting exactly on it.
- The SDKs throttle on their side too, at 60 reports per minute per process, so a crash loop in one app server doesn’t spend your whole workspace budget.
If a noisy exception is eating your quota, the fix is the SDK’s ignore
list — add the class name to error_ignore / errorIgnore and it’s
never sent.
Grouping by fingerprint
Section titled “Grouping by fingerprint”Occurrences are grouped so a thousand hits of the same bug are one row, not a thousand. The fingerprint is computed once, when the report is written, from:
- the error class, plus
- the top frame of the backtrace — same error, same place, same group.
When there’s no backtrace at all, which is common for JavaScript errors, the first 120 characters of the message are used instead.
Message text is deliberately not part of the fingerprint when a backtrace exists. Messages carry variable data — “order 4471 not found”, “order 4472 not found” — and including them would shatter one bug into thousands of groups.
Two consequences worth knowing:
- Two genuinely different bugs raising the same class from the same line will share a group.
- Changing the code moves the line numbers, so the same bug can start a new group after a deploy.
Reading them in your workspace
Section titled “Reading them in your workspace”The Errors page is at /w/<workspace-id>/<slug>/errors and needs the
errors.read capability — member and above have it by default.
The list shows one row per group: the error class, the latest message, the source, how many occurrences there are, and when it was first and last seen. It’s ordered by most recently seen, 25 groups per page, and you can filter to a single source. The header shows the total number of occurrences currently retained.
Opening a group shows its most recent occurrences (up to 100) with their full detail — backtrace, URL, request path, environment, app version, your context, and the visitor when one is known.
What the widget reports on its own
Section titled “What the widget reports on its own”The widget captures only its own errors. It checks that the failing script is the Vroxy widget bundle before reporting; unrelated JavaScript on your page is deliberately not collected. Your site’s errors are your site’s business, and silently hoovering them up would be both a surprise and a privacy problem.
To report your own errors from a page, call the API explicitly:
vroxy("reportError", err, { where: "checkout" });Either way, the widget sends at most 5 reports per page load, so a
render loop can’t turn one broken page into a flood. Reports carry
source: "js", environment: "browser", the page URL, the error’s
name, message and stack, and whatever context object you passed.
Reporting is fire-and-forget and swallows its own failures — a broken report never affects the page.