Centinel AnalyticaCentinel Analytica

Analytics Query API

Query your raw bot detection events with read-only SQL over HTTP.

Overview

The Analytics Query API gives you direct SQL access to your event data. Centinel writes one row for each request that it evaluates. You query those rows with ClickHouse SQL. The API returns the result in the format that you select.

Use the API to build a custom report. You can also load the data into a warehouse, or answer a question that the dashboard does not show.

The API is read-only. You can run only a SELECT statement.

Your key reads only the events of your own organization. The server applies this filter. You cannot disable it.

Authentication

Each request must have an API key in an Authorization header. Use the Bearer scheme:

Authorization: Bearer sk_live_your_key_here

Create a key in the dashboard. You can also revoke a key there. Treat a key as a password. The key gives read access to all of your event data. Keep the key on your server. Do not put the key in a client application.

If the key is missing, malformed, or unknown, the API returns 401 and an empty body. The API accepts no other authentication scheme.

Send a request

Send a POST request to the query endpoint. Put your SQL in the request body:

curl -X POST https://api.centinelanalytica.com/query \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: text/plain" \
  --data-binary "SELECT count() FROM events_distributed"

The request has four constraints:

  • The API accepts only the POST method.
  • The SQL must be in the request body. The API ignores query string parameters.
  • Each request must have one statement. The API rejects multiple statements.
  • The request body has a limit of 64 KB.

The endpoint sends no CORS headers. Therefore, a browser cannot call the API. Call the API from your server.

Write a query

Query the events_distributed table. Always name each column that you want:

SELECT created_at, hostname, decision
FROM events_distributed
WHERE created_at > now() - INTERVAL 1 DAY
LIMIT 100

The API rejects SELECT *. Your key has access to a specific set of columns. The wildcard includes columns outside that set.

Use single quotes for a string value. In ClickHouse, double quotes identify a column. Thus WHERE hostname = "example.com" reads a column with the name example.com. The query then fails and returns an unknown identifier error.

SELECT count() FROM events_distributed WHERE hostname = 'www.example.com'

The API has no bound query parameters. When you build SQL from user input, replace each single quote with two single quotes. If you do not do this, an attacker can inject SQL.

Put a created_at filter in each query. The created_at column is the sort key of the table. A bounded time range prevents the execution timeout.

Response formats

The API returns tab-separated values by default. Add a FORMAT clause to select a different format:

SELECT hostname, count() FROM events_distributed GROUP BY hostname FORMAT JSONEachRow

These formats are the most useful:

FormatUse for
TabSeparatedThe default format. It is compact and has no header row.
TSVWithNamesTab-separated values with a header row.
CSVWithNamesA spreadsheet import.
JSONOne object with the meta field, the data field, and row statistics.
JSONEachRowOne JSON object on each line. Use this format to load data into a different system.
PrettyCompactA table for a person to read.

The API does not compress the response.

Event schema

The table has one row for each request that Centinel evaluates. The retention period is 90 days. The headers column is an exception. A column TTL clears the value 14 days after the event, and the row keeps every other column for the full 90 days.

ColumnTypeDescription
created_atDateTime64(3, 'UTC')The time of the evaluation. The value has minute precision.
session_idUUIDThe visitor session. Use this column to group the requests of one visitor.
request_idUUIDUnique for each request. Use this column to count distinct requests.
ipIPv6The client IP address. An IPv4 address has a mapped form, for example ::ffff:203.0.113.10.
urlStringThe full request URL.
referrerStringThe referrer header. The value is empty if the header is absent.
methodStringThe HTTP method.
hostnameStringThe host that serves the request.
endpoint_pathStringThe path of the URL, without the query string.
headersStringThe request headers as a JSON object. The value is empty 14 days after the event.
user_agentStringThe raw User-Agent header.
decisionStringThe conclusion of the detection engine. Refer to Decisions and actions.
actionStringThe action that your policy applied to the request.
block_reason_categories_addedArray(String)The reasons that the engine flagged the request. The array is empty for clean traffic.
ip_country_codeStringThe two-letter country code from IP geolocation.
ip_region_codeStringThe region or state code, if known.
browserStringThe browser family. The value is empty if the engine cannot identify the browser.
osStringThe operating system.
device_typeStringOne of desktop, mobile, tablet, or other.
is_mobileBoolTrue if the client has the properties of a mobile device.
crawler_nameStringThe name of the crawler, for example Googlebot. The value is empty for other traffic.
crawler_typeStringThe crawler category. Refer to Crawlers for the full list.
is_datacenterBoolTrue if the IP address belongs to a hosting or datacenter network.
is_proxyBoolTrue if the IP address is a known proxy.
is_vpnBoolTrue if the IP address is a known consumer VPN exit.
is_torBoolTrue if the IP address is a Tor exit node.
asnUInt32The autonomous system number of the client network.
asn_organizationStringThe name of the network operator.

