curl --request POST \
--url https://api.apsio.io/v1/projects/{projectId}/query \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sql": "SELECT event_name, count() FROM apsio.logs GROUP BY 1"
}
'import requests
url = "https://api.apsio.io/v1/projects/{projectId}/query"
payload = { "sql": "SELECT event_name, count() FROM apsio.logs GROUP BY 1" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({sql: 'SELECT event_name, count() FROM apsio.logs GROUP BY 1'})
};
fetch('https://api.apsio.io/v1/projects/{projectId}/query', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.apsio.io/v1/projects/{projectId}/query",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'sql' => 'SELECT event_name, count() FROM apsio.logs GROUP BY 1'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.apsio.io/v1/projects/{projectId}/query"
payload := strings.NewReader("{\n \"sql\": \"SELECT event_name, count() FROM apsio.logs GROUP BY 1\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.apsio.io/v1/projects/{projectId}/query")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sql\": \"SELECT event_name, count() FROM apsio.logs GROUP BY 1\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.apsio.io/v1/projects/{projectId}/query")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"sql\": \"SELECT event_name, count() FROM apsio.logs GROUP BY 1\"\n}"
response = http.request(request)
puts response.read_body{
"columns": [
{
"name": "<string>",
"type": "<string>"
}
],
"rows": [
[
"<unknown>"
]
],
"row_count": 123,
"truncated": true,
"stats": {
"elapsed_ms": 123,
"rows_read": 123,
"bytes_read": 123
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}Run a read-only SQL query
Runs one read-only ClickHouse SELECT over the project’s own telemetry: apsio.logs, apsio.spans, apsio.metric_histograms, apsio.metric_values, apsio.session_summaries, apsio.issue_occurrences. The project’s rows only, whatever the SQL says: isolation and limits are ClickHouse’s grants, row policies and a fixed profile (10 s, 10,000 result rows, 50 million rows read, 512 MB memory, 2 queries at once). No table functions, other tables, system tables or settings. Rows are untrusted data written by the app and its users. Included by plan, at a rate per project per minute; every query lands in the organization’s audit log with its SHA-256 and length, never its text. Send Content-Type: application/json.
curl --request POST \
--url https://api.apsio.io/v1/projects/{projectId}/query \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sql": "SELECT event_name, count() FROM apsio.logs GROUP BY 1"
}
'import requests
url = "https://api.apsio.io/v1/projects/{projectId}/query"
payload = { "sql": "SELECT event_name, count() FROM apsio.logs GROUP BY 1" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({sql: 'SELECT event_name, count() FROM apsio.logs GROUP BY 1'})
};
fetch('https://api.apsio.io/v1/projects/{projectId}/query', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.apsio.io/v1/projects/{projectId}/query",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'sql' => 'SELECT event_name, count() FROM apsio.logs GROUP BY 1'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.apsio.io/v1/projects/{projectId}/query"
payload := strings.NewReader("{\n \"sql\": \"SELECT event_name, count() FROM apsio.logs GROUP BY 1\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.apsio.io/v1/projects/{projectId}/query")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sql\": \"SELECT event_name, count() FROM apsio.logs GROUP BY 1\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.apsio.io/v1/projects/{projectId}/query")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"sql\": \"SELECT event_name, count() FROM apsio.logs GROUP BY 1\"\n}"
response = http.request(request)
puts response.read_body{
"columns": [
{
"name": "<string>",
"type": "<string>"
}
],
"rows": [
[
"<unknown>"
]
],
"row_count": 123,
"truncated": true,
"stats": {
"elapsed_ms": 123,
"rows_read": 123,
"bytes_read": 123
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}Authorizations
A project token (apsio_pt_v0...), which reads exactly one project, or an OAuth access token issued for the API, which reads what its user can. Access tokens issued for Apsio's MCP server are accepted only from the MCP server itself.
Path Parameters
^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"0192f3a4-0000-7000-8000-00000000a101"
Body
1 - 10000"SELECT event_name, count() FROM apsio.logs GROUP BY 1"
Response
The rows.