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 request | 1 (no batching) |
| Cost per request | 25 (each field that reads our database costs 1, introspection 5) |
| Depth | 5 levels (15 inside introspection) |
| Fields selected | 300, aliases and fragments counted where they appear |
| Request size | 16 KB, 2,000 tokens, variables 4 KB |
| Subscriptions | 5 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.
| Code | Status | Why | What to do |
|---|---|---|---|
| invalid_key | 401 | No 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_right | beside data | The 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_found | beside data | The 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_limited | 429 | Over 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_request | 400 | The 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_complex | 400 | Over 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_found | 200 | A 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_error | beside data | Something 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
}