ExtremeXCLIENT
πŸ“– Developer Documentation

ExtremeX ONE β€” Developer Documentation

One integration gives your application secure sign-in, verified identity, a consent screen you design, and an encrypted per-user data store β€” with live analytics on your dashboard. Everything you need is on this page: the platform concepts, the official libraries with full class references, platform-by-platform integration, guides, use cases, and the raw HTTP reference underneath it all.

Start here

Overview

There are three equivalent ways to talk to the platform β€” pick per component, mix freely; they all speak the same protocol:

Quickstart

  1. Sign in to this portal with your ExtremeX ONE account and claim your namespace β€” your company's reverse domain, e.g. com.yourco.
  2. Register an application (service name, display name, app type). Pick Web app / website if it runs its own browser sign-in and add a redirect URL; pick Other for a mobile, desktop or backend/service app and skip the redirect URL for now β€” add one later from Credentials if you ever wire up a browser flow. You receive a client_id and a client_secret β€” the secret is shown once, store it in a secrets manager.
  3. Configure Data access: which identity fields you request, each tagged required or optional, with the reason users will read.
  4. Integrate: add the library (Android/JVM), the JS SDK (web), or call the endpoints directly.
  5. Watch sign-ins, grant rates and connected users live on your dashboard.

Core concepts

  • Namespace β€” your reverse domain (com.yourco). All app IDs live under it. It can be renamed from the dashboard (stored data and consents migrate automatically), but every app ID under it changes β€” treat it as stable.
  • App ID β€” namespace.service, e.g. com.yourco.iwish. Also the namespace for your app's user data.
  • App type β€” Web app / website apps run their own browser-redirect sign-in and must register at least one redirect URL. Other covers mobile, desktop and backend/service apps that don't run /oauth/authorize themselves (e.g. a worker that only reads/writes App data with credentials another app obtained) β€” a redirect URL is optional and can be added later from the Credentials tab.
  • Client ID / secret β€” OAuth credentials. The secret is stored hashed on our side and must live only on your server; rotate it any time from the Credentials tab.
  • Data fields β€” the identity your app requests: first_name, last_name, avatar, email, date_of_birth (+ 18+ status), phone, address, work_email, work_phone, work_address, nationality, country_of_birth, languages, attributes (app data), offline (refresh token). Each is required or optional, with your stated reason.
  • Consent β€” checked live on every API call. Users can withdraw optional fields (or disconnect entirely) from their ONE account at any time; your code must tolerate absent fields.
  • App data β€” an encrypted per-user key-value store scoped to your app ID. Variables you declare are type-checked on write.
  • Tokens β€” access tokens live 1 hour; refresh tokens (with offline) rotate on every use and die on revocation.
  • Team β€” invite other ONE accounts to co-manage your workspace with scoped permissions and an access duration.

Sign-in

OAuth 2.0 flow

ExtremeX ONE implements the standard authorization-code grant:

Example
1. Browser  β†’  https://one.extremextechnology.com/oauth/authorize
              ?client_id=exone_xxxxxxxx
              &redirect_uri=https://app.yourco.com/auth/callback
              &state=<random-anti-csrf-value>

2. User signs in (if needed) and reviews the consent screen:
   your required fields are locked on, optional ones can be unticked.
   The user presses Proceed.

3. Browser  ←  302 https://app.yourco.com/auth/callback?code=…&state=…

4. Your server  β†’  POST https://api.extremextechnology.com/one/oauth/token   (exchange, server-side only)

5. Your server  β†’  GET https://api.extremextechnology.com/one/oauth/userinfo (Bearer access_token)

Two rules are non-negotiable: verify state matches the value you generated, and perform the code exchange from your backend β€” the client secret never ships to a browser or a mobile app.

Libraries

Install from our Maven repository

The official ExONE libraries are served from this domain as a Maven repository. They are pure Java 8 bytecode with zero dependencies (JSON and HTTP are built in) β€” the same artifacts run on Android (all API levels), Kotlin/JVM, and desktop or server Java. Sources JARs are published, so IDE navigation and step-debugging work out of the box.

Gradle
// settings.gradle.kts  (Android Studio / Gradle, Kotlin DSL)
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://client.extremextechnology.com/repo") }
    }
}

