DOCUMENTATION · DEVELOPERS

Atom9 login for developers.

This area covers one thing: authentication with Atom9 login over standard OpenID Connect. Your app sends people to id.atom9.com and gets back signed proof of who they are. Quickstart, a command-line test, examples for ten stacks, and the rules — all on this page.

Complete guide — every section on one page. The tabbed version for reading is at atom9.com/docs/add-atom9-login.

Quickstart

Everything from nothing to a working "Continue with Atom9" button: register your app, wire it up, and — the part most first integrations get slightly wrong — exactly what your app does when someone signs in, signs up, or signs out. Plus a terminal test before you write any code, an optional Atom9-managed-profile mode, and how your users manage or remove their own data.

1. What this is — and what it is not

a. What Atom9 login does. It answers exactly one question for your app: who is this person? Atom9 verifies the person (email, passkey, Google, Microsoft — whatever your app's policy allows) and hands your app a signed token containing a stable user id (sub), their email, and whether that email is verified. The protocol is standard OpenID Connect — the same one behind "Sign in with Google".

b. What it is not. It is not authorization (what a person may do in your app — that is yours) and not a session for your app (you mint your own after login). By default it is also not a user-profile store — your app owns its accounts and data — but you can opt into Atom9 managing the end-user profile instead (step 7).

c. What kind of app can use it. Server-side web applications — "confidential clients" that can keep a secret on a server. Single-page apps or mobile apps without a backend are not supported yet: the token exchange requires the client secret, which must never ship to a browser or device.

2. Add a sign-in app in the Atom9 admin

a. Sign in at admin.atom9.com and open Settings → Authentication → + Sign-in app.

b. Fill in the form: a name, the type (External app for anything that is not an Atom9 surface), and your redirect URLs — your callback, e.g. https://app.example.com/oidc/callback. Matching is exact, so register every environment's URL, including http://localhost:<port>/oidc/callback for development.

c. Copy the two credentials it gives you: the client_id (public) and the client_secret — shown once. Store the secret server-side (environment variable or secret manager). If you lose it, rotate from the same screen.

d. Optionally set the app's security level and allowed sign-in methods. Atom9 enforces those at login time; your app implements none of it.

3. Point your app at the discovery URL

a. Every OIDC library configures itself from one URL:

https://id.atom9.com/.well-known/openid-configuration

b. Give the library your three values — client_id, client_secret, redirect URL — usually via environment variables:

ATOM9_CLIENT_ID=a9-xxxxxxxxxxxx
ATOM9_CLIENT_SECRET=(the secret you copied)
ATOM9_REDIRECT_URI=https://app.example.com/oidc/callback

c. Pick your stack in Examples by stack — each one is a working configuration you can paste.

4. Your first login, end to end

a. Start your app and click your new login button. The browser goes to id.atom9.com.

b. Sign in with a fresh email. Atom9 walks the person through account creation (or straight in, if they already have an Atom9 account), shows your app's consent screen, and returns to your redirect URL with a one-time code.

c. Your library exchanges the code, validates the ID token, and hands you claims: sub, email, email_verified, name.

d. Resolve the account in your own database — key on sub, never on email:

user = db.find_by_atom9_sub(claims.sub)
if user:  log them in
else:     run YOUR onboarding, then db.create({ atom9_sub: claims.sub, ... })

e. Log in a second time with the same account. If your app created a second user, you keyed on email instead of sub — fix that before anything else.

4b. Sign in, sign up, sign out — in plain words

Atom9 proves who the person is. Your app decides what happens next. There are only three moments to handle, and each is short. This is the part most first integrations get slightly wrong, so here it is spelled out.

a. Signing in someone who already has an account. Your callback gets a token, your library checks it is genuine, and you read the claims. Look the person up in your database by their Atom9 sub. You found them → start your app's own session and send them to their dashboard. That is a login. Done.

b. Signing up someone new. It is the same callback and the same token — the only difference is your lookup by sub finds nobody. That is a first-time visitor, and the right move is simple: create the account right now, in the callback, from the claims Atom9 already gave you (sub, email, name), then start your session and continue. There is no separate "sign-up" form to send them to — the first successful login is the sign-up.

// ONE handler covers both. This is the whole thing.
user = db.find_by_atom9_sub(claims.sub)   // look up by the stable Atom9 id
if (!user) {                              // nobody yet? this is a new person
  user = db.create({ atom9_sub: claims.sub, email: claims.email, name: claims.name })
}
startYourSession(user)                    // your own cookie, your own lifetime
redirect("/dashboard")                    // let them in

The one rule that prevents the #1 bug — the sign-in loop. Once you hold a valid, verified token, the person is signed in. Create-or-find the account, start your session, and move on. Never send an authenticated person back to the login button (or to /oidc/authorize) just because a local record didn't exist yet — that is precisely what makes the browser bounce endlessly between your app and Atom9. "No account found" is not a dead end; it is your signal to create one.

c. Signing out. In your app, "log out" means one thing: end your own session — clear your session cookie or server-side session and send the person to your home page. They are now out of your app. That is all most apps ever need.

The Atom9 login is a separate session, and that is on purpose: keeping it is what makes the person's next visit one tap instead of a full sign-in — the same convenience as "Sign in with Google". You do not reach into it. If someone wants to sign out of Atom9 itself — for example on a shared computer — they do that from their own Atom9 account at id.atom9.com, where they can also see every app their login is connected to. There is deliberately no way for one app to sign a person out of all the others.

5. Test from the command line — no app required

You can drive the entire flow with curl and a browser. This is the fastest way to confirm your registration works before writing any code.

a. Check the public endpoints (works even before registering):

curl -s https://id.atom9.com/.well-known/openid-configuration
curl -s https://id.atom9.com/.well-known/jwks.json

b. Register a localhost callback on your sign-in app, e.g. http://localhost:9990/callback. Nothing needs to listen there — the redirect will just fail in the browser, and that is fine for this test.

c. Build an authorize URL (bash; generates PKCE + state):

CLIENT_ID="a9-xxxxxxxxxxxx"
REDIRECT="http://localhost:9990/callback"
VERIFIER=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 64)
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary \
  | openssl base64 -A | tr '+/' '-_' | tr -d '=')
