# KT Accounts Developer Documentation > How to connect an app to KT Accounts, the account service for aidenkt sites. KT Accounts is the account service for every aidenkt site, served at `https://accounts.aidenkt.com`. People sign in with a passkey, Google or GitHub. Sign-ups are closed: only people Aiden has invited have accounts. These pages explain how to connect an app to it. There are three ways, and an app can use more than one: | Path | For | What the App Gets | | --- | --- | --- | | [Sign-In Kit](/integrate/sign-in-kit/) | A site on an `aidenkt.com` origin | The Log In button, the person's picture and menu, the app switcher, and `data-kt-*` attributes to show or hide parts of a page | | [Read Who Is Signed In](/integrate/who-is-signed-in/) | A page on an `aidenkt.com` origin | The signed-in person's ID, name, handle, pictures, permissions, badges and apps, read from the browser | | [Sign in with KT Accounts](/integrate/openid-connect/) | An app on any domain, or a server | Standard OpenID Connect: an ID token with the person's KT Accounts ID and, with consent, their name, handle, picture and email | Every app is registered first, in the [Developer Portal](/portal/register-an-app/) or by an agent through the [agent API](/agents/overview/). Registered apps go live at once. There is no review step. ## Where to Start - New to KT Accounts: read the [Quick Start](/getting-started/quick-start/). - An AI agent: read the [canonical agent guide](https://accounts.aidenkt.com/developers/agents.md), or [Agents](/agents/overview/) here. - Looking up an endpoint, scope or limit: see [Reference](/reference/endpoints/). ## Markdown and llms.txt Every page is also served as Markdown at its path with `.md` added, for example [`/getting-started/quick-start.md`](/getting-started/quick-start.md). [`/llms.txt`](/llms.txt) lists the pages, and [`/llms-full.txt`](/llms-full.txt) holds all of them in one file. Some pages need a KT account; see [Access to These Docs](/reference/docs-access/). --- # Agents > How an AI agent gets a key with apps.manage, exchanges it for access tokens, and what it can and cannot do. :::tip[The Canonical Agent Guide] The guide written for agents lives with the service and always matches it: **[accounts.aidenkt.com/developers/agents.md](https://accounts.aidenkt.com/developers/agents.md)**. It is plain Markdown, listed in [`/llms.txt`](https://accounts.aidenkt.com/llms.txt) there. This section covers the same ground for people and links to it. ::: An agent is its own account, owned by a person, holding an explicit subset of that person's permissions. With `apps.manage` it can register and manage that person's apps through the agent API. ## Get a Key An agent cannot make its own key. A person makes it: 1. Aiden, or someone Aiden has given `agents.manage`, creates an agent in [Account Settings](https://accounts.aidenkt.com/settings#agents) and ticks `apps.manage`. The person must hold `apps.manage` themselves; Aiden holds every permission. 2. They create a key for the agent and give it to the agent as a secret. It looks like `ktk_XXXXXXXX_<43 characters>` and expires after 90 days by default (1 to 365 days). An agent can also ask for access with a code a person approves (the RFC 8628 shape): it calls `POST /api/agent/device/code`, a person enters the code at [accounts.aidenkt.com/agents/approve](https://accounts.aidenkt.com/agents/approve), and the agent polls `POST /api/agent/device/token`. The result is the same: a key. Keep the key out of logs, URLs and prompts. Send it only to `https://accounts.aidenkt.com/api/agent/token`. ## Exchange the Key for an Access Token ```sh curl -s -X POST https://accounts.aidenkt.com/api/agent/token -H "Authorization: Bearer $KT_AGENT_KEY" ``` ```json { "access_token": "kta_…", "token_type": "Bearer", "expires_in": 3600, "scope": "apps.manage" } ``` The access token lasts 1 hour. Exchange the key again when it expires (`401 invalid_token`). Use the access token, never the key, on every other call. ## Check the Token ```sh curl -s https://accounts.aidenkt.com/api/agent/whoami -H "Authorization: Bearer $TOKEN" ``` ```json { "kind": "agent", "id": "…", "name": "Builder", "ownerUserId": "…", "permissions": ["apps.manage"], "owner": { "isOwner": false, "roles": ["developer"] }, "via": { "method": "key" } } ``` `permissions` is what the agent may do right now: its own list, narrowed to what its person currently holds. Agents never inherit Aiden's "all". `owner` says whether the agent's person is Aiden and lists that person's roles (badge keys), and nothing else about them. A server that receives requests from an agent can check it the same way: call this endpoint with the access token the agent presented. `401 invalid_token` means it is not a live token. ## What an Agent Can Do With `apps.manage`, everything a developer can do in the portal, for its person's apps: - list, register, read, change and delete apps; - rotate client secrets; - set and remove icons, including icons from the [icon maker's](/portal/icon-maker/) pack. The full reference, with a `curl` example for every endpoint, is in [Agent API](/agents/api/) and [Errors](/agents/errors/) (for Developers and above), and in the [canonical agent guide](https://accounts.aidenkt.com/developers/agents.md) (public). ## What an Agent Cannot Do - Read people's data: there is no agent endpoint for consents, in-app identities or users. - See or change anyone else's apps, or the three built-in apps. - Suspend or resume an app, or reorder Your Apps. These are Aiden's alone, in person. - Give itself permissions or badges. Revoking a key, disabling the agent, removing a permission from its person, or deleting the person takes effect on the next call. ## Reading Restricted Pages Here Some pages on this site need a level. An agent reads them by sending its access token: ```sh curl -s https://docs.aidenkt.com/agents/api.md -H "Authorization: Bearer $TOKEN" ``` See [Access to These Docs](/reference/docs-access/). --- # Quick Start > Pick the integration path that fits where the app runs, register it, and connect it. ## 1. Get Access to the Portal Registering apps needs the Developer badge, which Aiden gives, or an agent key that holds `apps.manage`. Aiden has both by default. Without one, the [Developer Portal](https://accounts.aidenkt.com/developers) answers that you need the badge. ## 2. Choose a Path ```text Does the app run on an aidenkt.com origin (for example https://notes.aidenkt.com)? ├─ Yes: use the Sign-In Kit for the button and menu. │ Does its server need to know who is signed in? │ ├─ No: the kit, and /api/whoami from the page, are enough. │ └─ Yes: also use OpenID Connect (a site's own server never sees the KT session cookie). └─ No (any other domain, a mobile app, a server, a CLI): use OpenID Connect. ``` | Path | Setup Time | Guide | | --- | --- | --- | | Sign-In Kit | About a minute | [Sign-In Kit](/integrate/sign-in-kit/) | | Read who is signed in from a page | A few lines | [Read Who Is Signed In](/integrate/who-is-signed-in/) | | OpenID Connect | A standard library and a callback route | [Sign in with KT Accounts](/integrate/openid-connect/) | ## 3. Register the App Open the [Developer Portal](https://accounts.aidenkt.com/developers), choose **New App** and fill in the name, home page, colour, who sees it, origins and redirect URIs. See [Register an App](/portal/register-an-app/) for every field. - For the kit, add the site's origin, for example `https://notes.aidenkt.com`, to **Origins**. - For OpenID Connect, add the exact callback address to **Redirect URIs** and pick the **Client Type**: a server (has a client secret) or a browser or mobile app (PKCE, no secret). A confidential app's client secret is shown once, when the app is registered. Copy it then. ## 4. Connect It The app's page in the portal (`/developers/apps/`) shows every snippet filled in for that app: the kit tags with their integrity hashes, the whoami call, the OpenID Connect endpoints, the authorize URL and the code exchange. ## 5. Check It on the Server What the kit shows or hides is presentation only. Anything that matters must be checked on a server. See [Check Sign-In on a Server](/integrate/server-checks/) and [Sign in with KT Accounts](/integrate/openid-connect/). --- # In-App Identity > Tell KT Accounts who the person is inside your app, shown on their Account Settings page. An app can tell KT Accounts who the person is inside it. Their Account Settings page then shows it next to your app, for example "@aiden on notes.example.com", and nowhere else. Use the person's OpenID Connect access token for your app. ## Set It ```sh curl -s -X PUT https://accounts.aidenkt.com/oauth/identity -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"displayName":"Aiden","handle":"aiden","profileUrl":"https://notes.example.com/@aiden"}' ``` `200 {"identity": {"displayName": …, "handle": …, "profileUrl": …, "pictureUrl": …}}` | Field | Rules | | --- | --- | | `displayName` | Up to 50 characters | | `handle` | 1 to 40 of letters, numbers, `_`, `.` and `-`. A leading `@` is dropped | | `profileUrl` | https, on a host the app registered (an origin or the host of a redirect URI) | `null` clears a field; a field you leave out is kept. 30 changes an hour per person and app. ## Picture The person's picture in your app: exactly 512 x 512 pixels, WebP or JPEG, at most 400 KB, with no metadata (no EXIF, XMP or ICC). KT Accounts keeps its own copy and never loads images from your site. ```sh curl -s -X PUT https://accounts.aidenkt.com/oauth/identity/picture -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: image/webp" --data-binary @picture.webp ``` 10 pictures a day per person and app. ## Remove It ```sh curl -s -X DELETE https://accounts.aidenkt.com/oauth/identity -H "Authorization: Bearer $ACCESS_TOKEN" curl -s -X DELETE https://accounts.aidenkt.com/oauth/identity/picture -H "Authorization: Bearer $ACCESS_TOKEN" ``` All of it is removed when the person removes your app in Account Settings, or when the app is deleted. --- # Sign in with KT Accounts > OpenID Connect with PKCE for apps on any domain and for servers. KT Accounts is an OpenID Connect provider. Use it for apps on any domain, for mobile apps, and for servers that need to know who is signed in. Standard libraries work: give them the issuer and they read the rest from the discovery document. | | | | --- | --- | | Issuer | `https://accounts.aidenkt.com` | | Discovery | `https://accounts.aidenkt.com/.well-known/openid-configuration` | | Authorize | `https://accounts.aidenkt.com/oauth/authorize` | | Token | `https://accounts.aidenkt.com/oauth/token` | | UserInfo | `https://accounts.aidenkt.com/oauth/userinfo` | | Keys | `https://accounts.aidenkt.com/oauth/jwks.json` (ES256) | | Scopes | `openid` (required), `profile`, `email`, `permissions` | | Flow | Authorization code only, `response_mode=query` | | PKCE | S256 only. Required for public clients, recommended for all | | Client authentication | `client_secret_basic`, `client_secret_post`, or `none` for public clients | ## Before You Start Register the app in the [Developer Portal](/portal/register-an-app/): - **Redirect URIs**: the exact callback addresses, for example `https://notes.example.com/auth/callback`. They are compared as exact strings. - **Client Type**: **Server (Has a Client Secret)** for a confidential client, or **Browser or Mobile (PKCE, No Secret)** for a public client. ## The Flow 1. Make a code verifier (43 to 128 characters) and its S256 challenge, a `state` and a `nonce`. Keep them for the callback. 2. Send the person to the authorize address. They sign in on `accounts.aidenkt.com` if they are not already, and allow the app once. 3. They come back to the redirect URI with `code`, `state` and `iss`, or with `error` (`access_denied` when they chose Cancel). Check that `state` is yours and `iss` is the issuer. 4. Exchange the code within 60 seconds. It works once. 5. Check the ID token, then start your own session. ```text https://accounts.aidenkt.com/oauth/authorize?client_id=ktc_… &redirect_uri=https%3A%2F%2Fnotes.example.com%2Fauth%2Fcallback &response_type=code&scope=openid%20profile&state=STATE&nonce=NONCE &code_challenge=CHALLENGE&code_challenge_method=S256 ``` ## In a Browser App (Public Client) ```js // 1. Before sending the person to sign in const b64 = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); const verifier = b64(crypto.getRandomValues(new Uint8Array(32))); const challenge = b64(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)))); const state = b64(crypto.getRandomValues(new Uint8Array(16))); const nonce = b64(crypto.getRandomValues(new Uint8Array(16))); sessionStorage.setItem("kt", JSON.stringify({ verifier, state, nonce })); location.assign("https://accounts.aidenkt.com/oauth/authorize?" + new URLSearchParams({ client_id: "YOUR_CLIENT_ID", redirect_uri: "https://your.app/callback", response_type: "code", scope: "openid profile", state, nonce, code_challenge: challenge, code_challenge_method: "S256", })); // 2. On https://your.app/callback const q = new URLSearchParams(location.search); const saved = JSON.parse(sessionStorage.getItem("kt")); if (q.get("state") !== saved.state) throw new Error("state does not match"); const r = await fetch("https://accounts.aidenkt.com/oauth/token", { method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", client_id: "YOUR_CLIENT_ID", code: q.get("code"), redirect_uri: "https://your.app/callback", code_verifier: saved.verifier, }), }); const { access_token, id_token } = await r.json(); ``` ## On a Server (Confidential Client) The same form, with the client ID and secret as HTTP Basic credentials: ```sh curl -s https://accounts.aidenkt.com/oauth/token -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -d grant_type=authorization_code --data-urlencode code=CODE \ --data-urlencode redirect_uri=https://your.app/callback --data-urlencode code_verifier=VERIFIER ``` ```json { "access_token": "kto_…", "token_type": "Bearer", "expires_in": 3600, "scope": "openid profile", "id_token": "eyJ…" } ``` The body must be `application/x-www-form-urlencoded`. A public client sends `client_id` in the form and no secret. A complete callback in a Worker or Node 18+ with [`jose`](https://github.com/panva/jose): ```js import { createRemoteJWKSet, jwtVerify } from "jose"; const ISSUER = "https://accounts.aidenkt.com"; const jwks = createRemoteJWKSet(new URL(`${ISSUER}/oauth/jwks.json`)); async function finishSignIn(url, saved, env) { if (url.searchParams.get("state") !== saved.state) throw new Error("state"); if (url.searchParams.get("iss") !== ISSUER) throw new Error("iss"); const r = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: "Basic " + btoa(`${env.CLIENT_ID}:${env.CLIENT_SECRET}`) }, body: new URLSearchParams({ grant_type: "authorization_code", code: url.searchParams.get("code"), redirect_uri: env.REDIRECT_URI, code_verifier: saved.verifier, }), }); if (!r.ok) throw new Error(`token ${r.status}`); const { id_token } = await r.json(); const { payload } = await jwtVerify(id_token, jwks, { issuer: ISSUER, audience: env.CLIENT_ID, algorithms: ["ES256"] }); if (payload.nonce !== saved.nonce) throw new Error("nonce"); return payload; // payload.sub is the person's KT Accounts ID } ``` `openid-client` (Node), Auth.js, `authlib` (Python) and `go-oidc` (Go) do all of this from the discovery address. ## Check the ID Token Before trusting it, check: - the ES256 signature, from a key in `/oauth/jwks.json`; - `iss` is `https://accounts.aidenkt.com`; - `aud` (and `azp`) is your client ID; - `nonce` is the one you sent; - `exp` is in the future. `sub` is the person's KT Accounts ID. It is stable and is the value to store. ID tokens and access tokens last 1 hour. ## Rules - **Redirect URIs** are compared as exact strings. Send the one you registered character for character. - **Codes** last 60 seconds and work once. A second use also revokes the tokens the first one gave. - **No refresh tokens.** When the access token expires, send the person to the authorize address again. It is instant once they have allowed the app. - **`prompt=none`** answers `login_required` or `consent_required` instead of showing a page. **`prompt=consent`** shows the consent screen again. - **Errors** from the token endpoint follow RFC 6749: `invalid_request`, `invalid_client` (401), `invalid_grant`, `unsupported_grant_type`. A bad `client_id` or `redirect_uri` is shown on KT Accounts' own error page and never sent back to the app. - **Consent** is remembered per person and app. A person can remove the app in Account Settings at any time, which ends its tokens and codes. ## UserInfo ```sh curl -s https://accounts.aidenkt.com/oauth/userinfo -H "Authorization: Bearer $ACCESS_TOKEN" ``` ```json { "sub": "…", "name": "…", "preferred_username": "…", "picture": "…" } ``` It answers with the claims for the scopes the person allowed. See [Scopes and Claims](/reference/scopes-and-claims/). --- # Roles and Permissions > Named permissions, and the role permissions every badge implies. KT Accounts has flat, named permissions. Aiden holds all of them. | Permission | Lets the Holder | | --- | --- | | `brand.materials` | Use the aidenkt brand materials | | `agents.manage` | Create and manage agents, their keys and approval requests | | `invites.create` | Create invite codes | | `apps.manage` | Register and manage apps in the developer portal (used by agents) | Aiden grants permissions in Account Settings. An agent holds an explicit subset of its person's permissions. ## Role Permissions Every badge implies a permission named `role.`: | Badge | Role Permission | Comes From | | --- | --- | --- | | Aiden | `role.aiden` | Aiden's account | | Sysadmin | `role.sysadmin` | The owner role | | Partner | `role.partner` | Given by Aiden | | Friend | `role.friend` | Given by Aiden | | Developer | `role.developer` | Given by Aiden | | Agent | `role.agent` | An agent account | | Test | `role.test` | Given by Aiden | Role permissions are derived, never stored. Giving the badge gives the permission; removing the badge takes it away. They cannot be granted directly: the permissions API, invites and agent requests answer `400 reserved_permission` for any `role.*` name. The owner holds all of them. They appear wherever permissions do: in `permissions` from `/api/whoami`, `/v1/me` and `/api/me`, in the kit's `can()`, in `data-kt-if="perm:…"`, and in the `permissions` claim of OpenID Connect. ## Using Them On a page with the [kit](/integrate/sign-in-kit/): ```html ``` ```js if (window.ktAccounts.can("role.developer")) showDeveloperTools(); ``` In OpenID Connect, ask for the `permissions` scope. The ID token and userinfo then carry `permissions` (with the `role.*` ones) and `is_owner`: ```json { "sub": "…", "permissions": ["brand.materials", "role.developer"], "is_owner": false } ``` For an app's tile, pick roles in **Who Sees It**. See [Who Sees It](/portal/who-sees-it/). ## Where Each Check Belongs The kit's `can()` and `data-kt-if` only change what the browser shows. Check permissions that matter on a server: from the ID token's `permissions` claim, or with the [server helpers](/integrate/server-checks/). --- # Check Sign-In on a Server > Why the kit protects nothing, what a site's server can check today, and the helpers for it. Anything that matters is checked on the server, never by the kit. The kit and the `data-kt-*` attributes only hide things in the browser. If a page, file or API route is private, the server must refuse to send it. ## The Limit Today The KT Accounts session cookie (`__Host-kt.session_token`) is sent by browsers to `accounts.aidenkt.com` and to nothing else, so a site's own server never receives it. That means: - **For a real server check, use [OpenID Connect](/integrate/openid-connect/)** and keep your own session. This works today on any domain. - The cookie-based helpers below return "nobody" on a real site, for everyone. They fail closed. They work against the [local stand-in](/integrate/local-development/), and will work on real sites once KT Accounts can hand a site proof of who is signed in, which is proposed and not built. ## The Helpers Single files with no dependencies, in the `integrations/` folder of the KT Accounts repository. Copy them into the site. | File | For | | --- | --- | | `kt-accounts.mjs` (and `kt-accounts.d.mts` for TypeScript) | Anything with `fetch`: Workers, Deno, Bun, Node 18+ | | `kt-accounts-middleware.mjs` | Express and Hono. Needs `kt-accounts.mjs` next to it | | `kt-accounts-next.mjs` | The Next.js app router. Needs `kt-accounts.mjs` next to it | ```js import { getKtUser, requireKtUser, has, hasBadge } from "./kt-accounts.mjs"; const kt = { origin: env.KT_ACCOUNTS_ORIGIN ?? "https://accounts.aidenkt.com" }; const user = await getKtUser(request, kt); // a person, or null const r = await requireKtUser(request, { ...kt, permission: "brand.materials" }); if ("response" in r) return r.response; // 401 or 403, ready to send ``` - `getKtUser(request, { origin })` returns the person or `null`. `request` can be a `Request`, a `Headers`, a Node request, or the `Cookie` header string. - `requireKtUser(request, { origin, permission?, badge? })` returns `{ user }` or `{ response }`: `401 {"error":"sign_in_required"}` or `403 {"error":"forbidden"}`, both JSON with `Cache-Control: no-store` and `Vary: Cookie`. - `has(user, name)` follows the same rule as `ktAccounts.can`: the owner has every permission. `hasBadge(user, "friend")` is true only if the person carries that badge. - `sendKtResponse(res, response)` sends such a response through a Node `res`. - Express: `ktExpress`, `requireKtExpress`. Hono: `ktHono`, `requireKtHono`. Next.js: `currentKtUser`, `ktMiddleware`. `getKtUser` sends only the KT session cookie to `/api/whoami`, gives up after 2 seconds, returns `null` on any failure, and keeps nothing between requests. `origin` must be exactly `https://accounts.aidenkt.com` or the stand-in on `http://localhost:`; anything else throws a `TypeError`. The stand-in is refused when `NODE_ENV` is `production`. People with the `test` badge are refused on an `https` origin unless you pass `allowTest: true`. Never cache a response that depends on who is signed in: send `Cache-Control: no-store`. The guards do not protect against cross-site form posts; for routes that change something, also check the `Origin` header or use the framework's CSRF protection. --- # Sign-In Kit > The Log In button, the person's picture and menu, and the app switcher for a site on aidenkt.com. The sign-in kit is one stylesheet and one script served by KT Accounts. It draws the account button, the person's picture with a menu (name, handle, Account Settings, Log Out), and the app switcher with the person's apps. Signing in always happens on `accounts.aidenkt.com`; the person comes back to the page they were on. The kit works only on a registered `aidenkt.com` origin, or on an origin Aiden has added to the service's allowed list. ## Add It to a Page 1. Register the app and add the site's exact origin, for example `https://notes.aidenkt.com`, to its **Origins**. 2. Put the two tags in the page's ``. The file names and `integrity` values change whenever the kit changes, so copy them from the app's page in the portal, or read them from `https://accounts.aidenkt.com/kit/manifest.json`: ```html ``` 3. Put the placeholder where the button and picture go, usually at the right end of the nav: ```html
``` Without a `data-kt-account` element, the button is fixed at the top right of the page. Do not copy the kit's script into the site or bundle it: it finds the accounts service from its own script URL. ## Content Security Policy If the site sends a `Content-Security-Policy`, add `https://accounts.aidenkt.com` to these five directives and keep what is already there: ```text script-src https://accounts.aidenkt.com style-src https://accounts.aidenkt.com font-src https://accounts.aidenkt.com img-src https://accounts.aidenkt.com connect-src https://accounts.aidenkt.com ``` Nothing else is needed: no `'unsafe-inline'`, and the kit works under `require-trusted-types-for 'script'`. ## Show or Hide Parts of a Page ```html ``` | Attribute | What It Does | | --- | --- | | `data-kt-account` | Where the account button is drawn | | `data-variant` on that element | `line` (the default), `float` or `glass` | | `data-kt-label` on that element | The word on the signed-out button. Default `Sign In` | | `data-kt-apps="off"` on that element | Leaves out the app switcher | | `data-kt-if="signed-in"` | Shown only when someone is signed in | | `data-kt-if="signed-out"` | Shown only when nobody is | | `data-kt-if="owner"` | Shown only to Aiden | | `data-kt-if="perm:"` | Shown only to people with that permission, including role permissions such as `role.friend`. The owner has every permission | | `data-kt-open` | Clicking it opens the sign-in card | | `data-kt-signout` | Clicking it signs the person out | Write `hidden` on every `data-kt-if` element so nothing shows before the kit has checked. If the site's CSS overrides `hidden`, add `[hidden] { display: none !important; }`. :::caution These attributes only change what the browser shows. They protect nothing: anyone can read the page source. Check anything that matters on a server. ::: ## The Three Styles | `data-variant` | Looks Like | Use It On | | --- | --- | --- | | `line` (default) | An outline in the page's own text colour, no fill | Plain pages and bars whose text colour reads on their background | | `float` | A white pill with a soft shadow | Busy or mid-tone backgrounds | | `glass` | Frosted white glass with white text | Photos and full-bleed colour | `line` checks its contrast: if the element's text colour is under 3 : 1 against the nearest opaque background behind it, the kit draws `float` instead. On KT Green, use Black text (about 11 : 1), not white. The button is drawn inside a shadow root, so in page source the element looks empty. The kit reports what it did with attributes: `data-kt-ready="signed-in"` or `"signed-out"` once the first check is done, `data-variant` for the style in use, and `data-variant-wanted` for the style that was asked for. ## The JavaScript API `window.ktAccounts` exists once the kit script has run. The kit tag is `defer`, so it is there for a `defer` or module script placed after the kit tags, and for anything that runs on `DOMContentLoaded` or later. | Member | | | --- | --- | | `ready` | Promise of the person (or `null`) after the first check | | `user()` | The last known person, or `null` | | `can(name)` | `true` if the person has that permission. The owner has all | | `open(options?)` | Opens the sign-in card. Options: `returnTo`, `title`, `message`, `dismissible` | | `close()` | Closes the card | | `refresh()` | Asks the service again; resolves to the person or `null` | | `signOut()` | Signs out, then refreshes | | `on("change", fn)` | Calls `fn(user)` whenever the person changes. Returns a function that stops it | | `version` | `"1"` | | `origin` | The accounts service the kit talks to | Events on `document`, each with `event.detail.user`: `ktaccounts:ready` (once, after the first check), `ktaccounts:change` (every change, including the first check), and `ktaccounts:signedin` (after a sign-in in a card opened with `dismissible: false`). ```js const user = await window.ktAccounts.ready; if (user) console.log(user.displayName, window.ktAccounts.can("role.developer")); const stop = window.ktAccounts.on("change", (u) => console.log(u ? "signed in" : "signed out")); ``` The kit follows `data-theme="light"` or `data-theme="dark"` on `` if the site sets it. --- # Troubleshooting > The usual symptoms when connecting a site, and what each one means. Open the browser's developer console first. Find the message below. ## The Account Button Is Missing | Console Shows | Meaning | | --- | --- | | `Access to script at 'https://accounts.aidenkt.com/kit/…' … blocked by CORS policy` | The site's origin is not registered on an app (or not on the allowed list) | | `GET https://accounts.aidenkt.com/kit/kit..js net::ERR_ABORTED 404` | The kit was updated and the old file is gone. Copy fresh tags from the app's page in the portal | | `Failed to find a valid digest in the 'integrity' attribute` | The tags do not match the deployed kit. Copy fresh tags; never edit a hash by hand or drop `integrity` | | A Content Security Policy message | The named directive is missing `https://accounts.aidenkt.com` | | Nothing | The script tag is missing, lacks `defer`, or CSS hides `[data-kt-account]`. `window.ktAccounts` is `undefined` when the script did not run | ## The Element Looks Empty Normal. The button is drawn inside a shadow root. `data-kt-ready` on the element says whether it was drawn. ## The Button Is a White Pill Although `line` Was Asked For The contrast check: the element has `data-variant="float" data-variant-wanted="line"`. The bar's text colour does not stand out from its `background-color`. A background image or gradient is not seen by the check, so give such a bar a `background-color` as well. ## `GET https://accounts.aidenkt.com/api/whoami 401` Normal when nobody is signed in. If someone just signed in, the browser is blocking cookies between subdomains, or the sign-in happened in another browser profile. ## CSP Violations The directive named at the end of the message is the one missing the accounts origin: `script-src` (the kit), `style-src` (its stylesheet), `font-src` (fonts), `img-src` (profile pictures), `connect-src` (`/api/whoami`; always signed out). ## OpenID Connect | Symptom | Meaning | | --- | --- | | KT Accounts shows an error page instead of returning to the app | The `client_id` or `redirect_uri` is wrong. Redirect URIs are compared exactly | | `invalid_grant` from the token endpoint | The code is older than 60 seconds, was already used, or the `redirect_uri` or `code_verifier` does not match | | `invalid_client` (401) | Wrong client secret, or a public client sent a secret it does not have | | `access_denied` on the callback | The person chose Cancel | | Tokens stop working | The person removed the app in Account Settings, the app was suspended, or the person's permissions or sign-in methods changed | ## The Page Shows Signed In, but the Server Helper Returns `null` Expected on a real site: a site's own server never receives the KT session cookie. See [Check Sign-In on a Server](/integrate/server-checks/). --- # Read Who Is Signed In > Ask KT Accounts who is signed in from a page on a registered aidenkt.com origin. A page on a registered `aidenkt.com` origin can ask who is signed in, with the visitor's cookie: ```js const r = await fetch("https://accounts.aidenkt.com/api/whoami", { credentials: "include" }); const me = r.ok ? await r.json() : null; // 401: nobody is signed in ``` ```json { "signedIn": true, "id": "9f2c…", "kind": "user", "displayName": "Aiden", "handle": "aiden", "avatarUrl": "https://accounts.aidenkt.com/media/…", "bannerUrl": "https://accounts.aidenkt.com/media/…", "isOwner": false, "permissions": ["brand.materials", "role.friend"], "badges": ["friend"], "apps": [{ "id": "…", "name": "Notes", "href": "https://notes.aidenkt.com/", "iconUrl": null, "builtin": null, "color": "kt-green", "accent": "#15DD7D" }] } ``` Signed out, the answer is `401 {"signedIn": false}`. `GET /v1/me` is the same endpoint. ## The Fields | Field | | | --- | --- | | `id` | The person's KT Accounts ID. The same value is `sub` in OpenID Connect | | `kind` | `user` or `agent` | | `displayName` | `""` when unset | | `handle` | Can be `null` | | `avatarUrl`, `bannerUrl` | Images served by KT Accounts | | `isOwner` | `true` for Aiden | | `permissions` | Named permissions, including the `role.` ones. See [Roles and Permissions](/integrate/roles-and-permissions/) | | `badges` | Badge keys in display order: `aiden`, `sysadmin`, `partner`, `friend`, `developer`, `agent`, `test` | | `apps` | The person's Your Apps tiles in order, already filtered by each app's audience. Left out when the apps cannot be read | It never includes an email address, a session, a token, an IP address or sign-in providers. `displayName` and `handle` are typed by the person, and `<`, `>` and `&` are allowed in a display name. Escape them before putting them into HTML (`textContent` and JSX do it for you). ## What It Is For What a page shows or hides with this is presentation only. A site's own server does not receive the KT Accounts session cookie: it is sent only to `accounts.aidenkt.com`. For anything that matters on a server, use [OpenID Connect](/integrate/openid-connect/) and keep your own session. Rate limit: 120 requests a minute per IP address. --- # Appearance > The app's colour from the aidenkt palette, and where its name, colour and icon appear. An app's look comes from three things: its **Name**, its **Colour** and its **Icon**. ## Colour The colour is one of the 40 colours of the aidenkt palette, stored by key. Nothing else is accepted, not even a hex value: the API answers `400 invalid_color`. The default is `kt-green`. The colour: - fills the app's tile in Your Apps and in the app switcher; - tints the sign-in page behind the card when someone signs in to the app; - shows behind the app's icon on the consent screen. Text and buttons on those pages stay KT Accounts' own, so every colour stays readable. In the portal, pick a swatch. Through the API, send the key: ```json { "color": "early-purple" } ``` The app object carries both the key (`color`) and its hex value (`accent`). The full list is in [Palette Keys](/reference/palette/). ## Icon Without an icon, the tile shows the app's first letter on its colour. To make one, see [Icon Maker](/portal/icon-maker/). ## Where the App Appears | Place | Shows | | --- | --- | | Your Apps and the app switcher | Icon, name, colour; links to the home page | | The sign-in page, when signing in to the app | Name, icon and colour, beside the KT mark | | The consent screen | Name, icon, colour, the home page's domain and where the person is sent back | | Account Settings, Connected Apps | Name, icon, and the person's identity inside the app | --- # Credentials and Rotation > The client ID, the client secret, how the secret is shown once, and how to rotate it. Each app has a **Client ID** (`ktc_…`). A confidential app also has a **client secret** (`kts_…`). Both are on the app's page in the portal under **Credentials**. ## The Client Secret The secret is shown once: - when the app is registered; - when it is rotated; - when a public client becomes confidential. Copy it then. KT Accounts keeps only a keyed hash of it and cannot show it again. The app object never contains it; `hasSecret` and `secretRotatedAt` say whether there is one and when it was made. Send it to the token endpoint as HTTP Basic credentials (`client_secret_basic`) or in the form (`client_secret_post`). Keep it on a server. A page or a mobile app cannot keep a secret, so it is a public client and uses PKCE instead. ## Rotate the Secret Choose **Rotate Secret** in the portal, or: ```sh curl -s -X POST https://accounts.aidenkt.com/api/agent/apps/$APP_ID/secret -H "Authorization: Bearer $TOKEN" ``` ```json { "clientId": "ktc_…", "clientSecret": "kts_…", "rotatedAt": 1794779798000 } ``` The old secret stops working at once. Access tokens already issued keep working until they expire (1 hour). A public client has no secret: `409 public_client`. 10 rotations an hour per app. ## Change the Client Type Changing **Client Type** from public to confidential makes a secret and shows it once. Changing it from confidential to public removes the secret; every sign-in then needs PKCE. --- # Icon Maker > Make an app's icon from an uploaded image, an icon from the pack on a palette colour, or one emoji on a palette colour. The icon maker is on the app's page in the portal. It has three tabs: **Upload Image**, **Icon** and **Emoji**. What an icon was made from is remembered, so the maker shows it again the next time. ## Upload Image Choose an image, crop it to a square, and it is saved as a 512 x 512 WebP (or JPEG where the browser cannot encode WebP). The browser removes metadata before uploading. Through the API, send the bytes yourself: exactly 512 x 512 pixels, WebP or JPEG, at most 400 KB, no EXIF, XMP or ICC. ```sh curl -s -X POST https://accounts.aidenkt.com/api/agent/apps/$APP_ID/icon -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: image/webp" --data-binary @icon.webp ``` `201 {"ok": true, "iconUrl": "…/….webp", "iconSpec": null}` ## Icon From the Pack Search the pack, pick an icon, pick a **Background** from the palette, and choose **Save Icon**. KT Accounts draws the icon itself as an SVG: the background colour filling a square, the icon centred in Black or Tech White, whichever reads on that colour. The pack holds: - the KT mark (`kt:mark`); - KT Accounts' own line icons (`kt:passkey`, `kt:mail`, `kt:agent`, `kt:device`, `kt:phone`, `kt:shield`, `kt:key`, `kt:settings`, `kt:pencil`, `kt:sprout`, `kt:code`, `kt:person`, `kt:list`, `kt:check`); - the badge icons (`badge:aiden`, `badge:sysadmin`, `badge:partner`, `badge:friend`, `badge:developer`, `badge:agent`, `badge:test`); - 214 icons from Phosphor Icons, regular weight (`ph:`, for example `ph:notebook`). Phosphor Icons are MIT licensed. Through the API: ```sh curl -s -X PUT https://accounts.aidenkt.com/api/agent/apps/$APP_ID/icon -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"kind":"glyph","glyph":"ph:notebook","background":"early-purple"}' ``` `201 {"ok": true, "iconUrl": "…/….svg", "iconSpec": {"kind": "glyph", "glyph": "ph:notebook", "background": "early-purple"}}` The canonical [agent guide](https://accounts.aidenkt.com/developers/agents.md) lists every glyph key. ## Emoji Type one emoji, pick a **Background**, and choose **Save Icon**. The emoji is drawn in the browser (only the browser has the emoji font) and uploaded like an image. Through the API, draw the emoji on the palette colour yourself at 512 x 512 and upload it with two query parameters: ```sh curl -s -X POST "https://accounts.aidenkt.com/api/agent/apps/$APP_ID/icon?emoji=%F0%9F%93%9D&background=early-purple" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: image/webp" --data-binary @icon.webp ``` The app then remembers `iconSpec: {"kind": "emoji", "emoji": "📝", "background": "early-purple"}`. `400 invalid_emoji` unless the value is exactly one emoji. Sending `{"kind": "emoji"}` to the `PUT` endpoint answers `400 emoji_needs_image`. ## Remove the Icon **Remove Icon** in the portal, or: ```sh curl -s -X DELETE https://accounts.aidenkt.com/api/agent/apps/$APP_ID/icon -H "Authorization: Bearer $TOKEN" ``` `200 {"ok": true, "iconUrl": null}`. The remembered `iconSpec` is forgotten too. ## Limits 10 icon changes an hour per app. App icons and in-app pictures share a daily upload budget, with a share of 40 a day per person. | Error | Meaning | | --- | --- | | `400 invalid_glyph` | Not a key from the pack | | `400 invalid_color` | The background is not a palette key | | `400 invalid_icon_kind` | `kind` is not `glyph` | | `400 invalid_emoji`, `emoji_needs_image` | See Emoji above | | `413 too_large` | Over 400 KB | | `415 invalid_image` | Not an acceptable image; `reason` says why | --- # Register an App > Who can register apps, every field on the New App form, and the rules each one follows. The [Developer Portal](https://accounts.aidenkt.com/developers) is for Aiden and people with the Developer badge, which Aiden gives. Agents holding `apps.manage` use the [agent API](/agents/overview/) with the same fields and rules. Choose **New App** and fill in the form. The app goes live as soon as you register it; there is no review step. Its page then shows the client ID, the client secret (once), and every integration snippet filled in for it. ## The Fields | Field | Rules | | --- | --- | | **Name** | Required. 1 to 40 characters. Shown on the app's tile, on the sign-in page and on the consent screen | | **Home Page** | Required. https. Where the tile in Your Apps links. It must be on one of the app's origins or on the host of one of its redirect URIs, and never on `accounts.aidenkt.com` or another site that is part of KT Accounts (`aidenkt.com`, `www.aidenkt.com` and the like) | | **Colour** | One of the 40 aidenkt palette colours. Default KT Green. See [Appearance](/portal/appearance/) | | **Who Sees It** | Who gets the app's tile. Default Everyone. See [Who Sees It](/portal/who-sees-it/) | | **Origins** | One per line, at most 10. Exact `https://host` or `https://host:port`, no path, no trailing slash, no wildcard. Each origin belongs to one app. Only origins on `aidenkt.com` get anything: the kit, who is signed in, and sign-out | | **Redirect URIs** | One per line, at most 10. For OpenID Connect from any domain. https, no fragment, no wildcard, no user info, not on `accounts.aidenkt.com`, and written exactly as a browser writes it | | **Client Type** | **Server (Has a Client Secret)** for a confidential client, or **Browser or Mobile (PKCE, No Secret)** for a public client. Default server | The app also gets a slug (2 to 32 of `a-z`, `0-9` and `-`), made from the name when you do not choose one. Slugs are unique. ## What Happens at Once - The app's tile appears in Your Apps and in the app switcher for the people who can see it. - Its `aidenkt.com` origins can load the kit, read who is signed in and sign the person out, within 30 seconds everywhere. - Its redirect URIs can receive OpenID Connect codes. A registered origin can never start a sign-in, run a passkey ceremony, or reach Account Settings. Signing in always happens on `accounts.aidenkt.com`. ## Limits 10 new apps an hour per person or agent, 30 a day per person, and at most 20 apps per person (an app an agent registers belongs to the agent's person). Aiden has no limit on the number of apps. ## Who Owns What A developer sees and changes only their own apps, including the apps their agents registered. Aiden sees and manages every app, and alone may suspend, resume, reorder Your Apps, or change the three built-in apps (aidenkt.com, Brand Book and The Garden). Built-in apps cannot be deleted. --- # Suspend and Delete > What suspending and deleting an app do, and who can do each. ## Suspend Only Aiden can suspend or resume an app, with **Suspend App** and **Resume App** on its page. Suspending: - stops its tile, its origins' access (the kit, who is signed in, sign-out) and its sign-ins at once, and everywhere within 30 seconds; - revokes its access tokens and deletes its unexchanged codes. Resuming brings the app back, but not the tokens and codes that were revoked. People sign in again. Removing a person's Developer badge suspends their apps, unless one of their agents still holds `apps.manage`. ## Delete The app's owner (or Aiden) can delete it with **Delete App**, or: ```sh curl -s -X DELETE https://accounts.aidenkt.com/api/agent/apps/$APP_ID -H "Authorization: Bearer $TOKEN" ``` `200 {"ok": true}`. Deleting ends every sign-in, token, consent and in-app identity of the app, and frees its origins and slug. It cannot be undone. The three built-in apps cannot be deleted (`409 builtin_app`). ## When a Person Removes an App A person can remove an app in Account Settings, under Connected Apps. That forgets their consent and their in-app identity, and ends the app's tokens and codes for that person. The next sign-in asks for consent again. --- # Who Sees It > The app's audience, which decides who gets its tile in Your Apps and the app switcher. **Who Sees It** decides who gets the app's tile in Your Apps and in the app switcher. Aiden sees every app. | Option in the Portal | `audience` Value | Who Gets the Tile | | --- | --- | --- | | Everyone | `everyone` | Every signed-in person | | Only Aiden | `owner` | Aiden | | People with a Role | `roles:,` | People holding any of the chosen roles | | People with `` | `permission:` | People holding that permission | ## Roles Choosing **People with a Role** shows the badges, each with its icon and name: Aiden, Sysadmin, Partner, Friend, Developer, Agent and Test. Tick any of them. The app is shown to people with any of the ticked roles. Every badge implies the permission `role.`, so `roles:friend,partner` means "holds `role.friend` or `role.partner`". `permission:role.friend` works too. Badge keys other than `aiden`, `sysadmin`, `partner`, `friend`, `developer`, `agent` and `test` are `400 invalid_audience`. See [Roles and Permissions](/integrate/roles-and-permissions/). ```json { "audience": "roles:friend,partner" } ``` ## What the Audience Does Not Do The audience only decides who sees the tile. It does not stop anyone from opening the app's home page or signing in to it. An app that must refuse people checks their permissions itself, for example from the `permissions` claim in OpenID Connect. --- # Access to These Docs > Who can read which pages of docs.aidenkt.com, and how people and agents sign in to read them. Most of this site is public. Some pages are for a narrower group. Every page has one of four levels: | Level | Who Can Read | | --- | --- | | Everyone | Anyone, without signing in | | Developers and Above | People with the Developer badge, their agents, Aiden's agents, and Aiden | | Agents and Above | Aiden's agents and Aiden | | Aiden Only | Aiden | An agent reads at its person's level, except that Aiden's own agents read at Agents and Above. An agent never reads more than its person. ## Signing In Choose **Log In** at the top right. It signs you in with KT Accounts and asks to share your permissions with the docs, which decide your level. The docs session lasts at most 8 hours. **Log Out** ends it; it does not sign you out of KT Accounts. A page you cannot read shows **Log In** when you are signed out, and says your account does not have access when you are signed in. Search, `/llms.txt`, `/llms-full.txt` and the sitemap leave out pages you cannot read. ## Agents Send an agent access token (`kta_…`, from `POST /api/agent/token`) as a bearer token. Never send the key itself; it goes only to KT Accounts. ```sh curl -s https://docs.aidenkt.com/agents/api.md -H "Authorization: Bearer $TOKEN" curl -s https://docs.aidenkt.com/llms-full.txt -H "Authorization: Bearer $TOKEN" ``` The docs check the token with KT Accounts (`GET /api/agent/whoami`) and keep the answer for up to a minute. A page above the agent's level answers `403 {"error": "forbidden"}`; no token, or a token that is not live, answers `401`. ## Changing a Page's Level Aiden changes a page's level with **Share** at the top right of the page. Everyone else sees only **Copy Link** there. A change applies at once on the server that saved it and everywhere within about a minute. --- # Endpoints > Every KT Accounts endpoint an app or agent uses, with its authentication. Base address `https://accounts.aidenkt.com`. JSON bodies unless stated; every JSON answer carries `Cache-Control: no-store`. ## Who Is Signed In | Method and Path | Auth | Notes | | --- | --- | --- | | `GET /api/whoami` (also `GET /v1/me`) | Session cookie, from a registered `aidenkt.com` origin | `200` the person, `401 {"signedIn": false}`. See [Read Who Is Signed In](/integrate/who-is-signed-in/) | | `GET /kit/manifest.json` | None | The current kit file names and their SRI hashes | | `GET /kit/kit..js`, `/kit/kit..css`, `/kit/fonts/*` | None | The sign-in kit. CORS for allowed and registered origins only | ## OpenID Connect | Method and Path | Auth | Notes | | --- | --- | --- | | `GET /.well-known/openid-configuration` | None | Discovery | | `GET /oauth/jwks.json` | None | ES256 public keys | | `GET /oauth/authorize` | The person signs in | `client_id`, `redirect_uri`, `response_type=code`, `scope`, `state`, `nonce`, `code_challenge`, `code_challenge_method=S256`, `prompt` (`none`, `consent`) | | `POST /oauth/token` | Client secret (Basic or form), or `client_id` alone for a public client | Form body. `grant_type=authorization_code`, `code`, `redirect_uri`, `code_verifier` | | `GET` or `POST /oauth/userinfo` | `Bearer kto_…` | Claims for the granted scopes | | `PUT /oauth/identity` | `Bearer kto_…` | `{displayName?, handle?, profileUrl?}` | | `DELETE /oauth/identity` | `Bearer kto_…` | | | `PUT /oauth/identity/picture` | `Bearer kto_…` | Raw 512 x 512 WebP or JPEG | | `DELETE /oauth/identity/picture` | `Bearer kto_…` | | The `/oauth/*` endpoints other than `/oauth/authorize` carry no cookies and answer `Access-Control-Allow-Origin: *` without credentials. ## Agents | Method and Path | Auth | Notes | | --- | --- | --- | | `POST /api/agent/token` | `Bearer ktk_…` (the key) | `{access_token: "kta_…", expires_in: 3600, scope}` | | `GET /api/agent/whoami` | `Bearer kta_…` | The agent, its current permissions, and whether its person is Aiden with their roles | | `POST /api/agent/authorize` | `Bearer kta_…` | `{permission}`: `200 {allowed: true}` or `403`. Audited | | `POST /api/agent/device/code` | None | Ask for access with a code a person approves | | `POST /api/agent/device/token` | None | Poll for the result | | `GET`, `POST /api/agent/apps` | `Bearer kta_…`, `apps.manage` | List, register | | `GET`, `PATCH`, `DELETE /api/agent/apps/:id` | `Bearer kta_…`, `apps.manage` | Read, change, delete | | `POST /api/agent/apps/:id/secret` | `Bearer kta_…`, `apps.manage` | Rotate the secret | | `POST`, `PUT`, `DELETE /api/agent/apps/:id/icon` | `Bearer kta_…`, `apps.manage` | Upload, make from the pack, remove | ## Developer Portal The portal's own API takes the same bodies and gives the same answers as the agent API. It needs a session on `accounts.aidenkt.com` with the Developer badge (or Aiden), a sign-in within the last 10 minutes for changes, and the accounts origin. | Method and Path | Notes | | --- | --- | | `GET`, `POST /api/developer/apps` | List, register | | `GET`, `PATCH`, `DELETE /api/developer/apps/:id` | Read, change, delete. `position` is Aiden's alone | | `POST /api/developer/apps/:id/secret` | Rotate the secret | | `POST`, `PUT`, `DELETE /api/developer/apps/:id/icon` | Upload, make from the pack, remove | | `POST /api/developer/apps/:id/suspend`, `…/resume` | Aiden only | ## Apps for the Signed-In Person | Method and Path | Auth | Notes | | --- | --- | --- | | `GET /api/apps` | Session | The person's Your Apps tiles | | `GET /api/apps/lookup?return_to=` | None | The active app a sign-in returns to, else `404` | | `GET /api/me/apps` | Session | The apps the person allowed, with their in-app identity | | `DELETE /api/me/apps/:appId` | Session | Removes the app for this person and ends its tokens | --- # Limits > Rate limits, sizes and lifetimes across KT Accounts. ## Rate Limits | What | Limit | | --- | --- | | Every dynamic route | 120 requests a minute per IP address (IPv6: per /64) | | `GET /api/whoami` | 120 a minute per IP address | | `POST /api/agent/token` | 30 a minute per IP address, 20 a minute per key | | New apps | 10 an hour per person or agent, 30 a day per person, 300 a day in all | | Apps per person | 20 (Aiden: no limit) | | Secret rotations | 10 an hour per app | | App icon changes | 10 an hour per app | | App and in-app picture uploads | 40 a day per person, 200 a day in all | | `GET /oauth/authorize` | 60 a minute per IP address | | `POST /oauth/token` | 60 a minute per IP address, 120 a minute per client | | In-app identity changes | 30 an hour per person and app | | In-app pictures | 10 a day per person and app | A `429` carries `Retry-After` in seconds. ## Sizes | What | Limit | | --- | --- | | App name | 40 characters | | Slug | 2 to 32 of `a-z`, `0-9`, `-` | | Origins, redirect URIs | 10 each per app | | App icon, in-app picture | 512 x 512, WebP or JPEG, 400 KB, no metadata | | In-app display name | 50 characters | | In-app handle | 1 to 40 characters | ## Lifetimes | What | Lifetime | | --- | --- | | Authorization code | 60 seconds, single use | | OpenID Connect access token, ID token | 1 hour. No refresh tokens | | Agent access token | 1 hour | | Agent key | 90 days by default, 1 to 365 days | | Device code | 10 minutes | | KT Accounts session | 7 days from sign-in | | Recent sign-in for sensitive changes | 10 minutes | | A registered origin's change reaching every server | Up to 30 seconds | --- # Palette Keys > The 40 aidenkt palette colours an app's colour and icon background can use. An app's `color`, and the `background` of an icon from the [icon maker](/portal/icon-maker/), is one of these keys. Nothing else is accepted, not even a hex value: `400 invalid_color`. The default is `kt-green`. The order is the brand book's. | | Key | Name | Hex | | --- | --- | --- | --- | | | `aiden-red` | Aiden Red | `#E41D4C` | | | `id-coral` | ID Coral | `#FF566A` | | | `3ds-red` | 3DS Red | `#B5121B` | | | `dragon-red` | Dragon Red | `#7D3230` | | | `strawberry-lemonade` | Strawberry Lemonade | `#F76D53` | | | `overwatch-orange` | Overwatch Orange | `#FF7D25` | | | `starlight-gold` | Starlight Gold | `#DCA021` | | | `berkeley-yellow` | Berkeley Yellow | `#FBC81F` | | | `website-lemon` | Website Lemon | `#F4DD75` | | | `glasses-green` | Glasses Green | `#919762` | | | `bot-green` | Bot Green | `#5FE21D` | | | `lax-mint` | LAX Mint | `#B6EAB6` | | | `reveria-green` | Reveria Green | `#36FD98` | | | `kt-green` | KT Green | `#15DD7D` | | | `matcha` | Matcha | `#0A864C` | | | `payback-pine` | Payback Pine | `#14452F` | | | `scout-teal` | Scout Teal | `#08C6BE` | | | `park-sky` | Park Sky | `#5FE4FF` | | | `airport-blue` | Airport Blue | `#69A6FD` | | | `stardust-blue` | Stardust Blue | `#225FFD` | | | `persian-sapphire` | Persian Sapphire | `#0445DF` | | | `space` | Space | `#1C3249` | | | `after-hours-indigo` | After Hours Indigo | `#252452` | | | `elementary-purple` | Elementary Purple | `#3F0585` | | | `turn-back-purple` | Turn Back Purple | `#6728EF` | | | `early-purple` | Early Purple | `#7367FF` | | | `coastal-lavender` | Coastal Lavender | `#D4CDFF` | | | `booster-orchid` | Booster Orchid | `#E46BF4` | | | `held-violet` | Held Violet | `#DE5BB6` | | | `disney-pink` | Disney Pink | `#FC2F99` | | | `creamsicle` | Creamsicle | `#FCE3C8` | | | `iced-latte` | Iced Latte | `#755640` | | | `mocha` | Mocha | `#563A26` | | | `tech-white` | Tech White | `#FDFFFF` | | | `house-white` | House White | `#FBF7F0` | | | `snow` | Snow | `#BAC1CB` | | | `titanium` | Titanium | `#8F8A82` | | | `iphone-gray` | iPhone Gray | `#534D5B` | | | `charcoal` | Charcoal | `#4D4947` | | | `black` | Black | `#0D0D0D` | --- # Scopes and Claims > The OpenID Connect scopes, the claims each one adds to the ID token and userinfo, and what each integration path can see. ## Scopes | Scope | Claims | Notes | | --- | --- | --- | | `openid` | `sub` | Required. The person's KT Accounts ID | | `profile` | `name`, `preferred_username`, `picture` | `preferred_username` is the handle, left out when the person has none | | `email` | `email`, `email_verified` | Only when the account has a verified address and the person ticks it on the consent screen | | `permissions` | `permissions`, `is_owner` | The person's permissions, including the `role.` ones, and whether they are Aiden | ## ID Token Claims | Claim | | | --- | --- | | `iss` | `https://accounts.aidenkt.com` | | `sub` | The person's KT Accounts ID | | `aud`, `azp` | Your client ID | | `iat`, `exp` | Issued at, and one hour later | | `auth_time` | When the person signed in | | `nonce` | The one you sent | | Scope claims | As in the table above | Signed with ES256 by a key in `/oauth/jwks.json`. ## What Each Path Can See | Path | Sees | | --- | --- | | Kit and `/api/whoami` | The signed-in person's ID, name, handle, picture, banner, permissions, badges and apps. Never an email address | | `openid` | The person's KT Accounts ID | | `profile` | Name, handle and picture | | `email` | The email address, only when verified and allowed by the person | | `permissions` | Permissions with role permissions, and `is_owner` | No path ever gives out a sign-in method, a session, a token of another app, or another app's data.