Skip to content

Queries


6. Org API: identity

GET /identity/security_metadata

Returns your service client filter and a hash that identifies your access.

  • URL: https://<org>.api.comodash.io/v2/identity/security_metadata
  • Scope: none. Any valid dash_api token works.
  • SDK equivalent: none.

The filter lists every service client in your service_client_id claim plus 901. It is 1=1 (all rows) if your claim includes 0, or if your highest role is AllServiceClientAnalyst or ZoneAdmin.

This filter does not drop service clients that your role cannot read.

Response 200

Field Type Description
athena_service_client_where_clause string SQL condition, for example service_client_id IN ('12', '901'), 1=1 or 1=0.
service_client_hash string 16-character hash of your roles and service clients. It changes when your access changes.
{
  "athena_service_client_where_clause": "service_client_id IN ('12', '13', '901')",
  "service_client_hash": "3f9c2a7b1d4e5f60"
}

Errors

Status Body Cause and fix
403 {"error": "Unauthorized", "message": "No service client provided in access token"} Your token has no service_client_id claim. Ask your administrator to assign a service client.
403 {"error": "Unauthorized", "message": "Sub not provided in access token"}, "no JTI in the access token", "No resource access provided in access token" or "No resource access provided for dash_api in access token" The token lacks a required claim, or you have no roles on dash_api.
403 {"error": "Unauthorized", "message": "Invalid Service Client claim (claim in incorrect format: ...)"} or "No service client is configured" The service_client_id claim is not in the form [12 13], or is empty.
500 {"error": "Error", "message": "There was a problem returning the security metadata"} Unexpected error. Contact support.
curl -s "$API/identity/security_metadata" -H "Authorization: Bearer $TOKEN"
r = requests.get(f"{API}/identity/security_metadata", headers=HEADERS)
r.raise_for_status()
print(r.json()["athena_service_client_where_clause"])

7. Queries

7.1 Lifecycle

POST /query ──► QUEUED ──► RUNNING ──► SUCCEEDED ──► GET /query/{id}/result (first 1,000 rows)
                                  │                 └► GET /query/{id}/csv    (full result)
                                  ├──► FAILED     (read status.stateChangeReason)
                                  └──► CANCELLED  (after DELETE /query/{id})
State Meaning Final?
QUEUED Accepted, waiting to run. no
RUNNING Running. no
SUCCEEDED Finished. Results are ready. yes
FAILED Failed. status.stateChangeReason says why. yes
CANCELLED Stopped by a cancel request. yes

Polling: call GET /query/{query_id} every few seconds until the state is final. The SDK polls every 5 seconds. Do not poll more often than once a second.

7.2 POST /query: start a query

Starts a SQL query and returns its ID straight away.

Request body

Field Type Required Description
query string yes SQL text, in the Amazon Athena (Trino) dialect.
database string no Default database for unqualified table names. Defaults to your lake database: <org>_lake_v2 on the Org API, <zone>_lake on the Zone API.
{"query": "select * from policies where service_client_id IN ('12', '901') limit 10"}

Response 202

{"queryId": "06ba1d95-8c4f-460c-90b3-bc68fddf2fde"}

Errors

Status Message Cause and fix
400 JSON not well formed. The body is missing or is not valid JSON.
400 MissingParameterError: "query" parameter missing from request. Add the query field.
400 The query engine's message, e.g. An error occurred (InvalidRequestException) when calling the StartQueryExecution operation: line 1:8: ... The engine rejected the SQL when it was submitted. Fix the SQL.
403 You seem to have a permissions problem. You may be trying to perform a query you do not have permission to execute. The query uses a database or table you cannot access.
403 You seem to have a permissions problem. You are trying to access data you do not have permissions for. Your role or service clients do not allow this.
403 You don't have the right permissions to create tables there. Only some roles can create tables, and only in some databases. See section 4.3.
403 You don't have the right permissions to execute that query. Same as above, for other statements.
409 The engine's message A CREATE TABLE AS target location already has data. Use a new table name, or drop and clean up the old table.
500 An Internal Server Error occurred. Please contact support if this error persists. Unexpected error. This includes tokens with missing or malformed claims (see 3.6).
QUERY_ID=$(curl -s -X POST "$API/query" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"query": "select count(*) as n from policies"}' | jq -r .queryId)
echo "$QUERY_ID"
r = requests.post(f"{API}/query", headers=HEADERS,
                  json={"query": "select count(*) as n from policies"})
r.raise_for_status()
query_id = r.json()["queryId"]

7.3 GET /query/{query_id}: get query status

  • URL: https://<org>.api.comodash.io/v2/query/06ba1d95-8c4f-460c-90b3-bc68fddf2fde
  • Scope: query:read
  • SDK equivalent: Query.get_query_info(), Query.state(), Query.wait_to_complete()

Response 200