To read the current column list, query the system catalog:

SELECT name, type FROM system.columns
WHERE database = 'default' AND table = 'events_distributed'
ORDER BY position

Decisions and actions

The decision column and the action column answer two different questions. If you confuse them, your numbers will be incorrect.

ColumnValuesMeaning
decisionallow, block, redirectThe verdict of the engine.
actionallow, monitor, skip, redirectThe enforcement result.

The difference between the two columns is intentional. A rule in monitor mode writes decision = 'block' and action = 'monitor'. The engine flagged the request, and the visitor received the page.

To count the traffic that you stopped, filter on action:

SELECT count() FROM events_distributed
WHERE action = 'redirect'
  AND created_at > now() - INTERVAL 7 DAY

To count the traffic that the engine would stop when every rule enforces, filter on decision:

SELECT count() FROM events_distributed
WHERE decision = 'block'
  AND created_at > now() - INTERVAL 7 DAY

If one rule is in monitor mode, the count of decision = 'block' is more than the number of requests that you stopped. Do not report that count as blocked requests.

Example queries

Daily traffic by decision:

SELECT toDate(created_at) AS day, decision, count() AS requests
FROM events_distributed
WHERE created_at > now() - INTERVAL 30 DAY
GROUP BY day, decision
ORDER BY day DESC, requests DESC
FORMAT JSONEachRow

The crawlers that made the most requests in the last seven days:

SELECT crawler_name, crawler_type, count() AS requests
FROM events_distributed
WHERE created_at > now() - INTERVAL 7 DAY
  AND crawler_name != ''
GROUP BY crawler_name, crawler_type
ORDER BY requests DESC
LIMIT 25

The reasons that the engine flagged traffic:

SELECT arrayJoin(block_reason_categories_added) AS reason, count() AS hits
FROM events_distributed
WHERE created_at > now() - INTERVAL 7 DAY
GROUP BY reason
ORDER BY hits DESC

Datacenter traffic by network. This query is a good first check for a scraper:

SELECT asn_organization, count() AS requests, uniqExact(session_id) AS sessions
FROM events_distributed
WHERE created_at > now() - INTERVAL 7 DAY
  AND is_datacenter
GROUP BY asn_organization
ORDER BY requests DESC
LIMIT 25

The paths that flagged traffic requested most often:

SELECT endpoint_path, count() AS requests
FROM events_distributed
WHERE created_at > now() - INTERVAL 1 DAY
  AND decision = 'block'
GROUP BY endpoint_path
ORDER BY requests DESC
LIMIT 50

Limits

LimitValue
Query execution time30 seconds
Rows returned1,000,000
Rows scanned500,000,000
Memory for each query4 GB
Request body64 KB
Response body4 MB
Retention90 days
headers retention14 days

If a query exceeds a limit, the API cancels the query. The API returns an error, not a truncated result. If a query exceeds the execution time, decrease the time range. As an alternative, aggregate the data in the database.

The response body has a hard limit of 4 MB. A query that returns more rows than the limit fails, even if the row count is below one million. To export a large result, read one time window in each request:

SELECT created_at, session_id, decision
FROM events_distributed
WHERE created_at >= '2026-08-01 00:00:00'
  AND created_at <  '2026-08-01 00:15:00'
FORMAT JSONEachRow

Then move the window forward and send the next request. Do not use LIMIT with a large OFFSET. If a window returns the response size error, divide the window and try again. The correct window size depends on your traffic and on the number of columns that you select.

You cannot change the session settings. The API rejects a SET statement and a SETTINGS clause. This includes a change that decreases a limit.

Errors

The API returns an error as a plain-text body. A ClickHouse error has a code, a message, and an identifier:

Code: 62. DB::Exception: Syntax error (Multi-statements are not allowed): failed at position 9 (end of query): ; SELECT 2. . (SYNTAX_ERROR) (version 26.2.19.43 (official build))

The Code value and the identifier show the cause. These errors are the most frequent:

CodeIdentifierCause
62SYNTAX_ERRORThe SQL is malformed. The API also returns this error for multiple statements.
47UNKNOWN_IDENTIFIERThe column does not exist. A double-quoted string literal is a frequent cause.
60UNKNOWN_TABLEThe table does not exist. Query events_distributed.
497ACCESS_DENIEDThe query reads a table or column that your key cannot access. SELECT * and INSERT are frequent causes.
396TOO_MANY_ROWS_OR_BYTESThe result is more than one million rows.
164READONLYThe query tries to change a setting or to write data.

Two errors do not have the ClickHouse format. If the result is more than 4 MB, the body is clickhouse response too large. If the key is missing or unknown, the body is empty.

If a query runs longer than 30 seconds, the API cancels it. The error shows the elapsed time and the maximum.

See also

On this page