Skip to content
AtheronDEVELOPERS
← SDKs and libraries

Elixir SDK

Install and use aton_network.

Sign in to see your own test client ID in the samples.

Coming soon. aton_network is not published on Hex yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from Endpoints.

atheron is Sign in with Atheron for Elixir: PKCE, the authorize address, the code swap, userinfo, webhook signatures, the events stream and the developer API client, on Elixir 1.18 and Erlang/OTP's own :httpc. Plug and Phoenix get routes and plugs.

Install

Every call checks TLS against the system's certificates, follows no redirects and has ten seconds. To route the SDK through your own HTTP client or a proxy, set config :aton_network, transport: MyTransport (a module with call/4, or a function of four arguments).

Functions answer {:ok, value} or {:error, %AtonNetwork.Error{}}, so they sit in a with.

elixir
def deps do
  [{:aton_network, "~> 0.1"}]
end

Sign somebody in

Build the address on your server and keep AtonNetwork.Started.kept(started) (the verifier, state and nonce) in their session.

# The authorize request is a page the person opens, not an API call. Build the address on your
# server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it.
VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n')
STATE=$(openssl rand -hex 16)
echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
Example response302
(no body: the browser follows the redirect)

Handle the callback

finish_sign_in checks state in constant time, then swaps the code. Send the secret from a server app; leave it out in a public app.

For a whole web app, AtonNetwork.Auth does both ends: begin/2 gives the flow to keep in the session and the address to send the browser to, and complete/3 checks the flow you took back out and answers the person and what to keep. Phoenix does the keeping for you.

# Swap the code from the redirect for tokens, on your server. HTTP Basic carries your client ID and
# secret; the verifier proves this is the browser that started the sign-in.
curl https://accounts.aton-network.org/oauth/token \
  -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri=https://app.example.com/callback \
  -d code_verifier="$VERIFIER"
Example response200
{
  "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "openid profile email offline_access",
  "id_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjYtMTAtMDEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmF0b24tbmV0d29yay5vcmciLCJzdWIiOiI5YTZk...",
  "refresh_token": "ath_rt_6Qm9..."
}

Read who it is now

# Who the person is as of now, with an access token issued to your client.
curl https://accounts.aton-network.org/oauth/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Example response200
{
  "sub": "9a6d4c1e-1f0b-4a51-8a8e-3b2f6c0d7e11",
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "email_verified": true,
  "age": { "verified": true, "adult": true, "sixteen_or_over": true }
}

Keep them signed in

# A new access token and a new refresh token for the newest refresh token you hold. The old one stops working.
curl https://accounts.aton-network.org/oauth/token \
  -u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN"
Example response200
{
  "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "openid profile email offline_access",
  "refresh_token": "ath_rt_k2Lp..."
}

Check a webhook

Pass the raw body, exactly as it arrived. In Phoenix, AtonNetwork.Plug.RawBody keeps it for you.

This example is not written in cURL yet. Showing TypeScript.

// Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received.
import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk";

export async function handleWebhook(request: Request): Promise<Response> {
  const raw = await request.text();
  const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!);
  if (!ok) return new Response("bad signature", { status: 400 });
  const event = JSON.parse(raw) as WebhookEvent;
  if (event.type === "consent.revoked") {
    // Delete the refresh token you hold for event.data.sub, and stop using what you read.
  }
  return new Response(null, { status: 204 });
}

Follow the events stream

AtonNetwork.Events.follow/3 reconnects when the connection drops and resumes after the last event it handed you; it stops only on a refusal that waiting cannot fix, such as a key without events:read, or when :stop says so. Run it in a Task or a GenServer of your own. See the events stream.

# Follow the events stream. -N keeps curl from buffering; pass the last id you handled to resume.
curl -N https://developers.aton-network.org/api/v1/events \
  -H "Authorization: Bearer $ATHERON_API_KEY" \
  -H "Accept: text/event-stream" \
  -H "Last-Event-ID: $LAST_EVENT_ID"
Example response200
retry: 3000

event: ready
data: {"types":null}

id: 5b1c2a8e-0000-4000-8000-000000000001
event: user.signed_in
data: {"id":"5b1c2a8e-0000-4000-8000-000000000001","type":"user.signed_in","created":"2026-10-15T17:02:11.000Z","client_id":"live-1a2b3c4d5e6f7a8b","data":{"sub":"9a6d4c1e-1f0b-4a51-8a8e-3b2f6c0d7e11","scopes":["openid","email"],"at":"2026-10-15T17:02:11.000Z"}}

: beat

GraphQL

AtonNetwork.GraphQL runs queries on :httpc: query(graphql, AtonNetwork.GraphQL.usage_query(), %{"days" => 30}). The documents the SDK ships (the App, Usage, Overview and Events documents the GraphQL reference publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. query answers {:error, {:field_errors, data, errors}} when any field failed, with the data that could be read; request answers {:ok, data, field_errors}. Use AtonNetwork.Events.follow/3 for events as they happen, or a WebSocket client of your own with ws_close_action/1, ws_error_action/1 and resume_variables/2.

This example is not written in cURL yet. Showing TypeScript.

// Your app and its usage in one GraphQL request, then its events as they happen over the subscription.
// Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package.
import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk";

export async function usageThisMonth(apiKey: string): Promise<number> {
  const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 });
  return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0;
}

export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise<string | null> {
  for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) {
    forget(event.data.sub);
    lastId = event.id; // keep it somewhere durable, to pass back after a restart
  }
  return lastId;
}

The developer API

# Your app as registered, for the environment this API key belongs to. Needs app:read.
curl https://developers.aton-network.org/api/v1/app \
  -H "Authorization: Bearer $ATHERON_API_KEY"
Example response200
{
  "name": "Price Check",
  "state": "live",
  "kind": "confidential",
  "quota": "free",
  "env": "live",
  "client_id": "live-1a2b3c4d5e6f7a8b",
  "redirect_uris": ["https://app.example.com/callback"],
  "scopes": ["openid", "profile", "email"],
  "approved_scopes": ["openid", "profile", "email"],
  "privacy_url": "https://app.example.com/privacy",
  "homepage_url": "https://app.example.com",
  "credentials": 2,
  "testers": 0,
  "webhooks": 1
}

Errors

Every failure is an %AtonNetwork.Error{} with code (for example invalid_grant), description, the HTTP status and, on a 429, retry_after. See Errors for every code.

Frameworks

Phoenix, which works the same way in any Plug app.

Ask Atlas

AI assistant

Ask anything about Sign in with Atheron, the API or the portal. You get an answer from our docs, with links to the pages it used.