VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Get started
Introduction
Quickstart
How Voidhash works
Glossary
Versions and releases
Concepts
Teams, projects and environments
API keys
Products and perks
Offers
Subscription lifecycle
People and identity
Events
Features
Paywalls in your app
Feature flags and A/B tests
Paywall components
Push notifications
Ad attribution in your app
Client SDKs
React Native
Swift
Kotlin
Web
Your backend
Node.js
PHP
Go
Rust
Other languages
Payment sources
Forward store notifications
Developer tools
CLI
MCP server
Query analytics with VoidQL

Query analytics with VoidQL

Run read-only SQL over a project's events, people, purchases and catalog from the API or an AI agent.

VoidQL is a read-only dialect of SQL over a project's Voidhash data: events, people, subscriptions, transactions, the catalog, paywalls, experiments, push and ads. Use it from your backend, scripts or an AI agent when a built-in report doesn't answer your question. Studio's Analytics → Query page runs the same language; see Query with VoidQL in the Help center.

Early access

The Query page is in early access. Ask us on Discord, or email support@voidhash.com on a paid plan, to turn it on for your team.

The API operations and MCP tools need no early access, and they work on a self-hosted server too.

WhereScopeRows returned
POST /api/v1/projects/{projectId}/analytics/voidql/queryOne project100 by default, up to 10,000 with limit
voidql_query MCP toolOne project100 by default, up to 1,000
Analytics → Query in Studio (Voidhash Cloud)Every project in the team whose analytics you can readUp to 100,000

Every operation needs read access to Analytics (project:analytics:read). See API keys.

Run a query

curl https://api.voidhash.com/api/v1/projects/$VOIDHASH_PROJECT_ID/analytics/voidql/query \
  -H "Authorization: Bearer $VOIDHASH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "SELECT store, count() AS active FROM subscriptions WHERE is_active GROUP BY store", "limit": 100 }'

The response has columns (each with name and type), rows (one object per row) and isTruncated, which is true when the query had more rows than limit. Times come back as UTC text. A query changes nothing, so it is safe to repeat. From a terminal, run voidhash api call voidql.query --project my-app --body '{"query": "…"}'.

Two more operations help you write queries:

  • GET …/analytics/voidql/schema lists every table and its columns, with types, descriptions and which columns are personal data, plus the functions a query may call. The voidql_schema MCP tool returns the same, with the rules and example queries.
  • POST …/analytics/voidql/explain and the voidql_explain MCP tool check a query without running it: the columns it returns, the tables and projects it reads, whether it reads personal data, and an estimate of the rows it would scan.

Syntax

VoidQL is a subset of ClickHouse SELECT. One statement per query, with an optional trailing ;.

  • Clauses: WITH, SELECT [DISTINCT], FROM, JOIN (INNER, LEFT, RIGHT, FULL, CROSS, with ON or USING), WHERE, PREWHERE, GROUP BY (with ROLLUP, CUBE or TOTALS), HAVING, QUALIFY, ORDER BY, LIMIT … OFFSET, LIMIT … BY.
  • Set operators: UNION ALL, UNION DISTINCT, INTERSECT, EXCEPT. A bare UNION is an error.
  • Subqueries in FROM (with an alias), IN (SELECT …), EXISTS (SELECT …) and as scalars.
  • Window functions: fn(…) OVER (PARTITION BY … ORDER BY … ROWS BETWEEN … AND …).
  • Literals: strings in single quotes ('' escapes a quote), numbers, TRUE, FALSE, NULL.
  • Case: keywords, table names and function names are case-insensitive; column names are not.

Only listed functions are allowed. They include:

KindFunctions
Aggregatecount, countIf, countDistinct, sum, sumIf, avg, avgIf, min, max, any, argMin, argMax, median, quantile, percentile, stddevPop, varPop, corr
Windowrow_number, rank, dense_rank, lagInFrame, leadInFrame, first_value, last_value
Conditionalif, multiIf, coalesce, ifNull, nullIf, greatest, least
Text and URLlower, upper, length, trim, concat, substring, startsWith, endsWith, position, replaceAll, match, toString, domain, path, extractURLParameter
Datenow, today, toDate, toStartOfDay (and of hour, week, month, quarter, year), addDays, subtractDays (and minutes to years), dateDiff, toYear, toMonth, toDayOfWeek, toHour, toTimeZone
Mathround, floor, ceil, abs, sqrt, pow, exp, log
ConversiontoFloat64OrNull, toInt64OrNull, toDateOrNull

