Querying Metrics with GraphQL
The platform exposes a GraphQL endpoint for reading usage metrics and access history about the data you own — how often your datasets are accessed, by whom, the royalties they earn, and the latency and billing of the API and connector traffic flowing through your clusters.
This is a separate surface from the REST API. It is not part of the REST OpenAPI reference: there is no generated schema page for it, and the query language is GraphQL rather than path-and-verb REST. Point your GraphQL client (or a plain fetch / curl) at a single endpoint and ask for exactly the fields you need.
This endpoint answers questions about data your organization owns. Every query is scoped to a single organization, and the endpoint refuses callers whose organization owns no datasets (see Authorization and org-scoping). It is intended for dashboards, reporting jobs, and billing reconciliation run by the data owner — not for end-user document search (use Search and Query for that).
Prerequisites
- A platform credential: either an organization API token (
oat_…, see Organization User Management) or an Authentik OAuth access token. - An organization that owns at least one dataset. The endpoint returns
403 Forbiddenfor organizations with zero datasets. - The organization ID you want to scope the query to, if your credential belongs to more than one organization.
The Endpoint
POST https://api.alien.club/graphql
All requests are POST, regardless of the operation. The body is the standard GraphQL JSON envelope:
| Field | Type | Description |
|---|---|---|
query | string | The GraphQL operation document. |
variables | object | Variable values referenced by the operation. Send {} when the operation takes none. |
Headers
| Header | Required | Value |
|---|---|---|
Content-Type | Yes | application/json |
Authorization | One of these two | Bearer oat_YOUR_API_TOKEN — for an organization API token. |
x-oauth-access-token | One of these two | YOUR_AUTHENTIK_ACCESS_TOKEN — for an Authentik OAuth access token. |
x-organization-id | Recommended | The numeric organization ID to scope the query to. |
The platform runs two auth guards. An API token (oat_…) must be sent as Authorization: Bearer … (the api guard). An Authentik access token must be sent in x-oauth-access-token (the oauth guard). Sending an oat_ token in x-oauth-access-token fails JWT decoding and returns 401. Send one or the other, in the matching header.
A minimal request:
curl -X POST "https://api.alien.club/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer oat_YOUR_API_TOKEN" \
-H "x-organization-id: 77" \
-d '{
"query": "query { executionHistoryList(first: 5) { nodes { id datasetId count accessedAt } } }",
"variables": {}
}'
Authorization and org-scoping
Every request runs through an auth-and-scope gate before any resolver executes. Understanding it explains the responses you will see:
-
Authentication. The caller is authenticated against the
apiandoauthguards. If neither succeeds, the request fails with401 Unauthorized. -
Organization resolution. The endpoint resolves the current organization in this order:
- the
x-organization-idheader — used only if the caller is a member of that organization; - the user's stored current-organization, if they are a member of it;
- otherwise the user's first organization (oldest by creation date).
If none can be resolved, the request fails with
401 Unauthorized. - the
-
Dataset-ownership gate. The resolved organization must own at least one dataset. If it owns zero, the request fails with
403 Forbidden— the endpoint is for data owners. A consumer-only organization gets a403here, which clients typically treat as "no metrics to show" rather than a hard error.
All resolvers then read only rows belonging to the resolved organization. There is no way to read another organization's metrics: the org filter is applied server-side in every query, joined through the owning dataset, cluster, or connector.
If your credential belongs to more than one organization, always send x-organization-id. Without it the endpoint falls back to your current/first organization, which may not be the one whose metrics you intend to read.
Query limits
The endpoint enforces a few guardrails to keep queries cheap:
| Limit | Value |
|---|---|
| Max query depth | 8 |
| Max field selections per query | 500 |
Default page size (first) | 20 |
Max page size (first) | 100 |
A first value above the maximum is clamped to 100; it is not an error.
The period argument
The list and aggregate queries accept an optional period argument that windows the results by time. It is a PeriodInput object:
input PeriodInput {
preset: PeriodPreset # live | daily | weekly | monthly
start: DateTime # custom range start (ignored when preset is set)
end: DateTime # custom range end (ignored when preset is set)
}
enum PeriodPreset {
live # the last hour, minute granularity
daily # the last day, day granularity
weekly # the last week, week granularity
monthly # the last month, month granularity
}
- Pass a
presetfor a rolling window with a sensible time-bucket granularity (this granularity drives thetimePeriodbuckets in the aggregate queries). - Pass
start/endfor a custom range (monthly bucket granularity). - Omit
periodentirely to default to the last 30 days.
Query catalogue
There are three subjects, each with a …List query (paginated raw rows) and a …Aggregate query (grouped roll-ups). All are read-only.
| Subject | List query | Aggregate query | What it covers |
|---|---|---|---|
| Dataset access / execution history | executionHistoryList | executionHistoryAggregate | Each time one of your datasets was accessed (incl. via MCP), with access counts and royalties. |
| Cluster proxy calls | proxyCallLogList | proxyCallLogAggregate | HTTP calls proxied to clusters your organization owns, with status and latency. |
| External API / connector calls | externalApiCallLogList | externalApiCallLogAggregate | Calls to external-API connectors you publish, with billing units and latency. |
Pagination
The …List queries are cursor-paginated:
- Pass
firstfor the page size andafterfor the opaque cursor of the previous page. - Each result carries
pageInfo { hasNextPage endCursor }. To fetch the next page, send theendCursorback asafter.
Dataset access — executionHistoryList / executionHistoryAggregate
executionHistoryList returns one node per recorded access event against your datasets.
query DashboardExecutionHistory($first: Int, $after: String, $period: PeriodInput) {
executionHistoryList(first: $first, after: $after, period: $period) {
nodes {
id
count
source
proxyPath
sourceType
mcpSessionId
mcpRequestId
datasetId
entryId
jobId
organizationId
userId
accessedAt
}
pageInfo {
hasNextPage
endCursor
}
}
}
Example variables:
{ "first": 5, "period": { "preset": "monthly" } }
Example response:
{
"data": {
"executionHistoryList": {
"nodes": [
{
"id": 1042,
"count": 3,
"source": "mcp",
"proxyPath": "/datacluster/mcp",
"sourceType": "mcp",
"mcpSessionId": "sess_8f2c…",
"mcpRequestId": "req_19ab…",
"datasetId": 12,
"entryId": 884,
"jobId": null,
"organizationId": 77,
"userId": 305,
"accessedAt": "2026-06-21T14:08:55.000Z"
}
],
"pageInfo": { "hasNextPage": true, "endCursor": "MTA0Mg==" }
}
}
}
executionHistoryAggregate rolls the same events up by a dimension. groupBy is required and accepts dataset, consumer, time, or entry:
query ExecutionRoyalties($period: PeriodInput) {
executionHistoryAggregate(period: $period, groupBy: dataset) {
datasetId
totalRequests
totalRoyalties
distinctConsumers
lastAccessedAt
}
}
totalRoyalties is the summed count × dataset access price for the group — i.e. what that dataset earned in the window. When groupBy: consumer, consumerId is populated instead of datasetId; when groupBy: time, timePeriod carries the bucket start (granularity follows the period preset); when groupBy: entry, entryId is populated.
Cluster proxy calls — proxyCallLogList / proxyCallLogAggregate
Traffic proxied through clusters your organization owns. The list nodes expose method, proxyPath, responseStatus, responseTimeMs, clusterId, sourceType, the MCP correlation IDs, and calledAt.
query ProxyLatency($period: PeriodInput) {
proxyCallLogAggregate(period: $period, groupBy: time) {
timePeriod
totalRequests
distinctConsumers
avgResponseTimeMs
p95ResponseTimeMs
lastCalledAt
}
}
proxyCallLogAggregate.groupBy accepts cluster, time, status, or source_type. The aggregate rows carry the average and 95th-percentile response time for each group, alongside request and distinct-consumer counts.
External API / connector calls — externalApiCallLogList / externalApiCallLogAggregate
Calls to external-API connectors you publish. The list nodes add billing fields — billedUnits, unitPriceCents, totalPriceCents — to the method / path / status / latency shape, plus connectorId, endpointId, and consumerOrganizationId.
query ConnectorBilling($period: PeriodInput) {
externalApiCallLogAggregate(period: $period, groupBy: connector) {
connectorId
totalRequests
distinctConsumers
totalBilledUnits
totalPriceCents
avgResponseTimeMs
p95ResponseTimeMs
lastCalledAt
}
}
externalApiCallLogAggregate.groupBy accepts connector, endpoint, time, status, or source_type. The aggregate adds totalBilledUnits and totalPriceCents (in cents) so you can reconcile connector revenue per dimension.
Error handling
GraphQL responses use the standard envelope. A successful response carries data; a failed one carries an errors array. Note this is the GraphQL errors[] shape, distinct from the REST API's ErrorResponse body and from the streaming error envelope.
{
"errors": [
{
"message": "Forbidden",
"path": ["executionHistoryAggregate"],
"extensions": { "code": "FORBIDDEN", "http": { "status": 403 } }
}
]
}
Each entry has at least a message; resolver errors also carry path (the field that failed) and extensions (a machine-readable code and the mapped HTTP status). A response can contain both data and errors when only part of the selection failed — always check for a non-empty errors array, not just the HTTP status.
Common cases:
| Condition | extensions.code | HTTP status |
|---|---|---|
| No / invalid credential, or no resolvable organization | UNAUTHORIZED | 401 |
| Organization owns no datasets (not a data owner) | FORBIDDEN | 403 |
| Query exceeds the depth or field-count limit | (validation error) | 400 |
For consumer-flavoured callers, a 403 FORBIDDEN simply means the organization owns no datasets and therefore has no metrics to report. Dashboards typically catch this and render an empty state rather than an error.
Next Steps
- Search and Query — Read your documents (the consumer-facing surface)
- Organization User Management — Issue the API tokens used to authenticate these queries
- Security Model — Authentication and authorization architecture
- Platform API Overview — The REST surface and its error envelope