API reference
Base URL: https://api.ecomsolo.com. All endpoints live under /v1, require the
read_data scope, and authenticate with Authorization: Bearer esk_live_....
GET /v1/account
Verify a key and see the account. Not plan-gated — it answers even when the API is not enabled, which makes it the right diagnostic.
Response
{
"account_id": "acct_9f3c...",
"account_name": "Acme Retail Group",
"subscription_id": "sub_2a71...",
"api_enabled": true,
"store_count": 3,
"scopes": ["read_data"]
}
| Field | Type | Meaning |
|---|---|---|
account_id | string | The account identifier. |
account_name | string | The account's display name. |
subscription_id | string | The account's subscription identifier. |
api_enabled | boolean | Whether the plan includes the API. |
store_count | number | How many stores the key can access. |
scopes | string array | The scopes granted to the key. |
GET /v1/stores
The stores the key can access.
Response — an array of:
| Field | Type | Meaning |
|---|---|---|
id | string | Store identifier (use in store_ids). |
name | string | Store name (use in WHERE store IN (...)). |
myshopify_domain | string | The store's myshopify.com domain. |
currency | string | The store's currency, ISO code. |
timezone | string | The store's IANA time zone. |
status | string | active or syncing. |
GET /v1/datasets
The self-describing catalog of datasets and fields. See Datasets and fields.
Response — an array of datasets:
[
{
"name": "sales",
"title": "Sales",
"description": "Consolidated sales across all your stores.",
"fields": [
{
"name": "total_sales",
"title": "Total sales",
"description": "...",
"type": "money",
"role": "metric",
"default_aggregation": "sum"
}
]
}
]
Field type is one of string, number, money, date, boolean, percent.
Field role is dimension or metric. default_aggregation is one of sum,
avg, min, max, count, count_distinct, or null for dimensions.
POST /v1/query
Run a SoloQL query. See Querying with SoloQL.
Request body
| Field | Type | Meaning |
|---|---|---|
query | string, required | The SoloQL query string. |
store_ids | string array, optional | Narrow to these store IDs; can only narrow within the key's scope, never widen. |
time_zone | string, optional | IANA zone; defaults to the account's zone. |
Response
{
"columns": [
{ "name": "store", "title": "Store", "type": "string", "role": "dimension" }
],
"rows": [
{ "store": "EU Shop", "total_sales": 48210.55 }
],
"totals": { "total_sales": 120144.65 },
"currency": "EUR",
"time_zone": "Europe/Athens",
"window": { "gte": "2026-06-24T00:00:00Z", "lt": "2026-07-24T00:00:00Z" }
}
| Field | Meaning |
|---|---|
columns | One entry per column: name, title, type, role. Rows are keyed by name. |
rows | The result rows. |
totals | Present only with WITH TOTALS. |
currency | ISO currency the money columns were converted to. |
time_zone | IANA zone the window resolved in. |
window | Resolved UTC range (gte, lt). |
Send Accept: text/csv to get the same tabular result as CSV, with the column
titles as the header row.
Error model
Errors use RFC 7807 problem+json, with Content-Type: application/problem+json:
{
"type": "about:blank",
"title": "Invalid query",
"status": 400,
"detail": "The query has one or more errors.",
"errors": [
{ "line": 1, "column": 13, "message": "Unknown field 'totl_sales'", "severity": "error" }
]
}
| Status | When | Notes |
|---|---|---|
| 400 | Invalid query — empty, or SoloQL with errors. | For SoloQL errors the body includes errors, an array of positioned diagnostics (line, column, message, severity). |
| 401 | Unauthorized. | Missing, malformed, unknown, or revoked key. |
| 403 | Forbidden. | "Insufficient scope" (key lacks read_data) or "Plan upgrade required" (plan does not include the API). GET /v1/account still works and reports api_enabled: false. |
| 429 | Rate limit exceeded. | Includes Retry-After (seconds) and X-RateLimit-Limit headers. See Rate limits. |