STATE=$(openssl rand -hex 12)

echo "https://id.atom9.com/oidc/authorize?response_type=code&client_id=$CLIENT_ID\
&redirect_uri=$REDIRECT&scope=openid%20profile%20email&state=$STATE\
&code_challenge=$CHALLENGE&code_challenge_method=S256"

d. Open that URL in a browser, sign in, and approve. The browser lands on localhost:9990/callback?code=...&state=... and shows a connection error — copy the code value from the address bar. It is single-use and expires within minutes.

e. Exchange the code for tokens (same shell, so $VERIFIER is still set):

curl -s https://id.atom9.com/oidc/token \
  -d grant_type=authorization_code \
  -d code="PASTE_THE_CODE" \
  -d redirect_uri="$REDIRECT" \
  -d client_id="$CLIENT_ID" \
  -d client_secret="PASTE_YOUR_SECRET" \
  -d code_verifier="$VERIFIER"

f. Look inside the ID token — decode its middle segment to see your sub and email claims:

printf %s "PASTE_THE_ID_TOKEN" | cut -d '.' -f2 | tr '_-' '/+' \
  | awk '{ p=length($0)%4; if(p==2) $0=$0"=="; else if(p==3) $0=$0"="; print }' \
  | base64 -d

Decoding is for looking, not for trusting: a real app must validate the token (signature against the JWKS, iss, aud, exp, nonce) — every library in the examples does this for you.

Light or dark — you drive it, and it round-trips. The Atom9 login does not follow the visitor's operating system; your site owns the choice so the theme stays consistent across your site, the Atom9 login, and your app. It's a simple round-trip: (1) track the visitor's theme on your site (default light); (2) when you send them to log in, add theme=dark or theme=light to the authorize request — the login and consent pages render in that theme (no param → light); (3) on the callback, Atom9 returns theme=dark alongside code and state — read it, apply it on your post-login page, and persist it for the session. Only dark and light are ever emitted.

6. Before you go live

a. Redirect URLs are https:// only (localhost is for development).

b. The client secret lives in server-side configuration, not in code or a repository.

c. Your app treats email_verified: false as unverified — no access to anything tied to that address.

d. Read Rules & responsibilities — the short list of practices that keep the integration safe, and where Atom9's responsibility ends and yours begins.

