Skip to content

Request counts across days, for overlaying

GET
/analytics/requests-compare
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_id given - 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.

granularity
string
default: hour
Allowed values: hour day

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.

days
integer
>= 1 <= 35

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.

scope
string
default: domains
Allowed values: domains all

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.

domain_id
integer

Numeric domain id. snake_case, not domainId - see the scoping note above. 404 if the caller may not see the domain.

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.

Media type application/json
object
data
Array<object>
object
timestamp
string format: date-time
count
integer
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.

Media type application/json

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
error
required

Human-readable description of what went wrong.

string
code

Stable machine-readable reason. Present on some failures only; the wording of error may change, this will not.

  • account_suspended403. 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_disabled409, not 403. 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_reseller403. 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_mismatch403 from POST /invites/{token}/accept. The invitation was addressed to a different email; the body also carries invited_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.

string
Examples
Example invalidKey
{
"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.

Media type application/json

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
error
required

Human-readable description of what went wrong.

string
code

Stable machine-readable reason. Present on some failures only; the wording of error may change, this will not.

  • account_suspended403. 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_disabled409, not 403. 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_reseller403. 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_mismatch403 from POST /invites/{token}/accept. The invitation was addressed to a different email; the body also carries invited_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.

string
Example
{
"error": "read-only API key"
}

The key exceeded its request budget (300 requests per minute by default).

Media type application/json

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
error
required

Human-readable description of what went wrong.

string
code

Stable machine-readable reason. Present on some failures only; the wording of error may change, this will not.

  • account_suspended403. 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_disabled409, not 403. 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_reseller403. 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_mismatch403 from POST /invites/{token}/accept. The invitation was addressed to a different email; the body also carries invited_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.

string
Examples
Example limited
{
"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.

Media type application/json

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
error
required

Human-readable description of what went wrong.

string
code

Stable machine-readable reason. Present on some failures only; the wording of error may change, this will not.

  • account_suspended403. 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_disabled409, not 403. 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_reseller403. 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_mismatch403 from POST /invites/{token}/accept. The invitation was addressed to a different email; the body also carries invited_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.

string
Examples
Example unavailable
{
"error": "analytics unavailable"
}