Skip to content
AtheronDEVELOPERS
GraphQL

GraphQL playground

The same API as REST, one request for what you need. Use a test key: it stays in this tab and goes nowhere but our API.

Where it lives

  • Queries: POST https://developers.aton-network.org/api/graphql
  • Subscriptions: wss://developers.aton-network.org/api/graphql/ws (graphql-transport-ws)
  • Schema, version 2026-10-15: schema.graphql
  • Events stream and webhooks: AsyncAPI description

Every field is a REST call too, with the same right and the same quota. A request takes its cost from your api quota, so asking for two things at once costs two.

Limits

Operations per request1 (no batching)
Cost per request25 (each field that reads our database costs 1, introspection 5)
Depth5 levels (15 inside introspection)
Fields selected300, aliases and fragments counted where they appear
Request size16 KB, 2,000 tokens, variables 4 KB
Subscriptions5 per socket; 10 event streams per app and environment over Server-Sent Events, and 10 over WebSockets

Errors

Every error has extensions.code, the same code REST answers where there is one.

CodeStatusWhyWhat to do
invalid_key401No API key was sent, or it is not one of ours, has expired or was revoked.Send a live key as Authorization: Bearer ath_test_... or ath_live_... (over WebSocket, in connection_init's payload).
missing_rightbeside dataThe key does not carry the right a field needs. That field is null; the rest of the answer is there.Make a key with the right (app:read, usage:read, events:read) in the portal, or ask only for what this key may read.
not_foundbeside dataThe application the key belongs to has been removed, or an events after id is not this app's or is older than 30 days.Use a key of an app that still exists; resume from an id you were sent within 30 days, or start without after.
rate_limited429Over your api quota for this minute or day. A request takes its cost from the quota; too many open event streams also answer this.Wait the seconds in Retry-After. Ask for fewer fields that reach the database, or fewer aliases.
invalid_request400The request could not run: not JSON, a batch (an array), a syntax error, a field or argument the schema does not have, more than one operation without operationName, or a subscription sent over HTTP.Check the message; the schema is at /graphql/schema.graphql. Send one operation per request; subscriptions go over the WebSocket.
query_too_complex400Over a limit: depth, fields selected (aliases and fragments counted where they appear), introspection depth, or cost.Split the request, or ask for less in one go. The limits are listed on this page.
persisted_query_not_found200A persisted query's hash is not one we know yet (message PersistedQueryNotFound).Send the same request again with the query as well as the hash; it is then kept, and the hash alone works.
server_errorbeside dataSomething went wrong at our end. Nothing about it is shown.Try again in a moment. If it keeps happening, tell us on the support page with the time.

Schema

"""
Atheron developer API, GraphQL, version 2026-10-15. A field is never removed or changed in a way that breaks a client without first being marked @deprecated, with the date and what replaces it, at least six months before.
"""
schema {
  query: Query
  subscription: Subscription
}

"""The developer API, version 2026-10-15. Every field is a REST call too."""
type Query {
  """
  Your application as registered, for this key's environment. Needs app:read. Same as GET /api/v1/app.
  """
  app: App

  """
  Calls, errors and latency per day and bucket, for this key's environment. Needs usage:read. Same as GET /api/v1/usage.
  """
  usage(
    """1 to 90; anything else is brought within."""
    days: Int = 7
  ): Usage
}

"""
Your application as registered, for the environment the API key belongs to. The same fields as GET /api/v1/app.
"""
type App {
  """The name people see on the consent screen."""
  name: String!

  """Where the app is: development, review or live."""
  state: String!

  """confidential (a server that keeps a secret) or public (PKCE only)."""
  kind: String!

  """The quota tier: free, growth, scale or partner."""
  quota: String!

  """The environment of this key."""
  env: Environment!

  """The OAuth client ID for this environment."""
  clientId: String!

  """The redirect addresses registered for this environment."""
  redirectUris: [String!]!

  """The scopes this environment's client may ask for."""
  scopes: [String!]!

  """The scopes review approved for live."""
  approvedScopes: [String!]!

  """Your privacy policy, shown on the consent screen."""
  privacyUrl: String

  """Your homepage, shown on the consent screen."""
  homepageUrl: String

  """Client secrets and certificates in force."""
  credentials: Int!

  """Named testers on the test client."""
  testers: Int!

  """Webhooks for this environment."""
  webhooks: Int!
}

"""Which of the app's two clients a key belongs to."""
enum Environment {
  """The test client: you, your team and up to 25 named testers."""
  TEST

  """The live client, after review."""
  LIVE
}

"""
Calls, errors and latency per day and bucket. The same answer as GET /api/v1/usage.
"""
type Usage {
  """The environment of this key."""
  env: Environment!

  """How many days the answer covers, 1 to 90."""
  days: Int!

  """One row per day and bucket, oldest first."""
  usage: [UsageDay!]!
}

"""One day and bucket of calls. Days are UTC."""
type UsageDay {
  """The day, YYYY-MM-DD, UTC."""
  day: String!

  """token, userinfo, userinfo:sensitive, api, sandbox or sign-in."""
  bucket: String!

  """Requests counted. A GraphQL request counts as its cost."""
  calls: Int!

  """Of those, how many failed."""
  errors: Int!

  """Mean time taken, in milliseconds."""
  msAvg: Int

  """Longest time taken, in milliseconds."""
  msMax: Int!
}

"""
Events as they happen, over WebSocket (graphql-transport-ws). The same events as GET /api/v1/events and your webhooks.
"""
type Subscription {
  """
  Events for this key's app and environment. Needs events:read. Same as GET /api/v1/events.
  """
  events(
    """Only these kinds. All kinds when left out."""
    types: [EventType!]

    """Resume after this event id (kept 30 days)."""
    after: ID
  ): Event!
}

"""
One event, with the same five fields a webhook delivery's body carries.
"""
type Event {
  """The event's id. Send it back as after to resume."""
  id: ID!

  """The kind of event."""
  type: EventType!

  """When it was raised, ISO 8601."""
  created: String!

  """The client it concerns."""
  clientId: String!

  """What happened."""
  data: EventData!
}

"""The kinds of event, as webhooks name them."""
enum EventType {
  USER_SIGNED_IN
  CONSENT_REVOKED
  MEMBERSHIP_CHANGED
  PING
}

"""
What happened. Which fields are present depends on the type, exactly as in a webhook's data.
"""
type EventData {
  """The person, as the sub claim your app was given. Absent for ping."""
  sub: ID

  """user.signed_in: the scopes of this sign-in."""
  scopes: [String!]

  """When it happened, ISO 8601."""
  at: String
}