// app/build.gradle.kts
dependencies {
    implementation("com.extremex.one:exone-auth:1.2.0")
    implementation("com.extremex.one:exone-data:1.2.0")
}
Gradle
// Groovy DSL
repositories { maven { url "https://client.extremextechnology.com/repo" } }
dependencies {
    implementation "com.extremex.one:exone-auth:1.2.0"
    implementation "com.extremex.one:exone-data:1.2.0"
}
XML Β· Maven
<!-- Maven -->
<repositories>
  <repository><id>extremex</id><url>https://client.extremextechnology.com/repo</url></repository>
</repositories>
<dependency>
  <groupId>com.extremex.one</groupId><artifactId>exone-auth</artifactId><version>1.2.0</version>
</dependency>
<dependency>
  <groupId>com.extremex.one</groupId><artifactId>exone-data</artifactId><version>1.2.0</version>
</dependency>

exone-data depends on exone-auth; Gradle/Maven resolves that automatically. All network calls are blocking β€” on Android run them off the main thread, or use the built-in …Async variants (callbacks arrive on a background thread; hop to the main thread before touching views).

exone-auth β€” class reference

Package com.extremex.one.auth (exceptions and callbacks in com.extremex.one). Handles the entire sign-in lifecycle: building the authorize URL, exchanging the code, refreshing tokens, fetching the profile, and keeping a session alive.

ExOneAuth

The stateless client, built once and reused. Thread-safe.

MemberWhat it does
ExOneAuth.builder(clientId)Start configuring a client.
.redirectUri(uri)Must exactly match a redirect URL registered for your app.
.clientSecret(secret)Server-side only β€” enables exchangeCode/refresh. Never set inside a shipped app.
.authBase(url) / .apiBase(url)Override endpoints (defaults: one.extremextechnology.com / api…/one).
.timeouts(connectMs, readMs)Network timeouts; defaults 15 s / 20 s.
static newState()Cryptographically random anti-CSRF state β€” store it, verify it in the callback.
authorizeUrl(state)The URL to open in the (system) browser to start sign-in.
exchangeCode(code) β†’ ExOneTokensTrade the callback code for tokens. Server-side.
refresh(refreshToken) β†’ ExOneTokensRotate a refresh token for a fresh pair. Persist the NEW refresh token.
userinfo(accessToken) β†’ ExOneUserThe profile, filtered by live consent.
exchangeCodeAsync / refreshAsync / userinfoAsyncSame calls with an ExOneCallback, run on a background thread.

ExOneTokens

MemberWhat it does
getAccessToken()Bearer token for API calls; lives 1 hour.
getRefreshToken()Present only when the user granted β€œStay signed in” (offline).
getExpiresIn() / getScope()Lifetime in seconds; space-separated granted fields.
isExpired()Local check with a 30 s safety margin.
static restore(access, refresh)Rebuild from your storage; counts as expired so a session refreshes it on first use.

ExOneUser

