Custom SQL

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/query

Runs 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_events only, or from CTEs you define over it. events, system.*, information_schema.* and other tables are rejected, and scoped_events can't be redefined.
  • One statement, starting with SELECT or WITH.
  • 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

ColumnTypeDescription
site_idUInt16Rybbit site ID
timestampDateTimeEvent time (UTC)
timestamp_msDateTime64(3)Event time with milliseconds, for ordering
session_idStringSession ID
user_idStringAnonymous device ID
identified_user_idStringID set via identify(); empty when not identified
hostnameStringPage hostname, e.g. www.example.com
pathnameStringURL path, e.g. /pricing
querystringStringRaw query string, usually with the leading ?
url_parametersMap(String, String)Parsed query parameters, e.g. url_parameters['utm_source']
page_titleStringdocument.title; may be empty
referrerStringExternal referrer URL; empty for direct traffic
channelStringAcquisition channel, e.g. Direct, Organic Search, Referral
browser, browser_versionLowCardinality(String)Browser and major version
operating_system, operating_system_versionLowCardinality(String)OS and version
languageLowCardinality(String)Browser language, e.g. en-US
countryLowCardinality(FixedString(2))ISO 3166-1 alpha-2 code
regionLowCardinality(String)Region code, e.g. US-CA
cityStringCity name
lat, lonFloat64Coordinates; 0 when unknown
screen_width, screen_heightUInt16Screen size in pixels; 0 when unavailable
device_typeLowCardinality(String)Desktop, Mobile, Tablet, TV, Console or Embedded
typeLowCardinality(String)pageview, custom_event, performance, outbound, error, button_click, copy, form_submit or input_change
event_nameStringCustom event name, error name, or web-vitals
propsJSONEvent properties; read with JSONExtractString(toString(props), 'key')
lcp, cls, inp, fcp, ttfbNullable(Float64)Web vitals; set only on performance events
ipNullable(String)Visitor IP when IP tracking is on for the site; otherwise null
timezoneLowCardinality(String)Visitor timezone from geolocation
tagLowCardinality(String)Optional script tag; empty when unset
import_idNullable(UUID)Import job ID for imported rows; null for tracked events
feature_flagsMap(String, String)Feature flag values recorded with the event
asnNullable(UInt32)Autonomous system number of the visitor's IP
asn_orgLowCardinality(String)Organization that owns the ASN; empty when unknown
is_datacenter_asnUInt81 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

Request
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
  }'
Request
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();
Request
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()
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
];

$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);
Request
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)
Request
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)
Request
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?;
Request
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());
Request
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();
Response
{
  "data": [
    {
      "pathname": "/",
      "pageviews": "4210"
    },
    {
      "pathname": "/pricing",
      "pageviews": "1388"
    }
  ],
  "meta": {
    "queryId": "9f2c7a1e-3b4d-4e5f-8a6b-1c2d3e4f5a6b",
    "rowCount": 2,
    "maxExecutionTimeSeconds": 60,
    "maxRows": 1000
  }
}