Skip to content
Aggre Docs

Loading…

Console

API reference

Accounts API

The control-plane API behind the console: browser contract, sessions, login flow, roles and handles.

https://accounts.aggre.org is the control plane: sign-in, personal accounts and organizations, invitations, API-key management and usage views. It is what the console calls. You do not need it to read data (that is the data API, which takes API keys); you need it only if you build your own console or automate account management from a browser session.

Data APIAccounts API
Hostapi.aggre.orgaccounts.aggre.org
CredentialAPI key (Authorization: Bearer aggre_sk_...)Session cookie
Browser access (CORS)noneonly https://app.aggre.org and https://admin.aggre.org
Accepts the other credential?nono: it never accepts an API key

The route list with parameters is generated in the Accounts API reference; the OpenAPI file is /openapi/accounts.yaml. Platform-staff routes (/v1/admin/*) are not documented here.

Browser contract#

  1. Every call is fetch(url, { credentials: "include" }) from the browser. The cookies belong to accounts.aggre.org, so a server on another host never sees them: logged-in data is fetched in the browser, not server-rendered.
  2. Every non-GET request needs all three: the Origin the browser sends (it must be exactly an allowed app origin), the header X-Aggre-CSRF: 1 (any non-empty value), and Content-Type: application/json when there is a body. Missing or wrong: 403 csrf_required or 403 origin_not_allowed.
  3. Every authenticated request, GET included, must carry an allowed Origin; one without is treated as unauthenticated (401).
  4. CORS: exact match on the configured origins, Access-Control-Allow-Credentials: true, allowed headers Content-Type, X-Aggre-CSRF, methods GET, POST, PATCH, DELETE, OPTIONS. No wildcard. Responses carry Vary: Origin and Cache-Control: no-store.
  5. Cookies are HttpOnly; Secure; SameSite=Lax, host-only, with the __Host- prefix: __Host-aggre_app for the app audience and __Host-aggre_admin for the admin audience. They are never interchangeable.
  6. Success bodies are { "data": ... }; a secret or token is a sibling key (secret, token) and appears once. Errors use the shared envelope (Errors).
  7. Times are RFC 3339 UTC, ids are UUID strings, handles are lowercase.

Signing in#

Login is a full-page navigation, not fetch:

text
https://accounts.aggre.org/login/{provider}?audience=app&return_to=https%3A%2F%2Fapp.aggre.org%2F

provider is one of the ids returned by GET /v1/config (for example google, github, orbbit). return_to must be an absolute URL on the audience's own origin; anything else is replaced by that origin's root. The browser comes back to return_to with the session cookie set, or to {origin}/login?error=<code> with one of:

errorMeaning
access_deniedThe person cancelled at the provider.
invalid_stateThe login state is missing, expired, reused, or the browser finishing the login is not the one that started it.
provider_errorThe provider rejected the code, returned an invalid token or was unreachable.
account_disabledThe account is suspended.
not_staffThe admin audience was requested by a user who is not platform staff.
identity_in_useLinking a provider identity that already belongs to another user.
session_requiredLinking without a valid session.
internalAnything else.

OIDC providers use authorization code with PKCE, state and nonce, and verify the id token's signature and claims. GitHub uses OAuth 2 with PKCE.

Who you become#

An identity is (provider, subject), never the e-mail address. A known identity logs you in as the same user. For a new identity, if the provider reports an e-mail it vouches for (a verified address) and an existing user's verified primary e-mail equals it, the identity is linked to that user (auto-merge); otherwise a new user and personal account are created. An unverified or absent e-mail never merges anything, and a later change of e-mail at the provider never moves an identity. You can link another provider while signed in (link=1) and unlink one as long as another remains.

Sessions#

A session is an opaque random token in the cookie; only its hash is stored. App sessions live 30 days from login (not sliding) and die after 14 days without use; admin sessions live 8 hours. At most 20 live sessions per user and audience: a login beyond that revokes the oldest. POST /v1/auth/logout revokes the current session and POST /v1/auth/logout-all all of them. A suspended account cannot sign in and all its sessions stop working.

Roles#

On a personal account you are the owner. An organization has owner, admin and member, always with at least one owner. Details of who can do what are in Organizations. A caller with no relation to an account gets 404 not_found (existence is not leaked); one with a relation but too little role gets 403 forbidden.

Handles#

Users and organizations share one namespace of handles: 3 to 39 characters of a-z, 0-9 and -, not starting or ending with -, no --. Reserved words (admin, api, app, docs, login, settings, ...) are refused. The first rename of an account is free; after that, once per 30 days (409 conflict with the next date). GET /v1/handles/{handle} checks availability.

Rate limits#

Besides the per-IP limit on every route, the login routes share a tighter per-IP limit; over it the answer is 429 rate_limited with Retry-After.

Source: content/api/accounts.md