Skip to main content

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.

Data-owner scope

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 Forbidden for 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:

FieldTypeDescription
querystringThe GraphQL operation document.
variablesobjectVariable values referenced by the operation. Send {} when the operation takes none.

Headers

HeaderRequiredValue
Content-TypeYesapplication/json
AuthorizationOne of these twoBearer oat_YOUR_API_TOKEN — for an organization API token.
x-oauth-access-tokenOne of these twoYOUR_AUTHENTIK_ACCESS_TOKEN — for an Authentik OAuth access token.
x-organization-idRecommendedThe numeric organization ID to scope the query to.
Match the header to the token kind

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:

  1. Authentication. The caller is authenticated against the api and oauth guards. If neither succeeds, the request fails with 401 Unauthorized.

  2. Organization resolution. The endpoint resolves the current organization in this order:

    • the x-organization-id header — 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.

  3. 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 a 403 here, 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.

Pin the organization explicitly

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:

LimitValue
Max query depth8
Max field selections per query500
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 preset for a rolling window with a sensible time-bucket granularity (this granularity drives the timePeriod buckets in the aggregate queries).
  • Pass start / end for a custom range (monthly bucket granularity).
  • Omit period entirely 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.

SubjectList queryAggregate queryWhat it covers
Dataset access / execution historyexecutionHistoryListexecutionHistoryAggregateEach time one of your datasets was accessed (incl. via MCP), with access counts and royalties.
Cluster proxy callsproxyCallLogListproxyCallLogAggregateHTTP calls proxied to clusters your organization owns, with status and latency.
External API / connector callsexternalApiCallLogListexternalApiCallLogAggregateCalls to external-API connectors you publish, with billing units and latency.

Pagination

The …List queries are cursor-paginated:

  • Pass first for the page size and after for the opaque cursor of the previous page.
  • Each result carries pageInfo { hasNextPage endCursor }. To fetch the next page, send the endCursor back as after.

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:

Conditionextensions.codeHTTP status
No / invalid credential, or no resolvable organizationUNAUTHORIZED401
Organization owns no datasets (not a data owner)FORBIDDEN403
Query exceeds the depth or field-count limit(validation error)400
Treat 403 as "no data," not failure

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