Skip to content

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:

  1. It opens your browser at the authorization endpoint with client_id=comotion_cli, response_type=code, scope=openid, a PKCE code_challenge (S256) and a redirect to http://localhost:<random port>.
  2. You log in. ComoAuth redirects back to the local port with a code.
  3. The SDK swaps the code for tokens at the token endpoint (grant_type=authorization_code, the code_verifier and the same redirect_uri).
  4. The SDK saves the refresh token in your operating system's credential manager (keyring). It does not save access tokens.
  5. Each time it needs an access token, the SDK posts grant_type=refresh_token, client_id=comotion_cli and 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 Public­Consumer OwnSC­Consumer OwnSC­Analyst AllSC­Analyst Zone­Admin
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:write scope (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') or 1=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 an error field. 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.

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

  • 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)

7. Loads

A load adds rows to one lake table. Four steps:

  1. Create the load with POST /load. You get a loadId.
  2. Upload one or more parquet files with POST /load/{load_id}/file, then upload each file to the link it returns.
  3. Commit the load with checksums using POST /load/{load_id}/commit.
  4. Poll GET /load/{load_id} until SUCCESS or FAIL.

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_id was not given in POST /load, files must include a service_client_id column.
  • If load_as_service_client_id was given, files must not include a service_client_id column.

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)

  1. Files are collected and validated (parquet format and column names).
  2. A temporary Athena table is created from all uploaded files.
  3. Service client rules are enforced per row.
  4. Checksums are calculated and compared with your expected values.
  5. The lake table is created (if new) or columns are matched (if it exists). Column types must match.
  6. Rows are inserted. Every row gets a dash$load_id column. The table is partitioned by service_client_id, dash$load_id and any extra partitions you 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 DailyRun enabled flag only controls the scheduled nightly kickoff. You can always trigger a manual run via POST /dailyRun/start_execution regardless 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"
}

executionArn and stateMachineArn are included for reference; treat them as opaque strings. They are not needed for normal polling — use GET /dailyRun/execution_status?mode=latest to 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.