7. Optional: let Atom9 manage the user profile

Everything above assumes your app owns its accounts and profile data — the default. If instead you'd like Atom9 to own the end-user record — run the onboarding, take the person's consent, store their profile, and answer their data requests — you can switch that on per sign-in app. Your login code does not change: you still send people to id.atom9.com and still receive the same token keyed on sub. Only Atom9's behaviour behind the login changes.

a. Turn it on. On your sign-in app in the admin (Settings → Authentication), enable "Create a managed end-user profile on first sign-in." Two settings shape it:

b. Sign-up policy decides who may create an account: open (anyone who signs in), invite-only (a profile is created only for invited people), or request access (the person is onboarded in a pending state and waits for approval).

c. Security level sets an assurance floor (standard, elevated, high, or regulated): Atom9 refuses or steps up weak sign-in methods so every managed profile meets at least the strength you require. If you also define which profile fields your service needs, Atom9 collects the required ones during onboarding.

d. What the person sees. A login themed as you (your name and look, no Atom9 admin surface), then a consent page that spells out exactly what data you will hold and why — required vs optional, no pre-ticked boxes — with anything Atom9 already knows (name, email) offered to share rather than retype. Declining optional data stores only what is required. Returning visitors are not asked again — they just sign in.

e. Who is responsible. Atom9 becomes the custodian of that managed profile and the person manages it themselves at atom9.com ("Your accounts & data"), where they can see every Atom9-managed account they hold and export or erase their data per service — so you do not build export or delete screens. You remain the data controller for the profile; Atom9 processes it on your behalf under a data processing agreement. Everything inside your own app — sessions, permissions, and any data you keep on your side — stays yours.

f. Your callback still does the same job — this is the trap. Turning managed profiles on does not change your login code: you still receive the same token keyed on sub, and you still mint your own session and let the person in. The difference is that in managed mode you may keep no local users table at all — so treat a validated sub as the account, start your session, and go. Do not look the person up in a local database and bounce them back to the login button when the row is missing — there may not be one, and that is the classic managed-mode login loop. A validated token is the green light, every time.

Pick pure-OIDC (steps 1–6) when your app already has its own accounts and data store; pick managed profiles when you'd rather Atom9 own the end-user record, the onboarding, and the data-rights obligations. The choice is per sign-in app and reversible.

8. Your users' data & privacy — and the one link to add

People increasingly expect a clear answer to "what do you know about me, and how do I leave?" Atom9 makes that easy — and where the person goes depends on the mode you picked in step 7.

a. If Atom9 manages the profile (step 7 is on). Atom9 is the custodian of that end-user record, and the person manages it themselves — there are no data screens for you to build. They open Your accounts & data at https://id.atom9.com/login?view=accounts, see every service their Atom9 login is connected to (yours included), and can export or remove their data from any one of them — including removing themselves from your app — in a couple of clicks. Removing your app there deletes the managed profile and revokes the connection.

b. Add one link — that is the whole task. Put a plain "Manage your data" or "Privacy" link in your app (in account settings, or your footer) pointing to https://id.atom9.com/login?view=accounts. That single link gives your users a real, working data-rights centre without you writing a line of export or delete code. They also see the same controls on the Atom9 consent screen every time they connect, and can leave from there too.

c. If your app owns the profile (pure OIDC — the default). Then the data your app stores is yours to manage: you are the data controller for it, and export and deletion of that data are your job. Atom9 still looks after the person's Atom9 account and login data on its own side.

d. Where the responsibilities are written down: the GDPR page explains controller vs custodian in each mode, and Rules & responsibilities has the one-glance table. The login itself is always governed by the Atom9 Privacy Policy and Terms.

Examples by stack

There is deliberately no Atom9 SDK: Atom9 login is standard OpenID Connect, so you use the same mature, audited library your stack already trusts for "Sign in with Google". Every example needs the same three values from your sign-in app registration: client ID, client secret, redirect URL. In every stack the job after login is identical: key your account on sub and mint your own session.

1. Node.js — openid-client

a. Install: npm i openid-client

b. Configure and run the flow:

import * as client from "openid-client";

