Metrics SQL API
Query your Qyra semantic layer over the Postgres wire protocol from any SQL client or BI tool
Beta Enterprise The Metrics SQL API is disabled by default and must be enabled for your organization by an admin. For more information on our plans, visit our pricing page. What Beta means.
The Metrics SQL API is a Postgres wire protocol compatible interface for querying your Qyra semantic layer. Any tool that can connect to a PostgreSQL database — psql, BI tools, notebooks, database drivers — can connect to Qyra and query your metrics and dimensions with SQL.
Your explores are exposed as tables, and their dimensions and metrics as columns. When you run a query, Qyra compiles it through the semantic layer, so metric calculations, joins, and access controls are handled for you — you get the same numbers as in the Qyra UI.
Enable it for your organization
Once the endpoint is available on your instance, an organization admin turns it on from Project settings → Semantic Layer API by toggling Semantic layer Postgres connection. The same page then shows the connection details, an inline Generate token button for creating a scoped service account token, and ready-to-copy client snippets. Non-admins see the tab but not the connection details.
Connect
You can find copy-paste connection details in your project settings under Semantic Layer API. Connect with any Postgres-compatible client using:
| Field | Value |
|---|---|
| Host | pg.<your_instance_name>.qyraflow.com |
| Port | 5432 |
| Database | Your project UUID. The slugified project name also works, but changes if the project is renamed — the UUID is stable. You can find it in the Qyra URL when viewing a project (/projects/<projectUuid>/...) |
| User | Your Qyra account email. This value is informational — authentication is via the token |
| Password | A service account token (ldsvc_..., recommended) or a personal access token (ldpat_...) |
As a connection URI:
psql "postgresql://<your_email>:<token>@pg.<your_instance_name>.qyraflow.com:5432/<project_uuid>?sslmode=require"Queries run with the permissions of the token, so you can only query explores and fields that the token's account has access to.
How queries work
- Tables are explores. Each explore in your project appears as a table, and
FROMtakes a single explore. Fields from tables joined in the explore are included as columns — joins are defined in the semantic layer, not in your SQL. - Columns are field IDs. Dimensions and metrics use their Qyra field IDs, e.g.
orders_status,orders_total_order_amount. - Metrics are pre-aggregated. Select a metric like any other column and Qyra computes the aggregation for you.
GROUP BYis optional — results are implicitly grouped by the dimensions you select. If you do write one, it's validated against yourSELECTlist. - Results are limited to 500 rows by default. Add a
LIMITto override it.
Supported SQL includes WHERE (=, !=, <, <=, >, >=, IN, LIKE/ILIKE, BETWEEN, IS NULL, AND/OR), HAVING, ORDER BY, LIMIT, date parts of date columns (EXTRACT, DATE_PART, DATE_TRUNC), and calculated expressions in the SELECT list (arithmetic, functions, CASE, and window functions).
Examples
Discover explores and fields
The catalog is exposed through information_schema. The field_type column on information_schema.columns tells you whether a field is a dimension or a metric:
SELECT table_name
FROM information_schema.tables;
SELECT column_name, data_type, field_type
FROM information_schema.columns
WHERE table_name = 'orders';Query a metric by a dimension
Select the dimensions and metrics you want — no GROUP BY or aggregate functions needed:
SELECT
orders_status,
orders_total_order_amount
FROM orders
ORDER BY orders_total_order_amount DESC;Filter and limit
SELECT
orders_order_date_month,
orders_total_order_amount,
payments_unique_payment_count
FROM orders
WHERE orders_order_date >= '2026-01-01'
AND orders_status IN ('completed', 'shipped')
ORDER BY 1
LIMIT 12;Date parts
EXTRACT, DATE_PART and DATE_TRUNC over a date or timestamp dimension compile to that dimension's Qyra time frame, so the warehouse groups by the date part directly. The explore's own interval column (e.g. orders_order_date_year) is used when it exists; otherwise the part is computed the same way Qyra builds interval dimensions. ORDER BY and GROUP BY can repeat the same expression — this is the shape BI tools like Looker Studio generate for derived year, quarter and month fields:
SELECT
orders_order_date_day,
CAST(EXTRACT(YEAR FROM orders_order_date::TIMESTAMP) AS INT) AS "Year",
orders_total_order_amount
FROM orders
ORDER BY CAST(EXTRACT(YEAR FROM orders_order_date::TIMESTAMP) AS INT) DESC;Supported parts are YEAR, QUARTER, MONTH, WEEK, DAY, DOY, HOUR and MINUTE for EXTRACT/DATE_PART, and year through milliseconds for DATE_TRUNC. EXTRACT(DOW) is rejected because Postgres and Qyra number weekdays differently — select the dimension's day-of-week interval column instead.
Parts the model already exposes as interval columns work for every role. Parts the model doesn't list are computed as custom SQL dimensions, which — like calculated expressions — need the developer role's custom fields permission.
Calculated expressions
Combine metrics and dimensions with expressions in the SELECT list, like table calculations in the Qyra UI:
SELECT
orders_status,
orders_total_order_amount,
orders_total_order_amount / payments_unique_payment_count AS amount_per_payment
FROM orders;Query from Python
Because the interface speaks the Postgres wire protocol, existing Postgres drivers work:
import psycopg2
import pandas as pd
conn = psycopg2.connect(
host="pg.<your_instance_name>.qyraflow.com",
port=5432,
dbname="<project_uuid>",
user="<your_email>",
password="<token>",
sslmode="require",
)
df = pd.read_sql(
"SELECT orders_status, orders_total_order_amount FROM orders",
conn,
)If a query isn't valid, error messages include hints — an unknown column error lists the available fields, and using an aggregate function suggests the matching metric instead.
Errors
Errors are returned as standard Postgres ErrorResponse messages, so your client's error handling behaves normally.
| SQLSTATE | Meaning |
|---|---|
28P01 | Authentication failed — the password is not a valid ldsvc_ or ldpat_ token, or the token belongs to a SCIM-only service account. |
3D000 | The database name is unknown or ambiguous. Use a project UUID, or one of the slugs listed in the error message. |
0A000 | The client tried to use an unsupported feature — for example, the extended query protocol, pg_catalog, or an operator that isn't allowed on information_schema. |
Self-hosting
If you self-host Qyra, the Metrics SQL API server is disabled until you set the PGWIRE_PORT environment variable. TLS is required by default, so you also need to provide a certificate and key (PEM format) for the hostname clients connect to:
PGWIRE_PORT=5432
PGWIRE_HOST=pg.analytics.example.com # optional: hostname shown to admins in project settings
PGWIRE_SSL_CERT_PATH=/path/to/tls.crt # server certificate (leaf first, then chain)
PGWIRE_SSL_KEY_PATH=/path/to/tls.key # private keyIf PGWIRE_PORT is set without certificate paths and without PGWIRE_SSL_MODE=disabled, Qyra fails fast at boot with a config error — this is intentional, so a misconfigured deployment can't accidentally accept Qyra tokens in plaintext. PGWIRE_HOST is optional. If unset, the project settings page shows an empty host and admins will need to fill it in themselves.
- If your certificate is from a public CA (e.g. Let's Encrypt), clients can use
sslmode=requireorverify-full. - If you use a private CA or a self-signed certificate, clients either pass the CA with
sslrootcert=/path/to/ca.pemfor verification, or usesslmode=require(encrypted, but the server identity isn't verified).
The certificate and key files are watched by modification time and reloaded on change, so cert-manager renewals apply without a restart. A failed reload keeps the previously loaded certificate in place and logs an error.
The server starts a separate TCP listener on that port, so you'll also need to expose it through your load balancer or network configuration alongside the main Qyra port. It requires a valid QYRA_LICENSE_KEY — see enterprise features.
The Postgres wire protocol handshake is StartTLS-style — the client sends a plaintext SSLRequest before upgrading — so a generic TLS-terminating load balancer (e.g. an L7 HTTPS proxy) cannot front this endpoint. Terminate TLS at Qyra by giving the backend the cert and key. If you must front it, use a Postgres-aware proxy (e.g. PgBouncer, HAProxy in mode tcp) and set PGWIRE_SSL_MODE=disabled on the Qyra side so it accepts plaintext from the proxy on a trusted network.
Rejecting plaintext clients
With TLS required (the default), a client that connects with sslmode=disable is rejected before authentication — no password is ever prompted for, so Qyra tokens cannot be leaked over cleartext:
psql: error: connection to server ... failed:
ERROR: connection requires TLS
SQLSTATE: 28000Update client connection strings to sslmode=require (or stronger) to fix this.
Limitations
- No explicit joins, subqueries, or CTEs. Queries select from a single explore; joins are defined in the semantic layer.
- No DML. The API is read-only —
SELECTqueries only. - No custom metrics or period-over-period comparisons.
- Text and common binary types only. Both the simple and the extended query protocol (prepared statements, bind parameters) are supported. Bound parameters and result columns can use text format for any type, and binary format for booleans, integers, floats, dates, timestamps and text — which covers what JDBC and psycopg drivers request by default. Other types in binary format return
0A000. - No
pg_catalogemulation. Schema browsers in GUI clients (e.g. DBeaver) won't populate their sidebar — queryinformation_schema.tablesandinformation_schema.columnsinstead.
If you're working in Python, the Qyra Python SDK offers a typed, dataframe-native way to query the semantic layer without writing SQL.