Request counts across days, for overlaying
const url = 'https://api.nsin.cloud/analytics/requests-compare?granularity=hour&scope=domains';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url 'https://api.nsin.cloud/analytics/requests-compare?granularity=hour&scope=domains' \ --header 'Authorization: Bearer <token>'Request counts bucketed by hour or by day over a window of whole days, so the dashboard can draw one line per day and compare them.
Scope, in order:
domain_idgiven - that domain alone, and 404 if the caller may not see it.scope=all- every domain. Admins only; anyone else falls through.- otherwise - every ACTIVE domain the caller owns or is a member of.
The scoping parameter is domain_id, in snake_case. It is the odd one
out on this surface, and getting it wrong is silent: a camelCase
domainId is never read, so the handler sees no id, falls through to
the third branch, and answers for every domain you can see instead of
the one you asked for. Nothing in the response says the scope was
widened.
domain_id selects that domain whatever its status; the fall-through
counts ACTIVE domains only. Naming a pending or disabled domain by
id therefore returns traffic the account-wide query leaves out.
Buckets are anchored to local midnight, so today’s partial day is still a whole-day bucket and lines stay aligned.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Query Parameters
Section titled “Query Parameters ”hour overlays one line per day; day gives a single daily series.
Only day is recognised as itself: every other value, misspelling
included, is silently treated as hour rather than rejected.
Window length in whole days. Both the default and the cap depend on
granularity: hour defaults to 3 and is capped at 14, day
defaults to 14 and is capped at 35 - the ClickHouse rows expire at
35 days, so nothing older exists to chart. The schema’s maximum is
the higher of the two caps because one schema cannot express the
split; over the cap for the granularity you asked for, the value is
clamped, not rejected, and 0, a negative number or a non-numeric
value takes the default. Either way the window can come back shorter
than you asked for with no error.
all is honoured for admins only; for anyone else it is ignored and
the query stays scoped to their own domains. It is also ignored
whenever domain_id is set - that branch wins outright.
Numeric domain id. snake_case, not domainId - see the scoping note
above. 404 if the caller may not see the domain.
Responses
Section titled “ Responses ”Time series. This operation formats timestamp as
2026-08-16 13:00:00 - a space separator and no zone suffix - not
the RFC 3339 2026-08-16T13:00:00Z the shared schema’s
format: date-time implies and the other series endpoints return.
Parse it as a local-time stamp, not as RFC 3339.
object
object
Example generated
{ "data": [ { "timestamp": "2026-04-15T12:00:00Z", "count": 1 } ]}Missing, malformed, revoked or expired API key, or a key whose owning
user row is gone. A key whose owning account has merely been deactivated
is not this: that is 403 with code: account_suspended, because the
credential itself is intact and re-issuing it changes nothing.
The error shape used by every endpoint. error is always present. code
is present only on the failures that have one — do not require it, and do
not parse error to recover it.
object
Human-readable description of what went wrong.
Stable machine-readable reason. Present on some failures only; the
wording of error may change, this will not.
account_suspended—403. The account behind the credential has been switched off, by an admin or by its provider. Every authenticated route answers this, so treat it as terminal rather than retrying.domain_disabled—409, not403. You have every right to the operation; the domain is simply switched off and is not being served, so its configuration cannot change. It stays readable, and writes work again once it is enabled.managed_by_reseller—403. The account is a reseller’s client and this surface belongs to its provider. See If your account is managed by a reseller.invite_email_mismatch—403fromPOST /invites/{token}/accept. The invitation was addressed to a different email; the body also carriesinvited_email, masked.
A panel session — not an API key — can additionally see
session_check_failed on a 503, which means the session could not
be verified, not that it is invalid. Retry it; do not discard the
token.
Examples
{ "error": "invalid API key"}No such domain, or it is not visible to this account. Domains you cannot access are reported as not found rather than forbidden.
The error shape used by every endpoint. error is always present. code
is present only on the failures that have one — do not require it, and do
not parse error to recover it.
object
Human-readable description of what went wrong.
Stable machine-readable reason. Present on some failures only; the
wording of error may change, this will not.
account_suspended—403. The account behind the credential has been switched off, by an admin or by its provider. Every authenticated route answers this, so treat it as terminal rather than retrying.domain_disabled—409, not403. You have every right to the operation; the domain is simply switched off and is not being served, so its configuration cannot change. It stays readable, and writes work again once it is enabled.managed_by_reseller—403. The account is a reseller’s client and this surface belongs to its provider. See If your account is managed by a reseller.invite_email_mismatch—403fromPOST /invites/{token}/accept. The invitation was addressed to a different email; the body also carriesinvited_email, masked.
A panel session — not an API key — can additionally see
session_check_failed on a 503, which means the session could not
be verified, not that it is invalid. Retry it; do not discard the
token.
Example
{ "error": "read-only API key"}The key exceeded its request budget (300 requests per minute by default).
The error shape used by every endpoint. error is always present. code
is present only on the failures that have one — do not require it, and do
not parse error to recover it.
object
Human-readable description of what went wrong.
Stable machine-readable reason. Present on some failures only; the
wording of error may change, this will not.
account_suspended—403. The account behind the credential has been switched off, by an admin or by its provider. Every authenticated route answers this, so treat it as terminal rather than retrying.domain_disabled—409, not403. You have every right to the operation; the domain is simply switched off and is not being served, so its configuration cannot change. It stays readable, and writes work again once it is enabled.managed_by_reseller—403. The account is a reseller’s client and this surface belongs to its provider. See If your account is managed by a reseller.invite_email_mismatch—403fromPOST /invites/{token}/accept. The invitation was addressed to a different email; the body also carriesinvited_email, masked.
A panel session — not an API key — can additionally see
session_check_failed on a 503, which means the session could not
be verified, not that it is invalid. Retry it; do not discard the
token.
Examples
{ "error": "rate limit exceeded"}The process holds no ClickHouse client at all - the connection was never opened at startup. It is built once and never rebuilt, so this state lasts until the backend restarts.
It is NOT what you get when the store is merely down, and that is the
trap. Startup opens the client and pings it as a separate step; a failed
ping is logged and the client is kept, so a ClickHouse that was
unreachable at boot still leaves a usable handle behind and this 503
stays rare. Every failure at query time answers 500 instead - the
store unreachable, or a query outrunning its timeout of 10s for windows
up to 24h, 20s for 7d, 45s for 30d - carrying either
{"error": "query failed"} or a “Could not load analytics right now”
message.
So retry on 500 exactly as you retry on 503. On the analytics endpoints a 500 nearly always means the store could not answer, not that your request was wrong.
The error shape used by every endpoint. error is always present. code
is present only on the failures that have one — do not require it, and do
not parse error to recover it.
object
Human-readable description of what went wrong.
Stable machine-readable reason. Present on some failures only; the
wording of error may change, this will not.
account_suspended—403. The account behind the credential has been switched off, by an admin or by its provider. Every authenticated route answers this, so treat it as terminal rather than retrying.domain_disabled—409, not403. You have every right to the operation; the domain is simply switched off and is not being served, so its configuration cannot change. It stays readable, and writes work again once it is enabled.managed_by_reseller—403. The account is a reseller’s client and this surface belongs to its provider. See If your account is managed by a reseller.invite_email_mismatch—403fromPOST /invites/{token}/accept. The invitation was addressed to a different email; the body also carriesinvited_email, masked.
A panel session — not an API key — can additionally see
session_check_failed on a 503, which means the session could not
be verified, not that it is invalid. Retry it; do not discard the
token.
Examples
{ "error": "analytics unavailable"}