const config = await client.discovery(
  new URL("https://id.atom9.com"),
  process.env.ATOM9_CLIENT_ID,
  process.env.ATOM9_CLIENT_SECRET,
);

// /login — send the browser to Atom9
const verifier = client.randomPKCECodeVerifier();
const state = client.randomState();
// store { verifier, state } in a short-lived HttpOnly cookie, then redirect to:
const authUrl = client.buildAuthorizationUrl(config, {
  redirect_uri: process.env.ATOM9_REDIRECT_URI,
  scope: "openid profile email",
  code_challenge: await client.calculatePKCECodeChallenge(verifier),
  code_challenge_method: "S256",
  state,
});

// /oidc/callback — exchange the code; the library validates the tokens
const tokens = await client.authorizationCodeGrant(config, callbackUrl, {
  pkceCodeVerifier: verifier,
  expectedState: state,
});
const claims = tokens.claims(); // { sub, email, email_verified, name, ... }

c. Key on claims.sub, mint your own session cookie.

2. Next.js — Auth.js (NextAuth)

a. Install: npm i next-auth@beta

b. auth.ts:

import NextAuth from "next-auth";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    {
      id: "atom9",
      name: "Atom9",
      type: "oidc",
      issuer: "https://id.atom9.com",
      clientId: process.env.ATOM9_CLIENT_ID,
      clientSecret: process.env.ATOM9_CLIENT_SECRET,
      authorization: { params: { scope: "openid profile email" } },
    },
  ],
  callbacks: {
    // profile.sub is the stable Atom9 user id — key your accounts on it.
    async signIn({ profile }) {
      return Boolean(profile?.email_verified);
    },
  },
});

c. Register https://your-app.example/api/auth/callback/atom9 as the redirect URL.

3. Python — Flask + Authlib

a. Install: pip install authlib flask

b. Configure:

from authlib.integrations.flask_client import OAuth

oauth = OAuth(app)
oauth.register(
    name="atom9",
    server_metadata_url="https://id.atom9.com/.well-known/openid-configuration",
    client_id=os.environ["ATOM9_CLIENT_ID"],
    client_secret=os.environ["ATOM9_CLIENT_SECRET"],
    client_kwargs={"scope": "openid profile email", "code_challenge_method": "S256"},
)

@app.route("/login")
def login():
    return oauth.atom9.authorize_redirect(url_for("callback", _external=True))

@app.route("/oidc/callback")
def callback():
    token = oauth.atom9.authorize_access_token()  # validates state + signature
    claims = token["userinfo"]                    # sub, email, email_verified, name
    # key your account on claims["sub"], mint your own session
    ...

4. Python — Django + mozilla-django-oidc

a. Install: pip install mozilla-django-oidc

b. settings.py:

OIDC_OP_DISCOVERY_ENDPOINT = "https://id.atom9.com/.well-known/openid-configuration"
OIDC_RP_CLIENT_ID = os.environ["ATOM9_CLIENT_ID"]
OIDC_RP_CLIENT_SECRET = os.environ["ATOM9_CLIENT_SECRET"]
OIDC_RP_SIGN_ALGO = "RS256"
OIDC_RP_SCOPES = "openid profile email"

AUTHENTICATION_BACKENDS = [
    "mozilla_django_oidc.auth.OIDCAuthenticationBackend",
    # ...
]

c. Subclass OIDCAuthenticationBackend and override filter_users_by_claims / create_user to key on claims["sub"] instead of email. Redirect URL: https://your-app.example/oidc/callback/.

5. Go — coreos/go-oidc

a. Install: go get github.com/coreos/go-oidc/v3/oidc golang.org/x/oauth2

b. Configure and handle the callback:

provider, err := oidc.NewProvider(ctx, "https://id.atom9.com")
if err != nil { log.Fatal(err) }

config := oauth2.Config{
    ClientID:     os.Getenv("ATOM9_CLIENT_ID"),
    ClientSecret: os.Getenv("ATOM9_CLIENT_SECRET"),
    RedirectURL:  os.Getenv("ATOM9_REDIRECT_URI"),
    Endpoint:     provider.Endpoint(),
    Scopes:       []string{oidc.ScopeOpenID, "profile", "email"},
}
verifier := provider.Verifier(&oidc.Config{ClientID: config.ClientID})

