# KID Identity Provider (KID IdP) — Integration Reference for AI
This document gives a complete picture of how to integrate KID OAuth into any application.
It is written for external developers and AI assistants implementing KID OAuth from scratch.
**Base URL (production):** https://api.kid.koompi.org
**Dashboard:** https://dash.kid.koompi.org
**OIDC Discovery:** https://api.kid.koompi.org/.well-known/openid-configuration
---
## What Is KID IdP?
KID IdP is a standards-compliant OAuth 2.0 / OpenID Connect (OIDC) authorization server
with Web3 integration. Every user automatically receives an EVM-compatible blockchain
wallet on signup.
Key properties:
- One KID identity per user, regardless of login method (Google, Apple, email, Telegram)
- Every user has a `wallet_address` (Selendra EVM chain) automatically generated on signup
- Human-readable identity number (`kid`) assigned to every user, e.g. `KID-000001`
- Full OIDC compliance — works as a drop-in SSO provider for Moodle, WordPress, Grafana, etc.
- SDK available: `@kid-oauth/sdk` (npm)
---
## Getting Started
1. Sign in to https://dash.kid.koompi.org
2. Create a new project → you receive:
- `client_id` (development: `pk_test_...`, production: `pk_live_...`)
- `client_secret`
3. Add your `redirect_uri` to the project's allowed redirect URIs
4. Select which scopes your project is allowed to request
5. Use the credentials to initiate the OAuth flow below
---
## OAuth 2.0 Authorization Code Flow
KID uses the Authorization Code flow with server-side PKCE. Your app does not need to
generate PKCE — KID handles it automatically.
### Step 1 — Redirect user to KID
```
GET https://api.kid.koompi.org/oauth
?client_id=pk_test_YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=openid profile.basic profile.contact wallet.read
&nonce=RANDOM_NONCE
```
Only `client_id` and `redirect_uri` are required. KID handles `response_type`, PKCE
(S256), and CSRF state automatically.
After the user authenticates, KID redirects to:
```
https://yourapp.com/callback?code=AUTH_CODE&state=STATE&scope=...
```
### Step 2 — Exchange code for tokens
```http
POST https://api.kid.koompi.org/oauth/token
Content-Type: application/json
{
"grant_type": "authorization_code",
"code": "AUTH_CODE",
"client_id": "pk_test_YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"redirect_uri": "https://yourapp.com/callback",
"state": "STATE_FROM_CALLBACK"
}
```
Response:
```json
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "a3f9c2e1...",
"refresh_token_expires_in": 2592000,
"scope": "openid profile.basic profile.contact wallet.read",
"user_id": "507f1f77bcf86cd799439011",
"user": {
"sub": "507f1f77bcf86cd799439011",
"kid": "KID-000001",
"name": "John Doe",
"given_name": "John",
"family_name": "Doe",
"preferred_username": "johndoe",
"picture": "https://...",
"updated_at": 1715000000,
"email": "john@example.com",
"email_verified": true,
"phone_number": "+855...",
"phone_number_verified": false,
"wallet_address": "0x742d35Cc...",
"public_key": "0x04a1b2c3..."
},
"id_token": "eyJ..."
}
```
> `id_token` is only included when `openid` scope is requested. It is RS256-signed.
The `user` object follows the OpenID Connect Core 1.0 standard claims (with KID
extensions `kid`, `wallet_address`, `public_key`, `telegram_id`). Field presence is
filtered by granted scopes — the same shape is returned by `GET /oauth/userinfo`.
### Step 3 — Use the access token
```http
GET https://api.kid.koompi.org/oauth/userinfo
Authorization: Bearer ACCESS_TOKEN
```
### Step 4 — Refresh the access token
```http
POST https://api.kid.koompi.org/oauth/refresh
Content-Type: application/json
{
"grant_type": "refresh_token",
"refresh_token": "REFRESH_TOKEN",
"client_id": "pk_test_YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}
```
Response shape is identical to the token exchange response.
**Important:** Every refresh issues a new `refresh_token` and immediately invalidates the
old one. Always store and use the latest refresh token.
### Step 5 — Revoke token (Sign Out)
```http
POST https://api.kid.koompi.org/oauth/revoke
Content-Type: application/json
{
"token": "REFRESH_TOKEN",
"client_id": "pk_test_YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}
```
To sign the user out of **all sessions** for your project:
```json
{
"revoke_all": true,
"client_id": "pk_test_YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}
```
Always returns `200 OK` even if the token is invalid (RFC 7009).
---
## Client Authentication
All token, refresh, and revoke endpoints accept credentials in two formats:
**JSON body (recommended):**
```json
{ "client_id": "pk_test_...", "client_secret": "..." }
```
**HTTP Basic auth:**
```
Authorization: Basic base64(client_id:client_secret)
```
---
## Scopes
### Standard OIDC scopes
| Scope | Data returned |
|---|---|
| `openid` | Enables OIDC mode — adds `id_token` to response |
| `profile` | `name`, `given_name`, `family_name`, `preferred_username`, `picture` |
| `email` | `email`, `email_verified` |
| `phone` | `phone_number`, `phone_number_verified` |
### KID custom scopes
| Scope | Data returned |
|---|---|
| `profile.basic` | `kid`, `name`, `given_name`, `family_name`, `preferred_username`, `picture`, `created_at`, `updated_at` |
| `profile.contact` | `email`, `phone_number` |
| `profile.telegram` | Telegram provider info |
| `account.status` | `is_confirmed`, account type flags |
| `wallet.read` | `wallet_address`, `public_key` |
| `wallet.sign` | Allows signing messages/transactions on behalf of user |
| `wallet.send` | Allows broadcasting transactions on behalf of user |
| `wallet.mnemonic` | Raw mnemonic — highly sensitive, requires explicit approval |
**Default scopes** (when none requested): `profile.basic`, `profile.contact`, `wallet.read`.
**Sensitive scopes** (`wallet.sign`, `wallet.send`, `wallet.mnemonic`) must be explicitly
requested AND enabled on your project. They are never granted implicitly.
---
## Userinfo Claims Reference
Returned in the `user` field of the token response and from `GET /oauth/userinfo`.
All names follow the OpenID Connect Core 1.0 standard.
| Claim | Scope | Description |
|---|---|---|
| `sub` | always | Stable unique identifier — use as your foreign key |
| `kid` | always | Human-readable identity number, e.g. `KID-000001` |
| `name` | `profile` / `profile.basic` | Full display name |
| `given_name` | `profile` / `profile.basic` | First name |
| `family_name` | `profile` / `profile.basic` | Last name |
| `preferred_username` | `profile` / `profile.basic` | Username |
| `picture` | `profile` / `profile.basic` | Avatar URL |
| `updated_at` | `profile` / `profile.basic` | Unix seconds — last profile update |
| `email` | `email` / `profile.contact` | Email address |
| `email_verified` | `email` / `profile.contact` | Boolean |
| `phone_number` | `phone` / `profile.contact` | Phone number (E.164) |
| `phone_number_verified` | `phone` / `profile.contact` | Boolean |
| `telegram_id` | `profile.telegram` | Numeric Telegram user ID |
| `wallet_address` | `wallet.read` | EVM wallet address (Selendra chain) — KID extension |
| `public_key` | `wallet.read` | EVM public key — KID extension |
> **Note:** `kid` (identity number like "KID-000001") is a KID-specific claim and is
> different from the JWT `kid` header (key ID used for signature verification).
---
## id_token (OIDC)
When `openid` is in the requested scopes, the token response includes an `id_token`.
It is a RS256-signed JWT with the following payload:
```json
{
"iss": "https://api.kid.koompi.org",
"aud": "YOUR_CLIENT_ID",
"sub": "USER_MONGODB_ID",
"kid": "KID-000001",
"name": "John Doe",
"email": "john@example.com",
"at_hash": "...",
"nonce": "YOUR_NONCE",
"iat": 1700000000,
"exp": 1700003600
}
```
Verify the signature using the public key from:
```
GET https://api.kid.koompi.org/.well-known/jwks.json
```
---
## OIDC Discovery
```
GET https://api.kid.koompi.org/.well-known/openid-configuration
```
```json
{
"issuer": "https://api.kid.koompi.org",
"authorization_endpoint": "https://api.kid.koompi.org/oauth",
"token_endpoint": "https://api.kid.koompi.org/oauth/token",
"userinfo_endpoint": "https://api.kid.koompi.org/oauth/userinfo",
"jwks_uri": "https://api.kid.koompi.org/.well-known/jwks.json",
"revocation_endpoint": "https://api.kid.koompi.org/oauth/revoke",
"scopes_supported": ["openid", "profile", "email", "phone", "profile.basic", "profile.contact", "wallet.read", "..."],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"id_token_signing_alg_values_supported": ["RS256"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
"code_challenge_methods_supported": ["S256"]
}
```
---
## SDK Integration (`@kid-oauth/sdk`)
### Install
```bash
npm install @kid-oauth/sdk
# or
pnpm add @kid-oauth/sdk
```
### Next.js App Router
```ts
// app/api/auth/callback/route.ts
import { createTokenHandler } from '@kid-oauth/sdk/server';
const handler = createTokenHandler({
clientId: process.env.KID_CLIENT_ID!,
clientSecret: process.env.KID_CLIENT_SECRET!,
redirectUri: process.env.KID_REDIRECT_URI!,
});
export const POST = handler;
// app/api/auth/refresh/route.ts
import { createRefreshHandler } from '@kid-oauth/sdk/server';
const handler = createRefreshHandler({
clientId: process.env.KID_CLIENT_ID!,
clientSecret: process.env.KID_CLIENT_SECRET!,
});
export const POST = handler;
// app/api/auth/revoke/route.ts
import { createRevokeHandler } from '@kid-oauth/sdk/server';
const handler = createRevokeHandler({
clientId: process.env.KID_CLIENT_ID!,
clientSecret: process.env.KID_CLIENT_SECRET!,
});
export const POST = handler;
```
### React Provider
```tsx
// app/layout.tsx
import { KIDProvider } from '@kid-oauth/sdk/react';
export default function RootLayout({ children }) {
return (
{children}
);
}
```
### React Hooks & Components
```tsx
import { useKID, SignInButton, SignOutButton, UserButton } from '@kid-oauth/sdk/react';
function MyComponent() {
const { user, isLoaded, isSignedIn } = useKID();
if (!isLoaded) return null;
if (!isSignedIn) return ;
return (
Hello, {user.name}
Wallet: {user.wallet_address}
);
}
```
### Server-side token exchange (`KIDOAuthServer`)
```ts
import { KIDOAuthServer } from '@kid-oauth/sdk/server';
const kid = new KIDOAuthServer({
clientId: process.env.KID_CLIENT_ID!,
clientSecret: process.env.KID_CLIENT_SECRET!,
redirectUri: process.env.KID_REDIRECT_URI!,
});
// Exchange code for tokens
const tokens = await kid.exchangeCode({ code, state });
// Get user info
const userinfo = await kid.getUserInfo(tokens.access_token);
// Refresh tokens
const newTokens = await kid.refreshToken({ refresh_token: tokens.refresh_token });
// Revoke session
await kid.revokeToken({ token: tokens.refresh_token });
```
### Token Response Shape
```ts
interface TokenResponse {
access_token: string;
token_type: 'Bearer';
expires_in: number; // 3600 seconds
refresh_token?: string;
refresh_token_expires_in?: number; // 2592000 seconds (30 days)
scope?: string; // space-separated granted scopes
id_token?: string; // RS256 JWT, only when openid scope
user_id?: string; // canonical user id (same as user.sub)
user?: UserInfoResponse; // OIDC-standard claims
}
interface UserInfoResponse {
sub: string;
kid?: string | null;
name?: string;
given_name?: string;
family_name?: string;
preferred_username?: string;
picture?: string;
updated_at?: number;
email?: string;
email_verified?: boolean;
phone_number?: string;
phone_number_verified?: boolean;
telegram_id?: number | null;
wallet_address?: string;
public_key?: string;
}
```
---
## Wallet API
After a user authenticates with `wallet.read` or higher wallet scopes, you can interact
with their Selendra EVM wallet on their behalf.
All wallet endpoints require `Authorization: Bearer ACCESS_TOKEN`.
### Get wallet info
```http
GET https://api.kid.koompi.org/wallet/info
Authorization: Bearer ACCESS_TOKEN
```
```json
{
"address": "0x742d35Cc...",
"public_key": "0x04a1b2c3...",
"network": "selendra",
"chain_id": 1961
}
```
### Sign a message (scope: `wallet.sign`)
```http
POST https://api.kid.koompi.org/wallet/sign-message
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"message": "Verify my identity",
"message_format": "text"
}
```
```json
{
"status": "SUCCESS",
"signature": "0x8a9c7b3d...",
"message_hash": "0xdef456...",
"signer": "0x742d35Cc..."
}
```
`message_format`: `"text"` (adds Ethereum personal sign prefix) or `"hex"` (raw bytes).
### Sign a transaction without broadcasting (scope: `wallet.sign`)
```http
POST https://api.kid.koompi.org/wallet/sign-transaction
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"transaction": {
"to": "0xRecipient...",
"value": "1000000000000000000",
"data": "0x",
"gas_limit": "21000",
"max_fee_per_gas": "1000000000",
"max_priority_fee_per_gas": "1000000"
},
"chain_id": 1961
}
```
```json
{
"status": "SUCCESS",
"signed_transaction": "0xf86c...",
"transaction_hash": "0xabc123...",
"from": "0x742d35Cc...",
"nonce": 42
}
```
### Send a transaction (scope: `wallet.send`)
KID signs and broadcasts. Rate limit: 10 requests / 15 min per user.
Supports automatic gas sponsorship — if user balance is below 0.01 SEL, the relayer
tops up to 0.5 SEL before sending.
```http
POST https://api.kid.koompi.org/wallet/send-transaction
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"transaction": {
"to": "0xRecipient...",
"value": "1000000000000000000"
},
"idempotency_key": "order-123",
"wait_for_confirmation": false,
"webhook_url": "https://yourapp.com/webhook"
}
```
For a registered contract call:
```json
{
"contract_call": {
"contract": "my-token",
"method": "transfer",
"params": ["0xRecipient...", "10000000000000000000"]
},
"idempotency_key": "order-123"
}
```
```json
{
"status": "SUCCESS",
"transaction_hash": "0xdef789...",
"from": "0x742d35Cc...",
"tx_status": "pending",
"gas_sponsored": true
}
```
### Check transaction status (no auth required)
```http
GET https://api.kid.koompi.org/wallet/tx-status/0xdef789...
```
```json
{ "tx_status": "confirmed", "block_number": 12345678, "gas_used": "21000" }
```
### Broadcast a pre-signed transaction from your project's wallet
Your project signs the transaction client-side; KID only relays it.
Use your project credentials in headers (not a user Bearer token).
```http
POST https://api.kid.koompi.org/wallet/broadcast
x-kid-public-key: YOUR_KID_PUBLIC_KEY
x-kid-secret-key: YOUR_KID_SECRET_KEY
Content-Type: application/json
{
"signed_tx": "0xf86c...",
"idempotency_key": "order-456",
"webhook_url": "https://yourapp.com/webhook"
}
```
---
## User Lookup API (Project credentials)
List users who have authorized your project, or look up a specific user.
```http
GET https://api.kid.koompi.org/user/list
x-kid-public-key: YOUR_KID_PUBLIC_KEY
x-kid-secret-key: YOUR_KID_SECRET_KEY
```
Look up a user by their Telegram ID:
```http
GET https://api.kid.koompi.org/user/telegram/TELEGRAM_USER_ID
x-kid-public-key: YOUR_KID_PUBLIC_KEY
x-kid-secret-key: YOUR_KID_SECRET_KEY
```
---
## Social Login Providers
KID supports the following login methods out of the box:
- **Google** — OAuth 2.0
- **Apple** — Sign in with Apple (id_token verification)
- **Telegram** — Bot auth, Telegram Mini App, or project-key-based login
- **Email + password** — via Better Auth endpoints at `/api/auth/*`
Your app does not need to integrate with these providers directly. KID handles all
provider authentication and presents a single unified user to your app.
---
## Consent Management
Users can revoke your app's access at any time via their KID account settings.
Your app can also check or revoke consent programmatically.
List consents granted to your project:
```http
GET https://api.kid.koompi.org/consent
Authorization: Bearer ACCESS_TOKEN
```
Revoke consent for your project:
```http
DELETE https://api.kid.koompi.org/consent/YOUR_CLIENT_ID
Authorization: Bearer ACCESS_TOKEN
```
---
## Third-Party Platform Setup (Moodle, WordPress, Grafana, etc.)
KID is a fully compliant OIDC provider. Any platform that supports OAuth2/OIDC can use
KID as an SSO provider using the discovery URL for auto-configuration.
**Discovery URL:** `https://api.kid.koompi.org/.well-known/openid-configuration`
**Manual endpoint configuration:**
| Setting | Value |
|---|---|
| Authorization endpoint | `https://api.kid.koompi.org/oauth` |
| Token endpoint | `https://api.kid.koompi.org/oauth/token` |
| Userinfo endpoint | `https://api.kid.koompi.org/oauth/userinfo` |
| JWKS URI | `https://api.kid.koompi.org/.well-known/jwks.json` |
| Scopes | `openid profile email` |
**User field mappings:**
| Platform field | KID claim |
|---|---|
| Username | `preferred_username` or `email` |
| Email | `email` |
| First name | `given_name` |
| Last name | `family_name` |
| Profile picture | `picture` |
| Unique ID | `sub` |
### Moodle
1. Create project in KID dashboard → copy `client_id` and `client_secret`
2. Add redirect URI: `https://your-moodle.com/admin/oauth2callback.php`
3. Moodle: Site Admin → Server → OAuth 2 services → New custom service
4. Set base URL: `https://api.kid.koompi.org` and enable auto-discovery
5. Map fields as shown above and enable "Create new accounts automatically"
---
## API Endpoint Summary
```
GET /oauth Initiate OAuth flow (redirect user here)
GET /oauth/authorize Issue auth code after user authenticates
POST /oauth/token Exchange authorization code for tokens
POST /oauth/refresh Rotate refresh token, get new access token
POST /oauth/revoke Revoke refresh token (RFC 7009)
GET /oauth/userinfo Get user info (Bearer token required)
GET /oauth/google Initiate Google login
GET /oauth/google/callback Google callback
GET /oauth/apple Initiate Apple login
POST /oauth/apple/callback Apple callback
POST /oauth/telegram/callback Telegram bot auth
GET /oauth/telegram/callback Token exchange with project key
POST /oauth/telegram/app Telegram Mini App login
GET /wallet/info User's wallet address & public key
GET /wallet/tx-status/:hash Transaction status (public)
POST /wallet/sign-message Sign a message
POST /wallet/sign-transaction Sign a transaction (no broadcast)
POST /wallet/send-transaction Sign & broadcast a transaction
POST /wallet/broadcast Broadcast pre-signed tx from project wallet
GET /user/me Get authenticated user (Bearer token; includes
KID-internal fields like status/role/providers)
GET /user/list List project users (project credentials)
GET /user/telegram/:id Look up user by Telegram ID
GET /consent List user's consents
DELETE /consent/:clientId Revoke consent
GET /health Service health status
GET /.well-known/openid-configuration OIDC discovery document
GET /.well-known/jwks.json Public key set (RS256)
```
---
## When to Use Which User Endpoint
| Endpoint | Use case | Returns |
|---|---|---|
| `GET /oauth/userinfo` | Standard OIDC clients (Moodle, WordPress, Grafana) and any flow scoped by OAuth permissions | OIDC claims + legacy aliases, **filtered by token scopes** |
| `GET /user/me` | First-party apps that need KID-internal fields (status, role, providers, locale) | OIDC claims + legacy aliases + KID-internal fields, **all available data** |
**Rule of thumb:**
- Building a generic OIDC integration → use `/oauth/userinfo`. It's standards-compliant and respects your declared scopes. This is what Moodle/WordPress/Grafana hit automatically.
- Building a first-party KID app that needs admin-style fields → use `/user/me`. It returns extra fields like `status`, `role`, `is_confirmed`, `providers`, `locale`, `timezone`, `last_login_at` that are not part of OIDC userinfo.
Both endpoints return the same OIDC-standard claims for shared fields, so swapping between them does not require remapping.
---
## Common Integration Mistakes
- **Do not generate your own PKCE.** KID handles PKCE server-side. Just pass `state`
from the callback into the token request.
- **Do not forward `state` as a parameter name in the token body thinking it is the auth state.**
In the token request, `state` is used by KID to look up the server-side PKCE record.
It is the same `state` value you received in the callback query string.
- **Always store the new refresh token after every refresh call.** The old one is immediately
invalidated — replaying it will revoke all your sessions for that user.
- **Do not request `wallet.sign` or `wallet.send` unless your project explicitly needs them.**
These scopes require your project to have them enabled in the dashboard.
- **`sub` is the stable user identifier** — use it as your foreign key, not `email` or
`preferred_username` which can change.
- **`kid` in userinfo (e.g. `KID-000001`) is the human identity number**, not the JWT `kid`
header used for key rotation. They are unrelated.
- **Development vs production keys are separate.** `pk_test_` keys work in development mode;
`pk_live_` keys require production approval. Do not mix them.
- **`id_token` is only included when you request the `openid` scope.** Without it, you get
`access_token` + `refresh_token` only.
- **The token response has a single `user` field — not separate `user` and `userinfo`.**
All OIDC-standard claims are flat under `user`. Older docs may reference a separate
`userinfo` object; it does not exist.
---
## Environment & Credentials
| Credential type | Prefix | Use case |
|---|---|---|
| Development client ID | `pk_test_...` | Local development, testing |
| Production client ID | `pk_live_...` | Live users (requires dashboard approval) |
| Project public key | `kid_pk_...` | Server-to-server calls (user list, broadcast) |
| Project secret key | `kid_sk_...` | Server-to-server calls (keep secret) |
Never expose `client_secret`, `kid_secret_key`, or any `sk_` key to the browser or
mobile client. These must only be used server-side.