Comotion Dash API Reference¶
Version 2 · October 2026
This document describes the HTTP APIs behind the Comotion Python SDK. It is for engineers who want to call the APIs directly, with curl, Python requests or any other HTTP client.
You should know HTTP and JSON. You do not need to know anything about how Comotion runs the service.
In examples, replace <org> with your org name and <zone> with your zone name. $TOKEN stands for a valid access token (see Authentication).
1. Overview¶
There are three APIs:
| API | What it is for | Base URL |
|---|---|---|
| Org API | Query and load data in your org's main data lake. | https://<org>.api.comodash.io/v2 |
| Zone API | Query and load data in one zone of your org. | https://<zone>.<org>.api.comodash.io/v2 |
| DailyRun API | Turn the nightly daily run on or off, start a run, check run status. | https://api.<org>.comodash.io/superset/dailyRun/... |
All three APIs:
- use HTTPS (TLS 1.2 or later) and JSON,
- need an
Authorization: Bearer <access token>header on every request, - get their tokens from your org's ComoAuth realm.
Org vs zone¶
An org is your Comotion Dash tenant. It has a main data lake.
A zone is a separate data area inside your org, with its own lake, its own permissions and its own API host. Use the Zone API for data in a zone. Use the Org API for the org's main lake. The two APIs have the same query and load endpoints. Only the Org API has no extra endpoints compared with the Zone API.
The data lake (data model v2)¶
- Lake tables hold your data. You add data to a lake table with a load.
- Every row has a service client (
service_client_id). Your access is limited to the service clients your user has been given. See Roles and permissions. - You read data with a query (SQL, Amazon Athena / Trino dialect).
Where the SDK fits¶
The Comotion Python SDK wraps these APIs. It handles login, token refresh, polling, file conversion and chunked uploads for you. Each endpoint below names the matching SDK call. If you work in Python, the SDK is usually the easiest option. Use the HTTP API directly from other languages, or when you need more control.
2. Hosts and regions¶
| Region | API domain |
|---|---|
| Europe (eu-west-1) | comodash.io |
| United States (us-east-1) | comodash.com |
| API | Host pattern |
|---|---|
| Org API | <orgname>.api.<domain> with base path /v2 |
| Zone API | <zonename>.<orgname>.api.<domain> with base path /v2 |
| DailyRun API | api.<orgname>.<domain> with base path /superset |
All paths in this document are relative to the base URL. For example, POST /query on the Org API is POST https://<org>.api.comodash.io/v2/query.
3. Authentication¶
3.1 Your ComoAuth realm¶
Each org has its own login realm on ComoAuth (an OpenID Connect provider):
| Item | Value for your org |
|---|---|
| Issuer | https://auth.comotion.us/auth/realms/<org> |
| Discovery document | https://auth.comotion.us/auth/realms/<org>/.well-known/openid-configuration |
| Authorization endpoint | https://auth.comotion.us/auth/realms/<org>/protocol/openid-connect/auth |
| Token endpoint | https://auth.comotion.us/auth/realms/<org>/protocol/openid-connect/token |
The APIs only accept tokens whose issuer is your org's realm. A token from another org's realm is rejected.
3.2 Getting a token as a user (interactive)¶
The SDK's comotion authenticate command uses the OAuth 2.0 authorization code flow with PKCE:
- It opens your browser at the authorization endpoint with
client_id=comotion_cli,response_type=code,scope=openid, a PKCEcode_challenge(S256) and a redirect tohttp://localhost:<random port>. - You log in. ComoAuth redirects back to the local port with a
code. - The SDK swaps the code for tokens at the token endpoint (
grant_type=authorization_code, thecode_verifierand the sameredirect_uri). - The SDK saves the refresh token in your operating system's credential manager (keyring). It does not save access tokens.
- Each time it needs an access token, the SDK posts
grant_type=refresh_token,client_id=comotion_cliand the saved refresh token to the token endpoint. It gets a new access token about 30 seconds before the current one expires.
If you already use the SDK, the simplest way to get a token for curl is:
from comotion.auth import Auth
print(Auth("<org>").get_access_token())
To do it yourself, refresh a token like this:
curl -s https://auth.comotion.us/auth/realms/<org>/protocol/openid-connect/token \
-d grant_type=refresh_token \
-d client_id=comotion_cli \
-d refresh_token="$REFRESH_TOKEN" | jq -r .access_token
3.3 Getting a token as an application (machine to machine)¶
For scheduled jobs and services, Comotion can set up an application client for you with a client ID and client secret. It uses the OAuth 2.0 client credentials grant:
TOKEN=$(curl -s https://auth.comotion.us/auth/realms/<org>/protocol/openid-connect/token \
-d grant_type=client_credentials \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET" | jq -r .access_token)
import requests
resp = requests.post(
"https://auth.comotion.us/auth/realms/<org>/protocol/openid-connect/token",
data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
)
resp.raise_for_status()
TOKEN = resp.json()["access_token"]
SDK equivalent: Auth("<org>", entity_type=Auth.APPLICATION, application_client_id=..., application_client_secret=...).
Ask Comotion support to create an application client. Its roles, scopes and service clients are set when it is created.
3.4 Token lifetime¶
Access tokens are short-lived. Read expires_in in the token response, or the exp claim in the token, and get a new token before it expires.
3.5 What the APIs check in the token¶
| Check | Org API | Zone API (<zone>) | DailyRun API |
|---|---|---|---|
| Issuer | your realm | your realm | your realm |
Audience (aud) | dash_api | dash_api_<zone> | dash_api |
| Scope | per endpoint, e.g. query:write | per endpoint, e.g. <zone>:query:write | per endpoint, e.g. dailyrun:execution:read |
| Claims the handler reads | sub, jti, service_client_id, resource_access | same | none |
The service_client_id claim is a string in the form [12 13]: square brackets around service client IDs separated by spaces.
resource_access must contain an entry for the API's client (dash_api or dash_api_<zone>) listing your roles. See Roles and permissions.
Your Comotion administrator sets your audiences, scopes, roles and service clients. If a call fails with 401 or 403, decode your token (for example at a JWT debugger you trust, or with jq -R 'split(".")[1] | @base64d | fromjson') and compare it with the table above.
3.6 Common 401 and 403 causes¶
| Response | Likely cause | Fix |
|---|---|---|
401 {"message":"Unauthorized"} | No Authorization header, a token that has expired, a token from the wrong realm, or the wrong audience (for example a dash_api token sent to the Zone API). | Get a fresh token from your org's realm with the right audience. |
403 {"message":"Forbidden"} | The token is valid but lacks the scope this endpoint needs. | Ask your administrator for the scope. |
403 with a "permissions problem" message | Your query touches data your role or service clients cannot read. | See Roles and permissions. |
500 on query endpoints right after login | The token lacks the service_client_id, sub, jti or resource_access claim, or service_client_id is badly formed. | Ask your administrator to assign you a service client and a role. |
4. Roles and permissions¶
4.1 Service clients¶
Comotion uses service clients for row-level security. A service client usually represents one of your own clients or business units, and your access is limited to the service clients assigned to you.
Two IDs are special:
| ID | Meaning |
|---|---|
901 | Public. Everyone with a role can read public rows. It is added to every user's list automatically. |
0 | All service clients. A user whose service_client_id claim includes 0, and who has any role other than PublicConsumer, can read every service client. |
4.2 Roles¶
Roles are assigned per API: on dash_api for the Org API, and on dash_api_<zone> for each zone. A user can have different roles in different zones.
| Role | In short |
|---|---|
PublicConsumer | Reads public (901) insights only. |
OwnServiceClientConsumer | Reads insights for their own service clients. |
OwnServiceClientAnalyst | Reads lake and insights data for their own service clients. Can create tables in the sandbox. |
AllServiceClientAnalyst | Reads all service clients. Can write to lake and insights tables. |
ZoneAdmin | Everything an AllServiceClientAnalyst can do, plus delete and modify tables. |
4.3 Capability matrix¶
| Capability | PublicConsumer | OwnSCConsumer | OwnSCAnalyst | AllSCAnalyst | ZoneAdmin |
|---|---|---|---|---|---|
Read public (901) insights | ✓ | ✓ | ✓ | ✓ | ✓ |
| Read insights for own service clients | ✓ | ✓ | ✓ | ✓ | |
| Read insights for all service clients | ¹ | ¹ | ✓ | ✓ | |
| Read lake and data-input tables (own service clients) | ✓ | ✓ | ✓ | ||
| Read lake and data-input tables (all service clients) | ¹ | ✓ | ✓ | ||
| Create, change and drop tables in the sandbox | ✓ | ✓ | ✓ | ||
| Create and update lake and insights tables, add and remove partitions, write data with SQL | ✓ | ✓ | |||
| Drop and modify lake and insights tables | ✓ | ||||
| Load rows for a service client through the load API | ² | ✓ | ✓ | ✓ (any) | ✓ (any) |
¹ Only if the user's service_client_id claim includes 0. ² Public (901) rows only.
Notes:
- If a user has several roles, the capabilities add up.
- The load API also needs the
load:writescope (or<zone>:load:write). Loads write through Comotion's load pipeline, not with your own permissions. The pipeline checks that every row belongs to a service client you can access. - A user with no recognised role can only read their own query results. Every query that touches data fails.
4.4 How data scoping works in queries¶
Access is enforced on the stored data, which is split by service client. In practice:
- A query that reads data outside your service clients fails with a permissions error. It does not silently drop those rows.
- To read only what you may see, add your service client filter to the query, for example
service_client_id IN ('12', '901')or1=1(all rows). - Your query results are stored in a location tied to your user, roles and service clients. Only you can read them. If your roles or service clients change, you can no longer read results of queries you ran before the change.
4.5 Scopes per endpoint¶
| Endpoint | Org API scope | Zone API scope |
|---|---|---|
POST /query | query:write | <zone>:query:write |
GET /query/{query_id} | query:read | <zone>:query:read |
DELETE /query/{query_id} | query:write | <zone>:query:write |
GET /query/{query_id}/result | query:read | <zone>:query:read |
GET /query/{query_id}/csv | query:read | <zone>:query:read |
POST /load | load:write | <zone>:load:write |
POST /load/{load_id}/file | load:write | <zone>:load:write |
POST /load/{load_id}/commit | load:write | <zone>:load:write |
GET /load/{load_id} | load:read | <zone>:load:read |
GET /dailyRun/dailyRun_enabled | dailyrun:enabled:read (DailyRun API) | not available |
POST /dailyRun/dailyRun_enabled | dailyrun:enabled:write (DailyRun API) | not available |
GET /dailyRun/execution_status | dailyrun:execution:read (DailyRun API) | not available |
POST /dailyRun/start_execution | dailyrun:execution:write (DailyRun API) | not available |
5. Conventions¶
- Requests: send JSON bodies with
Content-Type: application/json. - Responses: JSON unless stated otherwise.
- Errors: most errors have the body
{"message": "..."}. The identity endpoint also adds anerrorfield. Where a message includes a request code (...please quote this code: <id>), send that code to support. - Gateway errors: some errors come from the API gateway before your request reaches Comotion's code:
401 Unauthorized,403 Forbidden(missing scope),404 Not Found(unknown path),413(request too large) and a timeout error for requests that take longer than 30 seconds. - Timeouts: every endpoint is designed to answer within seconds. Long-running work (queries, load commits, daily runs) runs in the background, and you poll for status.
Shell examples use these variables:
export ORG=<org>
export API=https://$ORG.api.comodash.io/v2
export TOKEN=... # access token, see section 3
Python examples use:
import requests
ORG = "<org>"
API = f"https://{ORG}.api.comodash.io/v2"
TOKEN = "..." # access token, see section 3
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
6. Queries¶
6.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.
6.2 POST /query: start a query¶
Starts a SQL query and returns its ID straight away.
- URL:
https://<org>.api.comodash.io/v2/query - Scope:
query:write - SDK equivalent:
Query(config, query_text=...)
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"]
6.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"))
6.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()
6.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:]
6.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
Locationheader: the pre-signed link.- Body:
{"location": "<the same link>"}.
Important:
- Do not send your
Authorizationheader 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 Pythonrequestsdrop 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)
7. Loads¶
A load adds rows to one lake table. Four steps:
- Create the load with
POST /load. You get aloadId. - Upload one or more parquet files with
POST /load/{load_id}/file, then upload each file to the link it returns. - Commit the load with checksums using
POST /load/{load_id}/commit. - Poll
GET /load/{load_id}untilSUCCESSorFAIL.
Either all rows are added or none. Only you can see or change your loads.
7.1 Load status¶
| Status | Meaning |
|---|---|
OPEN | Created, not committed. You can still upload files. |
PROCESSING | Committed and being validated. |
SUCCESS | All rows were written to the lake table. |
FAIL | Nothing was written. ErrorType and ErrorMessages say why. |
Poll every 5 seconds while the status is PROCESSING.
7.2 POST /load: create a load¶
URL: POST https://<org>.api.comodash.io/v2/load | Scope: load:write
SDK equivalent: Load(config, load_type="APPEND_ONLY", table_name=...)
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
load_type | string | yes | Must be APPEND_ONLY. Rows are added, never changed or removed. |
table_name | string | yes | Target lake table. Lowercase letters, numbers and underscores. Cannot start or end with underscore. Created on first load. |
load_as_service_client_id | string | no | All rows belong to this service client. Files must not then have a service_client_id column. Send as a string, e.g. "12". |
partitions | string[] | no | Extra partition columns. Up to 100 distinct value combinations per load. |
Response 202
{"loadId": "20261013095801_355d42f6_0684_4b8f_b274_225bf7237494"}
Key errors: 400 MissingParameterError for missing fields; 400 InvalidParameterError: invalid load_type parameter (only APPEND_ONLY is accepted); 400 InvalidParameterError: table_name - Value should be in lowercase and contain only lowercase letters and underscores.
load_as_service_client_id is not verified here. If you cannot access that service client, the commit fails later with ErrorType: PermissionsError.
LOAD_ID=$(curl -s -X POST "$API/load" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"load_type":"APPEND_ONLY","table_name":"policies","load_as_service_client_id":"12"}' \
| jq -r .loadId)
r = requests.post(f"{API}/load", headers=HEADERS, json={
"load_type": "APPEND_ONLY", "table_name": "policies",
"load_as_service_client_id": "12",
})
r.raise_for_status()
load_id = r.json()["loadId"]
7.3 POST /load/{load_id}/file: upload a file¶
Returns a pre-signed S3 URL and temporary credentials for uploading one parquet file. Call once per file. The credentials expire after 1 hour.
URL: POST https://<org>.api.comodash.io/v2/load/{load_id}/file | Scope: load:write
SDK equivalent: Load.upload_file(data, ...) or Load.upload_df(df, ...)
Optional request body field
| Field | Type | Description |
|---|---|---|
file_key | string | Custom filename stem. Lowercase, letters, numbers, underscores. Uploading to the same file_key twice overwrites the first file (idempotent). |
Response 200
| Field | Type | Description |
|---|---|---|
presigned_url | object | Use as an HTTP POST to S3. Contains url and fields (AWSAccessKeyId, Policy, Signature, key, x-amz-security-token). |
sts_credentials | object | Temporary AWS credentials: AccessKeyId, SecretAccessKey, SessionToken. The SDK uses these with boto3. |
path | string | S3 object key. |
bucket | string | S3 bucket name. |
File requirements (enforced at commit time, not during upload):
- Must be a valid parquet file.
- Column names: only lowercase letters, numbers and underscores.
- If
load_as_service_client_idwas not given inPOST /load, files must include aservice_client_idcolumn. - If
load_as_service_client_idwas given, files must not include aservice_client_idcolumn.
Key errors: 400 InvalidParameterError: file_key - ... for a bad file_key; 404 NotFound: load_id does not exist for this user. for an unknown load ID.
7.4 POST /load/{load_id}/commit: commit the load¶
Starts the load pipeline. The status becomes PROCESSING. The load ID is the pipeline execution name, so calling this endpoint twice returns 400 InvalidParameter: Load Already Committed.
URL: POST https://<org>.api.comodash.io/v2/load/{load_id}/commit | Scope: load:write
SDK equivalent: Load.commit(check_sum={...})
Request body
{"check_sum": {"count(*)": 50000, "sum(face_amount)": 9876543210}}
check_sum is required and must have at least one entry. Keys are SQL aggregate expressions run over all uploaded rows. Values are what you expect. Accepts string, integer or float values.
Response 202: {"message": "Commit initiated."}
Key errors
| Status | Message | Cause |
|---|---|---|
| 400 | Invalid checksum data provided. | check_sum is not an object or has wrong types. |
| 400 | MissingParameter: missing check_sum parameter. At least one check_sum is required | The check_sum object is empty. |
| 400 | InvalidParameter: Load Already Committed. | Already committed. Check GET /load/{load_id}. |
| 404 | NotFound: load_id does not exist for this user. | Wrong load ID or not created by you. |
curl -s -X POST "$API/load/$LOAD_ID/commit" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"check_sum": {"count(*)": 50000}}'
requests.post(f"{API}/load/{load_id}/commit", headers=HEADERS,
json={"check_sum": {"count(*)": 50000}}).raise_for_status()
7.5 GET /load/{load_id}: get load status¶
URL: GET https://<org>.api.comodash.io/v2/load/{load_id} | Scope: load:read
SDK equivalent: Load.wait_to_complete(), Load.get_load_info()
Response 200
| Field | Type | Description |
|---|---|---|
LoadStatus | string | OPEN, PROCESSING, SUCCESS or FAIL. |
ErrorType | string | Only present on FAIL. |
ErrorMessages | array | Only present on FAIL. Items can be strings or objects with ErrorType, ErrorMessage and CheckResult. |
ErrorType values
| ErrorType | Cause |
|---|---|
NoFilesUploaded | No files were uploaded before the commit. |
ParquetValidation | File is not valid parquet, or has column names with disallowed characters. |
ServiceClientError | service_client_id is missing from both files and load_as_service_client_id, or present in both, or a row belongs to a service client you cannot access. |
CheckSumError | An aggregate value does not match your expected value. The message names the aggregate, expected and actual. |
ValidationError | Columns do not match the existing lake table (type mismatch, extra or missing columns). |
TooManyPartitions | More than 100 distinct partition value combinations. |
PermissionsError | load_as_service_client_id is not a service client you can access. |
InternalError | Unexpected internal error. Contact support. |
{
"LoadStatus": "FAIL",
"ErrorType": "CheckSumError",
"ErrorMessages": [
{
"CheckResult": "FAIL",
"ErrorType": "CheckSumError",
"ErrorMessage": "Checksum count(*) was expecting 50001 and got 50000"
}
]
}
import time
while True:
r = requests.get(f"{API}/load/{load_id}", headers=HEADERS).json()
print(r["LoadStatus"])
if r["LoadStatus"] not in ("OPEN", "PROCESSING"):
break
time.sleep(5)
if r["LoadStatus"] != "SUCCESS":
raise RuntimeError(r.get("ErrorMessages"))
7.6 What happens after commit (pipeline overview)¶
- Files are collected and validated (parquet format and column names).
- A temporary Athena table is created from all uploaded files.
- Service client rules are enforced per row.
- Checksums are calculated and compared with your expected values.
- The lake table is created (if new) or columns are matched (if it exists). Column types must match.
- Rows are inserted. Every row gets a
dash$load_idcolumn. The table is partitioned byservice_client_id,dash$load_idand any extrapartitionsyou requested.
8. DailyRun API¶
The DailyRun API controls and monitors the nightly ETL pipeline that refreshes your org's lake. It is accessed at api.{orgname}.{region-domain}/superset.
Note — The
DailyRunenabled flag only controls the scheduled nightly kickoff. You can always trigger a manual run viaPOST /dailyRun/start_executionregardless of the flag.
8.1 GET /dailyRun/dailyRun_enabled¶
Returns whether the scheduled nightly ETL run is enabled for this org.
Required scope: dailyrun:enabled:read
Request¶
GET https://api.{orgname}.{region-domain}/superset/dailyRun/dailyRun_enabled
Authorization: Bearer {access_token}
Responses¶
| Status | Body | Meaning |
|---|---|---|
200 OK | {"dailyRun": true} or {"dailyRun": false} | Current enabled state. |
404 Not Found | {"message": "DailyRun not found"} | The org metadata record has no DailyRun attribute yet. |
500 | {"message": "Error reading ClientMetaData"} | Internal server error. |
200 example
{ "dailyRun": true }
8.2 POST /dailyRun/dailyRun_enabled¶
Enables or disables the scheduled nightly ETL run.
Required scope: dailyrun:enabled:write
Request¶
POST https://api.{orgname}.{region-domain}/superset/dailyRun/dailyRun_enabled
Authorization: Bearer {access_token}
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
dailyRun | boolean | yes | true to enable, false to disable. Also accepts "true" / "false" / 1 / 0 / "yes" / "no" as strings. |
Example request body
{ "dailyRun": true }
Responses¶
| Status | Body | Meaning |
|---|---|---|
200 OK | {"dailyRun": <value>} | Successfully updated. Returns the value that was written. |
400 Bad Request | {"message": "Body must be valid JSON with 'dailyRun' set to true or false."} | Missing body, invalid JSON, or unrecognised value. |
500 | {"message": "Error updating ClientMetaData"} | Internal server error. |
200 example
{ "dailyRun": true }
8.3 GET /dailyRun/execution_status¶
Returns execution records for this org's nightly pipeline.
Required scope: dailyrun:execution:read
Request¶
GET https://api.{orgname}.{region-domain}/superset/dailyRun/execution_status
Authorization: Bearer {access_token}
Query parameters¶
| Parameter | Default | Description |
|---|---|---|
mode | list | One of list, latest, last_successful (case-insensitive). |
limit | 50 | Max executions to return when mode=list. Clamped to 1–200. |
mode values¶
mode | Returns |
|---|---|
list | An array of up to limit executions, most recent first. |
latest | A single execution object (the most recent), or null if none. |
last_successful | A single execution object with status: "SUCCEEDED", or a message object if none found. |
Execution object fields¶
| Field | Type | Description |
|---|---|---|
name | string | Execution name, format DailyScheduledETL_{orgname}_{timestamp}. |
status | string | Execution status: RUNNING, SUCCEEDED, FAILED, TIMED_OUT, ABORTED. |
startDate | string (ISO 8601) | When the execution started, e.g. "2025-04-01T02:00:00.000000+00:00". |
stopDate | string (ISO 8601) or null | When the execution finished; null if still running. |
Responses¶
200 — mode=list
{
"executions": [
{
"name": "DailyScheduledETL_<org>_20250401T020000Z",
"status": "SUCCEEDED",
"startDate": "2025-04-01T02:00:00.000000+00:00",
"stopDate": "2025-04-01T02:14:37.000000+00:00"
}
]
}
200 — mode=latest
{
"name": "DailyScheduledETL_<org>_20250401T020000Z",
"status": "SUCCEEDED",
"startDate": "2025-04-01T02:00:00.000000+00:00",
"stopDate": "2025-04-01T02:14:37.000000+00:00"
}
(Returns null body when no executions exist.)
200 — mode=last_successful (none found)
{ "message": "No successful Daily ETL executions found." }
400 — invalid mode
{
"message": "mode is not valid, mode should be one of: list, latest, last_successful"
}
8.4 POST /dailyRun/start_execution¶
Manually starts a new pipeline execution for this org. The pipeline input is constructed automatically from your org's configuration — you do not supply it.
Required scope: dailyrun:execution:write
Request¶
POST https://api.{orgname}.{region-domain}/superset/dailyRun/start_execution
Authorization: Bearer {access_token}
No request body required.
Responses¶
| Status | Body | Meaning |
|---|---|---|
200 OK | Success object (see below) | Execution started. |
409 Conflict | Conflict object (see below) | A DailyETLPipeline run is already RUNNING for this org. |
500 | {"message": "Error starting Daily ETL pipeline execution"} | Internal server error. |
200 success body
{
"message": "Daily ETL pipeline execution started.",
"started": true,
"executionArn": "arn:aws:states:eu-west-1:...:execution:DailyETLPipeline:DailyScheduledETL_<org>_20250401T020000Z",
"startDate": "2025-04-01T02:00:00.000000+00:00",
"executionName": "DailyScheduledETL_<org>_20250401T020000Z",
"stateMachineArn": "arn:aws:states:eu-west-1:...:stateMachine:DailyETLPipeline"
}
executionArnandstateMachineArnare included for reference; treat them as opaque strings. They are not needed for normal polling — useGET /dailyRun/execution_status?mode=latestto track progress.
409 conflict body
{
"message": "Daily ETL pipeline is already running for this client; no execution started.",
"started": false,
"executionName": null,
"runningExecution": {
"name": "DailyScheduledETL_<org>_20250401T020000Z",
"status": "RUNNING",
"startDate": "2025-04-01T02:00:00.000000+00:00"
},
"stateMachineArn": "arn:aws:states:eu-west-1:...:stateMachine:DailyETLPipeline"
}
Typical polling flow¶
POST /dailyRun/start_execution
→ 200 { "executionName": "DailyScheduledETL_<org>_..." }
Loop:
GET /dailyRun/execution_status?mode=latest
→ { "status": "RUNNING", ... } ← keep polling
→ { "status": "SUCCEEDED", ... } ← done
→ { "status": "FAILED", ... } ← check logs
9. Zone API¶
A zone is a separate data area inside your org with its own lake, its own AWS account, and its own API host. The Zone API has the same query and load endpoints as the Org API, but with the following differences.
11.1 Host¶
https://{zonename}.{orgname}.api.{region-domain}/v2/
Example: https://<zone>.<org>.api.comodash.io/v2/
11.2 Audience and scopes¶
| Aspect | Org API | Zone API |
|---|---|---|
| JWT audience | dash_api | dash_api_{zonename} |
| Scope prefix | none | {zonename}: |
Zone scopes follow the pattern {zonename}:{action}. Examples for a zone named <zone>:
| Operation | Scope |
|---|---|
| Run a query | <zone>:query:write |
| Poll / read query | <zone>:query:read |
| Start a load | <zone>:load:write |
| Poll / read load | <zone>:load:read |
You must request a token with the correct audience (dash_api_<zone>) and scope against the same ComoAuth realm (https://auth.comotion.us/auth/realms/{orgname}).
11.3 Available endpoints¶
The Zone API exposes only query and load endpoints:
| Endpoint | Supported |
|---|---|
POST /query | ✓ |
GET /query/{query_id} | ✓ |
DELETE /query/{query_id} | ✓ |
GET /query/{query_id}/result | ✓ |
GET /query/{query_id}/csv | ✓ |
POST /load | ✓ |
POST /load/{load_id}/file | ✓ |
POST /load/{load_id}/commit | ✓ |
GET /load/{load_id} | ✓ |
DailyRun endpoints | ✗ Frontend API only |
11.4 Base path¶
The Zone API base path is /v2 (not /v1 as on the Org API).
10. Error reference¶
All error responses use the shape:
{ "message": "Human-readable description" }
Where the error originates from a handler validation step it may also include:
{ "message": "MissingParameterError: load_type parameter missing from request." }
or
{ "message": "InvalidParameterError: invalid load_type parameter" }
HTTP status codes¶
| Status | When |
|---|---|
400 Bad Request | Missing or invalid input parameter. |
404 Not Found | Resource (query, load, DailyRun record) does not exist. |
409 Conflict | Concurrency conflict — load already committed, pipeline already executing. |
500 Internal Server Error | Unexpected AWS service error. A request code is included where possible — quote it to support. |
Load error types (recap)¶
error_type | Root cause |
|---|---|
CHECKSUM_MISMATCH | Row counts or null counts differ from the check_sum you supplied. |
COLUMN_TYPE_MISMATCH | Parquet column types differ from the existing lake table. |
MISSING_COLUMN | A column present in the lake table is absent from the uploaded file. |
UNEXPECTED_COLUMN | A column in the uploaded file does not exist in the lake table. |
MISSING_FILE | A committed file_key was never successfully uploaded. |
INVALID_PARQUET | The uploaded file is not valid Parquet. |
SERVICE_CLIENT_MISMATCH | Row-level service_client_id values violate the token's scoping rules. |
INTERNAL_ERROR | Unexpected pipeline failure — contact support with the load ID. |