MemberWhat it does
getSub()Stable unique user id β€” always present. Key your accounts on this, never on email.
getFirstName() / getLastName()The two halves of the user's name, each granted separately.
getName()Convenience: first and last joined with a space, skipping whichever half wasn't granted.
getAvatarUrl() / getEmail()Null when the user declined that field β€” code defensively.
getDateOfBirth() / getAgeStatus()ISO yyyy-MM-dd, and one of minor | adult_unverified | adult_verified.
isAdult() / isAgeVerified()18+ shorthand over getAgeStatus(); the verified form only once checked against a document.
getPhone() / isPhoneVerified()Phone number and whether it has been verified.
getAddress() / getWorkAddress()Map of the address; keys depend on the country (always country, then that format's fields).
getWorkEmail() / getWorkPhone()Optional work contact details.
getNationality() / getCountryOfBirth()Optional profile fields, null when not granted.
getLanguages()Immutable List of languages the user speaks; empty when not granted.
getRaw()The full userinfo payload as an immutable Map.

ExOneSession β€” sessions that keep themselves alive

Wraps the client + tokens and transparently refreshes the access token when it expires. Thread-safe (concurrent callers share one refresh). Because refresh tokens rotate on every use, attach a Store so rotated tokens land back in your persistence β€” otherwise the session dies on restart.

MemberWhat it does
new ExOneSession(auth, tokens, store)Start from a fresh exchangeCode() result.
static restore(auth, access, refresh, store)Resume after a restart from persisted tokens.
accessToken()A currently-valid token, refreshing first when needed.
userinfo() / userinfoAsync(cb)Profile fetch that retries once through a refresh on 401.
canRefresh()True while the session can keep itself alive.
invalidate()Drop the session locally (server-side revocation is the user's, from their ONE account).
interface Store { save(access, refresh) }Called after every successful refresh β€” persist both values.

ExOneException & ExOneCallback

MemberWhat it does
getCode()Machine-readable code: invalid_grant, access_revoked, insufficient_scope, network_error, session_expired, …
getStatus()HTTP status; 0 when the request never reached the server.
isAccessRevoked()True β†’ the user disconnected you: wipe stored tokens, re-run sign-in.
ExOneCallback<T> { onSuccess(T); onError(ExOneException); }Result handler for every …Async method.

Putting it together (Kotlin, server-side handler)

Kotlin
val auth = ExOneAuth.builder(env("EXONE_CLIENT_ID"))
    .clientSecret(env("EXONE_CLIENT_SECRET"))
    .redirectUri("https://app.yourco.com/auth/callback")
    .build()

// GET /auth/login
val state = ExOneAuth.newState()
cookies.set("oauth_state", state); redirect(auth.authorizeUrl(state))

// GET /auth/callback?code=…&state=…
require(params["state"] == cookies["oauth_state"]) { "state mismatch" }
val tokens = auth.exchangeCode(params["code"]!!)
val user = auth.userinfo(tokens.accessToken)
val account = accounts.findOrCreateBySub(user.sub)   // stable id
sessionFor(account, tokens)

exone-data β€” class reference

Package com.extremex.one.data. The per-user app data store: values are encrypted at rest on the platform and decrypted only for your app's authenticated requests. Requires the App data (attributes) field granted. Variables declared in your app's Create data tab are type-checked server-side on write.

ExOneData

MemberWhat it does
new ExOneData(accessToken)Simplest form β€” bind to one access token.
new ExOneData(session)Bind to an ExOneSession: data calls never go stale (recommended).
new ExOneData(tokenProvider)Plug in your own token logic via TokenProvider { accessToken() }.
list() β†’ Map<String,Object>Every key this user has for your app, decrypted.
read(key) β†’ ObjectOne value, or null if never written.
readString / readNumber / readDouble / readBoolean(key)Typed convenience readers β€” null when absent or a different type.
readMap / readList(key)JSON object / array values as Map / List.
write(key, value) Β· write(map)Upsert one value or up to 100 in one request. Declared variables are type-checked (400 Type mismatch).
increment(key, delta) β†’ longRead-modify-write counter helper (missing key counts as 0). Per-user, so races are rare; not atomic.
remove(key)Delete one key.
listAsync / readAsync / writeAsync / removeAsync(…, cb)Async variants with ExOneCallback on a background thread.

Example (Kotlin)

Kotlin
val session = ExOneSession.restore(auth, stored.access, stored.refresh) { a, r ->
    stored.save(a, r)                         // rotated tokens β†’ back to disk
}
val data = ExOneData(session)

data.write(mapOf("theme" to "dark", "level" to 3))
val streak = data.increment("streak", 1)      // 0 β†’ 1 β†’ 2 … per user
val prefs = data.readMap("preferences")       // null-safe typed readers

data.listAsync(object : ExOneCallback<Map<String, Any?>> {
    override fun onSuccess(all: Map<String, Any?>) = runOnUiThread { render(all) }
    override fun onError(e: ExOneException) {
        if (e.isAccessRevoked) signOutLocally()   // user disconnected the app
    }
})

Versions & downloads

VersionHighlightsArtifacts
1.2.0 latestFull identity profile on ExOneUser β€” first/last name, date of birth with 18+ status, phone, home & work address, nationality, country of birth, languages. getUsername() is gone (ONE no longer has usernames) and getName() is now computed from the two name fields.auth.jar Β· sources Β· data.jar Β· sources
1.1.0ExOneSession auto-refresh with rotation-safe Store, async variants of every call, typed readers (double/map/list), increment(), configurable timeouts, ExOneTokens.restore().auth.jar Β· data.jar
1.0.0Initial release: code exchange, refresh, userinfo; data list/read/write/remove.auth.jar Β· data.jar

Repository root: https://client.extremextechnology.com/repo Β· group com.extremex.one. Every artifact ships with .md5/.sha1/.sha256 checksums and maven-metadata.xml, so Gradle's dependency verification works unchanged.

Platforms

JavaScript SDK (web front-ends)

HTML / JS
<script src="https://api.extremextechnology.com/one/sdk/exone.js"></script>
<script>
  exONE.init({
    clientId: "exone_xxxxxxxxxxxx",
    redirectUri: "https://app.yourco.com/auth/callback",
  });

  exONE.auth.login();                                  // start sign-in anywhere

  // After your backend exchanged the code:
  exONE.auth.setTokens({ access_token, refresh_token });

  await exONE.data.write({ high_score: 9001 });        // per-user app data
  const all = await exONE.data.list();
</script>

Mobile & native β€” the three rules

Apps that ship to user devices are public clients β€” an APK or IPA can be decompiled, so the client secret must never be embedded in them. On every platform below, the pattern is the same:

  1. Open the authorize URL in the system browser (Custom Tabs / ASWebAuthenticationSession), never an embedded WebView β€” users must see the real one.extremextechnology.com origin.
  2. Receive the callback on an HTTPS app link / universal link registered as a redirect URL. Custom schemes like yourapp:// are not accepted.
  3. Send the code to your backend, which holds the secret, performs the exchange, and returns your own session (or the tokens) to the app.

The exone-auth library enforces rule 3 for you: token calls throw unless a client secret is configured, which by design only happens server-side.

Java (server-side)

With the library (recommended):

Example
ExOneAuth auth = ExOneAuth.builder(System.getenv("EXONE_CLIENT_ID"))
    .clientSecret(System.getenv("EXONE_CLIENT_SECRET"))
    .redirectUri("https://app.yourco.com/auth/callback")
    .build();

ExOneTokens tokens = auth.exchangeCode(code);
ExOneUser user = auth.userinfo(tokens.getAccessToken());
String sub = user.getSub();   // stable id β€” key your accounts on this

Or raw HTTP with java.net.http if you prefer no dependency β€” the token reference below has the exact payloads.

Kotlin / Android

Launch the consent screen in a Chrome Custom Tab and catch the app-link callback:

Kotlin
// build.gradle.kts: implementation("androidx.browser:browser:1.8.0")
//                    implementation("com.extremex.one:exone-auth:1.2.0")

val auth = ExOneAuth.builder(BuildConfig.EXONE_CLIENT_ID)   // id only β€” never the secret
    .redirectUri("https://app.yourco.com/auth/callback")
    .build()
val state = ExOneAuth.newState()
prefs.edit { putString("oauth_state", state) }
CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(auth.authorizeUrl(state)))

// AndroidManifest.xml β€” the app link that brings the user back:
// <intent-filter android:autoVerify="true">
//   <action android:name="android.intent.action.VIEW" />
//   <category android:name="android.intent.category.DEFAULT" />
//   <category android:name="android.intent.category.BROWSABLE" />
//   <data android:scheme="https" android:host="app.yourco.com" android:path="/auth/callback" />
// </intent-filter>

// In the activity that receives the link:
val code = intent.data?.getQueryParameter("code")
val returned = intent.data?.getQueryParameter("state")
if (code != null && returned == prefs.getString("oauth_state", null)) {
    api.completeLogin(code)   // YOUR backend exchanges it and returns your app session
}

Jetpack Compose

Kotlin Β· Compose
@Composable
fun SignInWithOneButton(viewModel: AuthViewModel) {
    val context = LocalContext.current
    Button(onClick = { viewModel.startLogin(context) }) {
        Text("Sign in with ExtremeX ONE")
    }
}

class AuthViewModel(private val api: BackendApi) : ViewModel() {
    val session = MutableStateFlow<Session?>(null)

    fun startLogin(context: Context) = launchAuthorize(context)  // Custom Tab (above)

    /** Called from the deep-link activity with the returned code. */
    fun completeLogin(code: String) = viewModelScope.launch(Dispatchers.IO) {
        session.value = api.completeLogin(code)   // backend exchanges code β†’ session
    }
}

Kotlin Multiplatform (KMP)

On JVM/Android targets use the library directly from commonMain via expect/actual, or keep flow logic common with Ktor; only opening the browser is platform-specific:

Swift
// commonMain
class OneAuth(private val client: HttpClient, private val backend: String) {
    fun authorizeUrl(state: String) =
        "https://one.extremextechnology.com/oauth/authorize" +
        "?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&state=$state"

    suspend fun completeLogin(code: String): Session =
        client.post("$backend/auth/exchange") {   // your server does the secret exchange
            contentType(ContentType.Application.Json)
            setBody(mapOf("code" to code))
        }.body()
}

// expect/actual β€” the only per-platform piece:
expect fun openBrowser(url: String)
// androidMain β†’ CustomTabsIntent.launchUrl(...)
// iosMain     β†’ ASWebAuthenticationSession (Swift section below)
// desktopMain β†’ Desktop.getDesktop().browse(URI(url))

Flutter

Swift
// pubspec.yaml: flutter_web_auth_2: ^4.0.0  (Custom Tabs / ASWebAuthenticationSession)

final state = base64UrlEncode(List<int>.generate(16, (_) => Random.secure().nextInt(256)));

final result = await FlutterWebAuth2.authenticate(
  url: Uri.parse('https://one.extremextechnology.com/oauth/authorize').replace(queryParameters: {
    'client_id': clientId,                       // id only β€” the secret stays server-side
    'redirect_uri': 'https://app.yourco.com/auth/callback',
    'state': state,
  }).toString(),
  callbackUrlScheme: 'https',                    // app link / universal link
);

final uri = Uri.parse(result);
if (uri.queryParameters['state'] != state) throw Exception('state mismatch');

final session = await http.post(                 // YOUR backend exchanges the code
  Uri.parse('https://api.yourco.com/auth/exchange'),
  headers: {'Content-Type': 'application/json'},
  body: jsonEncode({'code': uri.queryParameters['code']}),
);

Swift / iOS

Swift
import AuthenticationServices

let state = UUID().uuidString
var comps = URLComponents(string: "https://one.extremextechnology.com/oauth/authorize")!
comps.queryItems = [
    .init(name: "client_id", value: clientId),   // id only β€” never ship the secret
    .init(name: "redirect_uri", value: "https://app.yourco.com/auth/callback"),
    .init(name: "state", value: state),
]

let session = ASWebAuthenticationSession(
    url: comps.url!,
    callback: .https(host: "app.yourco.com", path: "/auth/callback")  // universal link
) { callbackURL, error in
    guard let url = callbackURL,
          let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems,
          items.first(where: { $0.name == "state" })?.value == state,
          let code = items.first(where: { $0.name == "code" })?.value
    else { return }
    Task { let session = try await backend.completeLogin(code: code) }
}
session.presentationContextProvider = self
session.start()

Other platforms

  • React Native / Expo β€” expo-auth-session or react-native-app-auth pointed at the authorize URL; exchange the code on your backend exactly as above.
  • Desktop (Electron, Tauri, JavaFX, .NET) β€” open the authorize URL in the default browser and listen on http://localhost:<port>/callback (accepted for development); production desktop apps should still exchange through your backend. JavaFX/Swing apps can use the library directly.
  • Server-rendered web (PHP, Django, Rails, Go, …) β€” confidential clients: run the whole flow server-side; any HTTP client works.
  • CLI tools β€” print the authorize URL, listen on localhost, exchange via your backend or (internal tools) with the secret from the machine's environment.

App data

App data β€” API & encryption

With the attributes field granted, your app gets a private key-value store per user, namespaced to your app ID β€” no application can read another's keys. Values are encrypted at rest (AES-256-GCM, a key derived per app) and decrypted only for your app's requests carrying an authenticated user's token with live consent β€” not for the portal, not for other apps, not for a database dump.

HTTP
GET    https://api.extremextechnology.com/one/data            β†’ list all keys (decrypted)
GET    https://api.extremextechnology.com/one/data?key=k      β†’ read one key
PUT    https://api.extremextechnology.com/one/data            β†’ { "data": { "k": v, … } } write/merge (≀100 keys)
DELETE https://api.extremextechnology.com/one/data?key=k      β†’ remove a key
(all with Authorization: Bearer <access_token>)

Typed variables (Create data)

In your app's Create data tab, declare the variables your app stores β€” a name, a type (string, number, boolean, date (ISO string), json), and a description. Writes to a declared variable are type-checked server-side and rejected with 400 Type mismatch on violation; undeclared keys stay free-form. Declare your schema early β€” it turns a whole class of client bugs into loud development-time errors instead of silent bad data.

Guides

Guide β€” add β€œSign in with ExtremeX ONE”

The shortest path for a web app with a JVM backend. (Other stacks: same three steps, different syntax β€” see platforms.)

  1. Register your app on the dashboard as a Web app / website; set the production redirect URL and (for dev) a http://localhost:… one β€” required for this app type since it drives its own sign-in.
  2. Configure Data access β€” start minimal: name required, email optional with an honest reason.
  3. Wire two routes with the library (see the full example): /auth/login generates state and redirects to authorizeUrl(state); /auth/callback verifies state, calls exchangeCode, then userinfo, and matches your local account by user.sub.

Handle the two β€œunhappy” paths from day one: the user pressing Cancel (?error=access_denied on your callback) and a declined optional field (user.getEmail() == null β€” offer value, don't nag).

Guide β€” keep users signed in

  1. Request the Stay signed in (offline) field in Data access β€” the token response then includes a refresh_token.
  2. Wrap tokens in an ExOneSession with a Store β€” refresh tokens rotate on every use, and the Store is how the rotated pair reaches your persistence. Missing this is the #1 integration bug: everything works until the first restart, then every session is dead.
  3. On isAccessRevoked() (or HTTP 403 access_revoked): the user disconnected you from their ONE account. Delete stored tokens, show the signed-out state, re-run sign-in when they return.
Kotlin
// App start:
val session = ExOneSession.restore(auth, disk.access, disk.refresh) { a, r -> disk.save(a, r) }
// From here, session.userinfo() / ExOneData(session) never hand you a stale token.

Guide β€” store per-user app data

  1. Declare variables in Create data (e.g. level: number, preferences: json) β€” get type errors in development, not corrupt data in production.
  2. Request the App data field (required if your app can't work without it).
  3. Use ExOneData (or exONE.data on web): typed reads, batch writes, increment() for counters.

What belongs here: settings, progress, preferences, lightweight state β€” anything per-user that you'd otherwise stand up a database table for. What doesn't: data shared between users, large blobs (16 KB/value cap), or anything you need to query across users β€” that's your own database's job.

Use cases

A game: identity + progress, no backend database

Request name (required β€” leaderboard identity), attributes (required β€” save games) and offline (optional β€” resume without re-login). Declare high_score: number, level: number, save: json. Use increment("games_played", 1) per round and write("save", state) on checkpoint. Result: full player persistence with zero server infrastructure of your own.

A SaaS tool: one identity across your product suite

Register each product as its own app under your namespace (com.yourco.crm, com.yourco.billing) β€” one ONE account signs into all of them, while consents, data namespaces and analytics stay cleanly per-app. Use the Team feature to give engineers scoped access (e.g. credentials but not delete) with a 1-year expiry that matches contractor terms.

A cross-device consumer app: settings that follow the user

Web front-end uses the JS SDK, Android uses exone-auth/exone-data, both write the same preferences: json variable β€” the user changes their theme on the phone and the web app has it on next load. No sync service to build.

An internal tool: staff sign-in in an afternoon

Register com.yourco.admin as a Web app / website with name + email required, redirect to your intranet host, and gate your routes on a verified sub allowlist. The consent screen, session management, and audit trail come from the platform.

A mobile app: register before you build the sign-in screen

Building the Android client and its data model before wiring up sign-in? Register it as Other and skip the redirect URL β€” you get a client_id/client_secret immediately and can start on App data. Come back to Credentials and add the https://…/auth/callback app-link redirect (see Mobile & native) once you build the actual /oauth/authorize call β€” every app that signs users in of its own accord needs at least one registered redirect URL before that call will work.

Launch checklist

  1. If your app signs users in directly (app type Web app / website, or any native app calling /oauth/authorize): production redirect URL registered (HTTPS); dev localhost URL removed.
  2. Secret in a secrets manager; CI has no access; rotate it if it ever touched a repo.
  3. state generated and verified in the callback.
  4. 401 β†’ refresh, 403 access_revoked β†’ local sign-out, declined-optional-field paths all tested.
  5. Icon, description and website set (Settings) β€” they appear on the consent screen.
  6. Data-access reasons reviewed β€” clear, honest, specific.
  7. Your privacy policy covers the ONE data you receive β€” see the legal hub.
  8. Post-launch: watch sign-ins, grant rates and active sessions on the dashboard.

Reference

GET /oauth/authorize

HTTP
GET https://one.extremextechnology.com/oauth/authorize
  ?client_id=…        required β€” your client ID
  &redirect_uri=…     required β€” must exactly match a registered redirect URL
  &state=…            recommended β€” anti-CSRF value echoed back to you
  &scope=…            legacy only β€” apps with a data-fields manifest ignore it

POST /oauth/token

HTTP
POST https://api.extremextechnology.com/one/oauth/token
Content-Type: application/json

// Exchange an authorization code:
{
  "grant_type": "authorization_code",
  "code": "…",
  "client_id": "exone_xxxxxxxxxxxx",
  "client_secret": "…",
  "redirect_uri": "https://app.yourco.com/auth/callback"
}
β†’ { "token_type": "Bearer", "access_token": "…", "expires_in": 3600,
    "scope": "first_name last_name email", "refresh_token": "…" }   // refresh only with "offline"

// Refresh (tokens rotate β€” store the new refresh_token):
{
  "grant_type": "refresh_token",
  "refresh_token": "…",
  "client_id": "…",
  "client_secret": "…"
}

GET /oauth/userinfo

HTTP
GET https://api.extremextechnology.com/one/oauth/userinfo
Authorization: Bearer <access_token>

β†’ 200 {
  "sub": "9f1c…",                  // stable user id β€” always present
  "first_name": "Ada",             // if granted
  "last_name": "Lovelace",         // if granted
  "avatar_url": "https://…",       // if granted
  "email": "[email protected]",      // if granted
  "date_of_birth": "1990-01-01",   // if granted
  "age_status": "adult_unverified",// if granted β€” minor | adult_unverified | adult_verified
  "phone": "+44…",                 // if granted (unverified)
  "address": { "country": "GB", "line1": "…", "street": "…", "town": "…", "postcode": "…" } // if granted
  // work_email, work_phone, work_address, nationality, country_of_birth, languages β€” same, if granted
}
β†’ 403 { "error": "access_revoked" }   // user disconnected your app

Data endpoints

HTTP
GET    https://api.extremextechnology.com/one/data            β†’ { "data": { key: value, … } }
GET    https://api.extremextechnology.com/one/data?key=k      β†’ { "key": "k", "value": … }
PUT    https://api.extremextechnology.com/one/data            body { "data": { key: value, … } } β†’ { "ok": true }
DELETE https://api.extremextechnology.com/one/data?key=k      β†’ { "ok": true }

Limits: ≀100 keys per write Β· ≀16 KB per value Β· key = [a-z0-9][a-z0-9_.-]{0,119}
Declared variables are type-checked β†’ 400 { "error": "Type mismatch for \"k\": expected number." }

Errors

MemberWhat it does
invalid_client (401)Client ID/secret pair is wrong, or the app is paused.
invalid_grant (400)Code expired (5 min), already used, or redirect URI mismatch.
invalid_token (401)Access token missing, malformed, or expired (1 h). Refresh or re-authorize.
access_revoked (403)The user disconnected your app (or withdrew the needed field). Delete stored tokens, re-run the flow.
insufficient_scope (403)The call needs a field the user didn't grant.
Type mismatch… (400)A declared data variable was written with the wrong type.
session_expired (401, library)ExOneSession has no refresh token left β€” re-run sign-in.
network_error (0, library)The request never reached the server β€” retry with backoff.

Security practices

  • Keep the client secret in server-side configuration only; rotate it from the Credentials tab if it may have leaked.
  • If your app runs its own sign-in flow, register exact redirect URLs β€” no wildcards, and use localhost only for development. Apps that never call /oauth/authorize themselves (app type Other) can skip this entirely.
  • Always send and verify state; the library's ExOneAuth.newState() gives you a safe value.
  • Treat sub as the user's primary key; emails can change or be withdrawn.
  • Request the minimum data set; mark optional everything your app can work without.
  • On access_revoked, stop processing and delete cached ONE data for that user within 30 days (see Terms).
  • Report suspected credential leaks to [email protected] and rotate immediately.

Questions β†’ [email protected] Β· libraries 1.2.0 Β· docs updated 29 August 2026