Field Type Description
query string The SQL text.
queryId string The query ID.
statementType string DML, DDL or UTILITY.
status.state string QUEUED, RUNNING, SUCCEEDED, FAILED or CANCELLED.
status.submissionDateTime string When the query was submitted, UTC, format MM-DD-YYYY HH:MM:SS.
status.completionDateTime string When it finished. Only present once finished.
status.stateChangeReason string Why it failed. Permission errors are replaced with the friendly messages listed under POST /query.
{
  "query": "select count(*) as n from policies",
  "queryId": "06ba1d95-8c4f-460c-90b3-bc68fddf2fde",
  "statementType": "DML",
  "status": {
    "state": "SUCCEEDED",
    "submissionDateTime": "10-13-2026 09:58:01",
    "completionDateTime": "10-13-2026 09:58:04"
  }
}

Errors: 404 with An error occurred (InvalidRequestException): QueryExecution <id> was not found for an unknown ID. 500 for unexpected errors.

curl -s "$API/query/$QUERY_ID" -H "Authorization: Bearer $TOKEN" | jq .status
import time

while True:
    status = requests.get(f"{API}/query/{query_id}", headers=HEADERS).json()["status"]
    if status["state"] in ("SUCCEEDED", "FAILED", "CANCELLED"):
        break
    time.sleep(5)
if status["state"] != "SUCCEEDED":
    raise RuntimeError(status.get("stateChangeReason"))

7.4 DELETE /query/{query_id}: cancel a query

  • URL: https://<org>.api.comodash.io/v2/query/06ba1d95-8c4f-460c-90b3-bc68fddf2fde
  • Scope: query:write
  • SDK equivalent: Query.stop()

Returns 200 with no body. The query moves to CANCELLED. Errors: 404 for an unknown ID, 500 for unexpected errors.

curl -s -X DELETE "$API/query/$QUERY_ID" -H "Authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}\n"
requests.delete(f"{API}/query/{query_id}", headers=HEADERS).raise_for_status()

7.5 GET /query/{query_id}/result: first page of results

Returns up to 1,000 rows of a SUCCEEDED query as JSON. On this page the first row holds the column names. Every value is a string.

This endpoint returns only the first page. If the response contains nextToken, there are more rows. Use the CSV download for the full result.

  • URL: https://<org>.api.comodash.io/v2/query/06ba1d95-8c4f-460c-90b3-bc68fddf2fde/result
  • Scope: query:read
  • SDK equivalent: none (the SDK uses the CSV download).

Response 200

Field Type Description
resultSet.Rows[] array One item per row. The first row holds the column names.
resultSet.Rows[].Data[].VarCharValue string Cell value. Left out for NULL.
resultSet.ResultSetMetadata.ColumnInfo[] array Column details: Name, Label, Type (varchar, bigint, double, decimal, date, timestamp, …), Precision, Scale, Nullable, CaseSensitive.
nextToken string Present when there are more than 1,000 rows.
{
  "resultSet": {
    "Rows": [
      {"Data": [{"VarCharValue": "policy_number"}, {"VarCharValue": "face_amount"}]},
      {"Data": [{"VarCharValue": "P0001"}, {"VarCharValue": "20000"}]}
    ],
    "ResultSetMetadata": {
      "ColumnInfo": [
        {"Name": "policy_number", "Label": "policy_number", "Type": "varchar", "Precision": 2147483647, "Scale": 0, "Nullable": "UNKNOWN", "CaseSensitive": true},
        {"Name": "face_amount", "Label": "face_amount", "Type": "bigint", "Precision": 19, "Scale": 0, "Nullable": "UNKNOWN", "CaseSensitive": false}
      ]
    }
  }
}

Errors: 404 for an unknown ID. 403 permission messages as for POST /query. 500 for unexpected errors.

page = requests.get(f"{API}/query/{query_id}/result", headers=HEADERS).json()
rows = [[c.get("VarCharValue") for c in r["Data"]] for r in page["resultSet"]["Rows"]]
header, data = rows[0], rows[1:]

7.6 GET /query/{query_id}/csv: download the full result

Redirects to a pre-signed HTTPS link for the full result as a CSV file. The query must be SUCCEEDED.

  • URL: https://<org>.api.comodash.io/v2/query/06ba1d95-8c4f-460c-90b3-bc68fddf2fde/csv
  • Scope: query:read
  • SDK equivalent: Query.download_csv(path), Query.get_csv_for_streaming()

Response 302

  • Location header: the pre-signed link.
  • Body: {"location": "<the same link>"}.

Important:

  • Do not send your Authorization header to the pre-signed link. The link carries its own signature, and the storage service rejects requests that have both. curl (7.58 and later) and Python requests drop the header when a redirect changes host.
  • The link expires quickly (at most 15 minutes). Start the download straight away. If it expires, call this endpoint again.
  • The CSV has a header row. Values are quoted.

Errors: any problem, including a query that is not SUCCEEDED or does not exist, returns 500 {"message": "Internal Server Error"}. Check the query status first.

curl -s -L "$API/query/$QUERY_ID/csv" -H "Authorization: Bearer $TOKEN" -o result.csv
r = requests.get(f"{API}/query/{query_id}/csv", headers=HEADERS,
                 allow_redirects=False)
r.raise_for_status()                     # 302
with requests.get(r.headers["Location"], stream=True) as dl:   # no auth header
    dl.raise_for_status()
    with open("result.csv", "wb") as f:
        for chunk in dl.iter_content(1024 * 1024):
            f.write(chunk)