// /login: redirect to config.AuthCodeURL(state, PKCE options)
// /oidc/callback:
token, err := config.Exchange(ctx, r.URL.Query().Get("code"), pkceVerifier)
idToken, err := verifier.Verify(ctx, token.Extra("id_token").(string))

var claims struct {
    Sub           string `json:"sub"`
    Email         string `json:"email"`
    EmailVerified bool   `json:"email_verified"`
}
idToken.Claims(&claims) // key your account on claims.Sub

6. PHP — Laravel Socialite

a. Install: composer require socialiteproviders/oidc

b. config/services.php:

'oidc' => [
    'base_url'      => 'https://id.atom9.com',
    'client_id'     => env('ATOM9_CLIENT_ID'),
    'client_secret' => env('ATOM9_CLIENT_SECRET'),
    'redirect'      => env('ATOM9_REDIRECT_URI'),
],

c. Routes:

Route::get('/login', fn () => Socialite::driver('oidc')->redirect());

Route::get('/oidc/callback', function () {
    $user = Socialite::driver('oidc')->user();
    // $user->getId() is the stable sub — key your account on it
    $local = User::firstOrCreate(['atom9_sub' => $user->getId()], [...]);
    Auth::login($local);
    return redirect('/dashboard');
});

7. Ruby — Rails + omniauth_openid_connect

a. Gemfile: gem "omniauth_openid_connect"

b. config/initializers/omniauth.rb:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :openid_connect, {
    name: :atom9,
    issuer: "https://id.atom9.com",
    discovery: true,
    scope: [:openid, :profile, :email],
    response_type: :code,
    client_options: {
      identifier: ENV["ATOM9_CLIENT_ID"],
      secret: ENV["ATOM9_CLIENT_SECRET"],
      redirect_uri: ENV["ATOM9_REDIRECT_URI"],
    },
  }
end

c. In the callback controller, request.env["omniauth.auth"]["uid"] is the stable sub — key your account on it.

8. Java — Spring Boot

a. Dependency: spring-boot-starter-oauth2-client

b. application.yml:

spring:
  security:
    oauth2:
      client:
        registration:
          atom9:
            client-id: ${ATOM9_CLIENT_ID}
            client-secret: ${ATOM9_CLIENT_SECRET}
            scope: openid,profile,email
        provider:
          atom9:
            issuer-uri: https://id.atom9.com

c. Redirect URL is Spring's default: {baseUrl}/login/oauth2/code/atom9 — register it. The principal's getName() returns sub.

9. C# — ASP.NET Core

a. Package: Microsoft.AspNetCore.Authentication.OpenIdConnect

b. Program.cs:

