Run SQL Query
Runs a read-only ClickHouse SQL query over the raw events of the sites you can access in an organization.
POST /api/organizations/:organizationId/analytics/queryRuns one read-only ClickHouse SELECT against scoped_events: the raw events of every site in the organization that the caller can access, or of a single site when siteId is given. Results are capped at 1,000 rows and 60 seconds of execution.
Permissions: the viewer role or above in the organization. API keys need the sql:read scope. Members with restricted site access only see their sites; organization API keys see every site in the organization.
Rate limit: 60 requests per minute per user (per organization for organization API keys), on top of the standard API limits.
Path Parameters
Prop
Type
Request Body
Prop
Type
Query Rules
- Read from
scoped_eventsonly, or from CTEs you define over it.events,system.*,information_schema.*and other tables are rejected, andscoped_eventscan't be redefined. - One statement, starting with
SELECTorWITH. - These keywords are rejected anywhere outside string literals:
ALTER,ATTACH,BACKUP,CREATE,DELETE,DESCRIBE,DETACH,DROP,EXCHANGE,EXPLAIN,FORMAT,GRANT,INFILE,INSERT,INTO,KILL,OPTIMIZE,OUTFILE,RENAME,RESTORE,REVOKE,SET,SETTINGS,SHOW,SYSTEM,TRUNCATE,USE,WATCH. - Table functions and functions that reach outside the events (for example
url(),s3(),file(),remote(),numbers(),dictGet(),sleep()) are rejected. - Double-quoted or backtick identifiers,
#and//comments, nested/* */comments,$$strings, and non-ASCII characters outside string literals are rejected. Use single quotes for strings. - Your query is wrapped as
SELECT * FROM (your query) LIMIT 1000.
scoped_events Columns
| Column | Type | Description |
|---|---|---|
site_id | UInt16 | Rybbit site ID |
timestamp | DateTime | Event time (UTC) |
timestamp_ms | DateTime64(3) | Event time with milliseconds, for ordering |
session_id | String | Session ID |
user_id | String | Anonymous device ID |
identified_user_id | String | ID set via identify(); empty when not identified |
hostname | String | Page hostname, e.g. www.example.com |
pathname | String | URL path, e.g. /pricing |
querystring | String | Raw query string, usually with the leading ? |
url_parameters | Map(String, String) | Parsed query parameters, e.g. url_parameters['utm_source'] |
page_title | String | document.title; may be empty |
referrer | String | External referrer URL; empty for direct traffic |
channel | String | Acquisition channel, e.g. Direct, Organic Search, Referral |
browser, browser_version | LowCardinality(String) | Browser and major version |
operating_system, operating_system_version | LowCardinality(String) | OS and version |
language | LowCardinality(String) | Browser language, e.g. en-US |
country | LowCardinality(FixedString(2)) | ISO 3166-1 alpha-2 code |
region | LowCardinality(String) | Region code, e.g. US-CA |
city | String | City name |
lat, lon | Float64 | Coordinates; 0 when unknown |
screen_width, screen_height | UInt16 | Screen size in pixels; 0 when unavailable |
device_type | LowCardinality(String) | Desktop, Mobile, Tablet, TV, Console or Embedded |
type | LowCardinality(String) | pageview, custom_event, performance, outbound, error, button_click, copy, form_submit or input_change |
event_name | String | Custom event name, error name, or web-vitals |
props | JSON | Event properties; read with JSONExtractString(toString(props), 'key') |
lcp, cls, inp, fcp, ttfb | Nullable(Float64) | Web vitals; set only on performance events |
ip | Nullable(String) | Visitor IP when IP tracking is on for the site; otherwise null |
timezone | LowCardinality(String) | Visitor timezone from geolocation |
tag | LowCardinality(String) | Optional script tag; empty when unset |
import_id | Nullable(UUID) | Import job ID for imported rows; null for tracked events |
feature_flags | Map(String, String) | Feature flag values recorded with the event |
asn | Nullable(UInt32) | Autonomous system number of the visitor's IP |
asn_org | LowCardinality(String) | Organization that owns the ASN; empty when unknown |
is_datacenter_asn | UInt8 | 1 when the ASN belongs to a datacenter or hosting provider |
Response
Prop
Type
Error Responses
Errors use the standard body { error, code, message, resolution }.
Prop
Type
curl -X POST "https://app.rybbit.io/api/organizations/org_123/analytics/query" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"query": "SELECT pathname, count() AS pageviews FROM scoped_events WHERE type = '\''pageview'\'' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY pathname ORDER BY pageviews DESC LIMIT 10",
"siteId": 123
}'const response = await fetch(
'https://app.rybbit.io/api/organizations/org_123/analytics/query',
{
method: 'POST',
headers: {
'Authorization': 'Bearer your_api_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: 'SELECT pathname, count() AS pageviews FROM scoped_events WHERE type = \'pageview\' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY pathname ORDER BY pageviews DESC LIMIT 10',
siteId: 123
})
}
);
const data = await response.json();import requests
response = requests.post(
'https://app.rybbit.io/api/organizations/org_123/analytics/query',
json={
'query': 'SELECT pathname, count() AS pageviews FROM scoped_events WHERE type = \'pageview\' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY pathname ORDER BY pageviews DESC LIMIT 10',
'siteId': 123
},
headers={
'Authorization': 'Bearer your_api_key_here'
}
)
data = response.json()$body = [
'query' => 'SELECT pathname, count() AS pageviews FROM scoped_events WHERE type = \'pageview\' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY pathname ORDER BY pageviews DESC LIMIT 10',
'siteId' => 123
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.rybbit.io/api/organizations/org_123/analytics/query');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer your_api_key_here',
'Content-Type: application/json'
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);require 'net/http'
require 'json'
uri = URI('https://app.rybbit.io/api/organizations/org_123/analytics/query')
request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'Bearer your_api_key_here'
request['Content-Type'] = 'application/json'
request.body = {
query: 'SELECT pathname, count() AS pageviews FROM scoped_events WHERE type = \'pageview\' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY pathname ORDER BY pageviews DESC LIMIT 10',
siteId: 123
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
data = JSON.parse(response.body)body := map[string]interface{}{
"query": "SELECT pathname, count() AS pageviews FROM scoped_events WHERE type = 'pageview' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY pathname ORDER BY pageviews DESC LIMIT 10",
"siteId": 123,
}
jsonBody, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", "https://app.rybbit.io/api/organizations/org_123/analytics/query", bytes.NewBuffer(jsonBody))
req.Header.Set("Authorization", "Bearer your_api_key_here")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
var data map[string]interface{}
json.NewDecoder(resp.Body).Decode(&data)let body = serde_json::json!({
"query": "SELECT pathname, count() AS pageviews FROM scoped_events WHERE type = 'pageview' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY pathname ORDER BY pageviews DESC LIMIT 10",
"siteId": 123
});
let client = reqwest::Client::new();
let res = client
.post("https://app.rybbit.io/api/organizations/org_123/analytics/query")
.header("Authorization", "Bearer your_api_key_here")
.json(&body)
.send()
.await?;
let data: serde_json::Value = res.json().await?;String json = """
{
"query": "SELECT pathname, count() AS pageviews FROM scoped_events WHERE type = 'pageview' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY pathname ORDER BY pageviews DESC LIMIT 10",
"siteId": 123
}
""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.rybbit.io/api/organizations/org_123/analytics/query"))
.header("Authorization", "Bearer your_api_key_here")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());var body = new
{
query = "SELECT pathname, count() AS pageviews FROM scoped_events WHERE type = 'pageview' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY pathname ORDER BY pageviews DESC LIMIT 10",
siteId = 123
};
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer your_api_key_here");
var content = new StringContent(
JsonSerializer.Serialize(body),
Encoding.UTF8,
"application/json");
var response = await client.PostAsync("https://app.rybbit.io/api/organizations/org_123/analytics/query", content);
var data = await response.Content.ReadAsStringAsync();{
"data": [
{
"pathname": "/",
"pageviews": "4210"
},
{
"pathname": "/pricing",
"pageviews": "1388"
}
],
"meta": {
"queryId": "9f2c7a1e-3b4d-4e5f-8a6b-1c2d3e4f5a6b",
"rowCount": 2,
"maxExecutionTimeSeconds": 60,
"maxRows": 1000
}
}