count(DISTINCT x) works as countDistinct(x). The schema operation returns the full list.

Times are UTC. Date functions take a time zone as their last argument: toStartOfDay(event_ts, 'Europe/Prague'). JSON keys read as text with a dot: properties.$app_version on events, traits.plan on people. There are no query parameters, so build values into the query text and escape quotes.

Tables

Every table is scoped to the project automatically; a query can't set or widen the scope.

DataTables
Events and peopleevents, people, people_history, person_traits, identities, person_facts, touches, cohorts, cohort_members
Purchasessubscriptions, subscription_history, transactions, purchases, entitlements, access_shares, checkout_sessions, store_notifications, exchange_rates
Metrics as Studio counts themrevenue, mrr_movements, subscription_changes, trial_conversions
Catalogprojects, products, perks, product_perks, store_products
Paywalls and experimentspaywalls, paywall_releases, placements, placement_showings, experiments, experiment_variants, feature_flags, feature_flag_history, feature_flag_variants, feature_flag_overrides
Pushpush_devices, push_device_owners, push_sends, push_deliveries
Adsad_accounts, ads, ad_creatives, ad_issues, ad_changes, ad_spend
Operationstracking_domains, webhook_endpoints, webhook_deliveries, webhook_delivery_attempts, dead_letters, deletion_requests

Know these before you count money or subscriptions:

  • Environments are rows. Add environment = 'production' for real revenue and subscriptions.
  • Money is in minor units. *_amount columns are in the row's currency and *_usd columns in US cents. revenue.amount_usd is in US dollars, and ad_spend in millionths of the ad account's currency. VoidQL doesn't convert to your reporting currency.
  • Balances add up over time. MRR at a moment is sum(mrr_change_usd) of every movement up to it, so filter the range after summing.
  • A LEFT JOIN without a match reads '', 0 or 1970-01-01, not NULL, except for columns the schema marks as nullable.

MRR by month, from its movements:

WITH monthly AS (
  SELECT toStartOfMonth(event_ts) AS month, sum(mrr_change_usd) AS net_new
  FROM mrr_movements WHERE environment = 'production' GROUP BY month
)
SELECT month, net_new / 100 AS net_new_usd,
  sum(net_new) OVER (ORDER BY month) / 100 AS mrr_usd
FROM monthly ORDER BY month

Personal data

People's email, name and traits, and event keys that identify a person, such as properties.$email or context.$ip, also need read access to Customers (project:persons:read). Without it, the whole query is rejected with personal_data. people.id identifies a person without it.

Limits

LimitValue
Query length50,000 characters
Run time20 seconds
Memory2 GB per query
Joins8 per SELECT
Subqueries16 per query
Queries running at once4 per team
Quota60 queries a minute and 1,000 an hour per team

A self-hosted server applies the quota only once you create its read-only analytics users; see Analytics users.

Statements other than SELECT, such as INSERT, DROP or SETTINGS, are rejected before they run.

Errors

A query that can't run fails with an error code and a message that says what to change:

StatusCodeMeaning
400voidql_syntaxThe query doesn't parse.
400voidql_unsupportedIt uses a keyword or function VoidQL doesn't have.
400voidql_unknown_relation, voidql_unknown_fieldA table or column doesn't exist. param names the field, and the message suggests the closest name.
400voidql_too_complexToo many joins, subqueries or nesting levels.
400voidql_rejectedThe database refused it, usually over a column type.
403personal_dataIt reads personal data without project:persons:read.
429voidql_quotaYour team ran too many queries, or 4 are already running. Try again shortly.
503voidql_timeout, voidql_memory, voidql_too_largeIt ran too long, used too much memory or would read too much. Narrow the time range or aggregate first.
503voidql_unavailableThe query service couldn't be reached. Retry with backoff.

Next steps

  • MCP tools for analytics: let an AI agent write and run queries.
  • Events: what events holds and how it's captured.
  • Metric definitions: how Studio computes MRR, churn and the other metrics.
  • Query with VoidQL in Studio.

Something wrong or missing on this page? Tell us by email or on Discord.

PreviousFiles and assets

On this page

Run a querySyntaxTablesPersonal dataLimitsErrorsNext steps