# Ident.ink Public API

Connect an application to a consenting Ident.ink account using OAuth 2.0 with
PKCE S256. The public API provides scoped identity, profiles, owned projects,
team memberships and subscription entitlements.

## Start here

1. Sign in at https://ident.ink/developers with your verified Ident.ink account.
2. Create or sign in to your linked developer account.
3. Register your application, exact callback URLs and the scopes it needs.
4. Complete application approval before offering the integration publicly.
5. Store confidential client credentials on your backend only.

API origin: `https://api.ident.ink`

Discovery: https://api.ident.ink/.well-known/oauth-authorization-server

OpenAPI: https://api.ident.ink/openapi.json

## Authorise an account

Create a fresh, cryptographically random `state` and PKCE verifier for each
attempt. Keep both in the initiating user's server session. A Node.js example:

```js
import { randomBytes, createHash } from 'node:crypto';

const state = randomBytes(32).toString('base64url');
const verifier = randomBytes(32).toString('base64url');
const challenge = createHash('sha256').update(verifier).digest('base64url');
const url = new URL('https://ident.ink/oauth/authorize');
url.search = new URLSearchParams({
  response_type: 'code',
  client_id: process.env.IDENT_CLIENT_ID,
  redirect_uri: process.env.IDENT_REDIRECT_URI,
  scope: 'identity profile projects:read',
  state,
  code_challenge: challenge,
  code_challenge_method: 'S256',
}).toString();
// Save state and verifier to this user's session, then redirect to url.href.
```

At your callback, reject a missing or mismatched state before exchanging the
code. Handle an OAuth error or declined consent without attempting an exchange.
Use the same registered redirect URI and the original verifier:

```js
const response = await fetch('https://api.ident.ink/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: process.env.IDENT_CLIENT_ID,
    redirect_uri: process.env.IDENT_REDIRECT_URI,
    code: callbackCode,
    code_verifier: sessionVerifier,
    // Confidential clients also send their server-held client_secret.
  }),
  signal: AbortSignal.timeout(15000),
});
const tokens = await response.json();
if (!response.ok) throw new Error(tokens.error || 'token_exchange_failed');
// Store tokens securely on the server. Never log this response.
```

Do not treat this as OpenID Connect ID-token validation: consume the supported
userinfo endpoint and use its stable `sub` as the account identifier. Email can
change and must not be your permanent account key.

## Available endpoints

| Method and path | Required scope | Response |
| --- | --- | --- |
| GET `/oauth/userinfo` | `identity` | `sub`; consented email/profile fields |
| GET `/v1/me` | `profile` | Account ID and profile fields |
| GET `/v1/profile` | `network:read` | `{ "profile": ... }`, possibly null |
| GET `/v1/projects` | `projects:read` | `{ "items": [...] }`, up to 100 owned projects |
| GET `/v1/teams` | `teams:read` | `{ "items": [...] }`, up to 100 active memberships |
| GET `/v1/entitlements` | `subscriptions:read` | Plan, status and feature entitlements |

The `email` scope adds email information where supported. Request only scopes
your feature uses; approval and user consent determine actual access.

```js
const response = await fetch('https://api.ident.ink/v1/projects', {
  headers: { Authorization: `Bearer ${accessToken}` },
  signal: AbortSignal.timeout(15000),
});
if (!response.ok) throw new Error(`Ident API returned ${response.status}`);
const { items } = await response.json();
```

These routes do not offer arbitrary database access, project writes, payment
execution or admin privileges. `/private/v1/*` is a separate restricted
integration API and cannot be unlocked by requesting public scopes.

## Refresh and disconnect

POST form-encoded `grant_type=refresh_token`, `client_id` and `refresh_token`
to `/oauth/token`, including client authentication where required. Persist the
new token response atomically and retire replaced tokens. Coordinate refreshes
per account so simultaneous requests do not race.

POST form-encoded `token` to `/oauth/revoke` to disconnect. Success returns HTTP
200 with an empty body; do not require JSON. Remove locally stored credentials
and offer reauthorisation when an account revokes access.

## Errors and operational behaviour

OAuth errors use `error` and `error_description`. Treat 401 as a credential
problem, 403 as insufficient access, and 429 as throttling. Honour Retry-After
when present and use bounded backoff with jitter for transient failures. Do not
automatically replay payments or other writes after an uncertain outcome.

Use HTTPS, bounded timeouts and server-side token storage. Never put secrets in
query strings, browser logs or support messages. Scope access to the signed-in
user; do not cache one account's response globally. Test declined consent,
expired codes, revoked access, missing scopes and upstream downtime.

Support: https://support.ident.ink

This README documents implemented routes. It does not promise future endpoints,
unlimited request capacity, write APIs or access to private platform data.