builder.Services.AddAuthentication(options => {
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options => {
    options.Authority = "https://id.atom9.com";
    options.ClientId = builder.Configuration["Atom9:ClientId"];
    options.ClientSecret = builder.Configuration["Atom9:ClientSecret"];
    options.ResponseType = "code";
    options.UsePkce = true;
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.GetClaimsFromUserInfoEndpoint = true;
});

c. The sub claim arrives as the name identifier — key your account on it, not on the email claim.

10. Rust — openidconnect

a. cargo add openidconnect

b. Configure against the discovery URL:

use openidconnect::core::{CoreClient, CoreProviderMetadata};
use openidconnect::{ClientId, ClientSecret, IssuerUrl, RedirectUrl};

let provider = CoreProviderMetadata::discover_async(
    IssuerUrl::new("https://id.atom9.com".to_string())?,
    &http_client,
).await?;

let client = CoreClient::from_provider_metadata(
    provider,
    ClientId::new(std::env::var("ATOM9_CLIENT_ID")?),
    Some(ClientSecret::new(std::env::var("ATOM9_CLIENT_SECRET")?)),
)
.set_redirect_uri(RedirectUrl::new(std::env::var("ATOM9_REDIRECT_URI")?)?);

// authorize_url() with PKCE + state + nonce, then exchange_code() in your
// callback handler — the crate validates the ID token against our JWKS.

c. The validated claims' subject() is the stable sub — key your account on it.

Rules & responsibilities

Two things every integrating team should read before going live: the short list of practices that keep the integration safe, and a plain statement of where Atom9's responsibility ends and yours begins — with links to the pages where each concept is defined.

1. Do this

a. Key accounts on sub. It is the only identifier that is stable for a person across logins and methods. Emails change and can be reassigned.
b. Validate every ID token with a maintained OIDC/JWT library — signature against our JWKS, plus iss, aud (= your client_id), exp, and nonce.
c. Trust email_verified: true only. An unverified email claim must never grant access to anything tied to that address.
d. Keep the client secret server-side — environment variable or secret manager. Rotate it from the admin immediately if it may have leaked.
e. Use state + nonce + PKCE (S256) on every authorization request. Every library in the examples does this for you.
f. Register every environment's exact redirect URL — production, staging, and http://localhost:<port> for development. Matching is exact.
g. Mint your own session after login with your own lifetime and logout. The ID token proves who arrived; it is not your session.

2. Do not do this

a. Never ship the client secret to a browser or mobile app, and never commit it to a repository. The token exchange belongs on your server.
b. Never key accounts on email address. The person who owns an email today may not own it next year — you would hand one user's account to another.
c. Never accept a token without checking aud. A valid Atom9 token issued for a different app must not log anyone into yours.
d. Never parse or verify JWTs by hand. Hand-rolled validation is the single most common OIDC integration vulnerability.
e. Never put tokens in URLs, logs, or client-side storage. Exchange the code server-side, read the claims, then drop the token.
f. Never skip state — that is login CSRF, and it lets an attacker attach their session to your user's browser.
g. Never reuse the ID token as an API credential. It authenticates one login moment to one app; your app issues its own credentials for its own APIs.

3. The responsibility line

A clear fence prevents finger-pointing at 2am. In plain terms: Atom9 is responsible for proving who the person is; your app is responsible for everything it does with that proof.

Atom9's side of the fenceYour side of the fence
Operating id.atom9.com: the authorize, token, JWKS, userinfo, and discovery endpoints. Your callback endpoint, your session management, and your app's availability.
Authenticating the person with the methods and security level your app's policy requires, including MFA when configured. Authorization inside your app — what a signed-in person is allowed to see and do.
Issuing correctly signed tokens with accurate claims (sub, email_verified, amr, acr), signing-key custody and rotation. Validating those tokens before trusting them, with a maintained library.
Wrong-account protection, verified account linking (e.g. a Google login joining an existing account), and Atom9-account credential security. Custody of your client secret, and the correctness of the redirect URLs you register.
Atom9 account data and Atom9-side sessions, including revocation on our side. All data your app stores about its users — you are the controller for it — plus your onboarding and your own session lifetime and logout.

Practically: if a person who should not have passed login got a valid Atom9 token, that failure is ours. If a valid token was mishandled after it reached you — not validated, wrong audience accepted, secret leaked, email used as the account key — that failure is yours. This page is the operational summary; the Atom9 Terms are the binding agreement between us.

The fence above describes the default (pure-OIDC) integration, where you own the user profile. If you switch on managed end-user profiles (Quickstart step 7), the profile fence moves: Atom9 becomes the custodian that stores the profile and handles the person's export/erasure at atom9.com, while you remain the data controller and Atom9 processes the data on your behalf under a data processing agreement. Your token-handling responsibilities on this page are unchanged in either mode.

4. Where these things are defined

a. Plain-language definitions — the glossary explains every term used here: OIDC, identity provider, token, session, secret, MFA, passkey, email verification, audit trail.

b. Security practices — the security page covers account protection, sessions, and how to report suspicious behaviour.

c. Data protection roles — the GDPR page explains controller and processor roles: for the data your app stores about its users, your company is the controller.

d. Compliance model — the compliance page describes safety checks, evidence, and audit trails; DORA alignment covers financial-sector requirements.

e. The binding agreement — the Terms and the Privacy Policy. Everything on this page is an operational summary of those documents, not a replacement for them.

5. If something goes wrong

a. Suspected secret leak: rotate the secret in the admin (Settings → Authentication → your sign-in app) — the old secret stops working immediately — then redeploy with the new one.

b. Suspected Atom9-side issue: capture the timestamp, your client_id, and the failing endpoint, and contact us. Never include tokens or secrets in reports.

c. Removing the integration: delete the sign-in app in the admin; outstanding tokens expire on their own and no new logins can start.