OIDC clients
An OIDC client is an app (web, mobile, CLI, or service) registered to authenticate users via Huudis. Each client gets a client_id (and, for confidential clients, a client_secret), a set of allowed redirect_uris, and an allowed scopes list. Registering one is the prerequisite to using any of the /oidc/* endpoints.
This page is about managing clients. For the wire-level OIDC protocol (authorize, token, refresh, userinfo) see OIDC overview.
All requests on this page require a bearer admin JWT — see Authentication.
The client secret is returned exactly once, at create. Huudis stores only a bcrypt hash. If you lose the secret, the only recovery is to rotate it — there is no fetch-secret endpoint.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/oidc/clients |
List clients visible to the active workspace |
POST |
/v1/oidc/clients |
Register a new client |
PATCH |
/v1/oidc/clients/:id |
Edit name, URIs, scopes, logo |
POST |
/v1/oidc/clients/:id/rotate-secret |
Mint a new secret, hash-revoke the old |
DELETE |
/v1/oidc/clients/:id |
Delete a client |
First-party clients (Huudis's own huudis-dashboard, the Plugipay portal, etc.) appear in GET results but are read-only — PATCH, rotate, and delete return 403 FORBIDDEN. You only manage your own (isFirstParty: false).
List clients
GET /v1/oidc/clients
Returns clients visible in the active workspace:
- Every client owned by the active workspace.
- First-party clients whose service is in the workspace's
enabledServiceslist (so the Plugipay portal client appears once you've enabled Plugipay).
First-party clients are returned with isFirstParty: true so the UI can show them as a different tier.
Response — 200 OK
{
"data": [
{
"id": "oc_01KPG40HMM…",
"accountId": "acc_01KPG30…",
"clientId": "oc_a4c2b1f8d6e9",
"name": "MejaStudio",
"redirectUris": ["https://mejastudio.com/callback"],
"scopes": ["openid", "profile", "email"],
"isFirstParty": false,
"logoUrl": null,
"hasSecret": true,
"createdAt": "2026-04-15T09:30:00.000Z",
"updatedAt": "2026-05-01T14:22:00.000Z"
}
]
}
hasSecret: false means the client was registered as public and uses PKCE without a confidential secret — the typical setup for SPAs and CLIs.
Register a client
POST /v1/oidc/clients
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string (1–120) | yes | Display name (shown on the consent screen for third-party clients). |
redirectUris |
string[] | no | HTTPS URLs Huudis will allow as redirect_uri on /oidc/authorize. Default []. Up to 20. |
scopes |
string[] | no | Scopes the client may request. Default ["openid", "profile", "email"]. Extra scopes must be ones Huudis advertises in /.well-known/openid-configuration. |
public |
boolean | no | When true, the client doesn't hold a secret — PKCE is required. Default false. Use for SPAs, mobile apps, and CLIs. |
logoUrl |
string (URL, ≤500) | no | Square logo shown on the consent screen. Optional. |
Response — 201 Created
{
"data": {
"id": "oc_01KPG50…",
"accountId": "acc_01KPG30…",
"clientId": "oc_4f2a1b9c8d7e",
"name": "MejaStudio",
"redirectUris": ["https://mejastudio.com/callback"],
"scopes": ["openid", "profile", "email"],
"isFirstParty": false,
"logoUrl": null,
"hasSecret": true,
"clientSecret": "cs_lW8M…redacted…3kQpY",
"createdAt": "2026-05-12T23:10:00.000Z",
"updatedAt": "2026-05-12T23:10:00.000Z"
}
}
clientSecret is shown exactly once on create. Capture it and store in your secret manager. Subsequent GET calls will return the same shape but without clientSecret.
Errors specific to this endpoint
| Status | error.code |
When |
|---|---|---|
400 |
VALIDATION_ERROR |
Field shape wrong, URI not HTTPS (except http://localhost* for dev), unknown scope. |
400 |
NO_ACTIVE_WORKSPACE |
The bearer token has no activeAccountId set. |
Update a client
PATCH /v1/oidc/clients/:id
Partial update. Send only the fields you want to change. clientId, hasSecret, and accountId are immutable.
Request body
| Field | Type | Description |
|---|---|---|
name |
string | New display name. |
redirectUris |
string[] | Replace the redirect-URI allowlist. To add a URI, send the full new list. |
scopes |
string[] | Replace the scope list. |
logoUrl |
string | null | New logo URL, or null to clear. |
Errors
| Status | error.code |
When |
|---|---|---|
403 |
FORBIDDEN |
The client is first-party, or owned by a different workspace. |
404 |
NOT_FOUND |
No such client. |
Rotate the client secret
POST /v1/oidc/clients/:id/rotate-secret
Mints a new secret and hashes-revokes the old. The new plaintext is returned exactly once on this response.
Response — 200 OK
{ "data": { "clientSecret": "cs_n8jM…redacted…W2vR" } }
Errors
| Status | error.code |
When |
|---|---|---|
400 |
PUBLIC_CLIENT |
The client was registered as public — there is no secret to rotate. |
403 |
FORBIDDEN |
First-party or cross-workspace. |
After rotation, any existing access tokens issued by this client remain valid until they expire (typically ≤1 hour) — rotation does not retroactively invalidate sessions. Refresh attempts with the old secret fail immediately.
Delete a client
DELETE /v1/oidc/clients/:id
Removes the client. All existing access tokens and refresh tokens issued by it stop working immediately; any active sessions get invalid_client on next refresh.
204 No Content on success.
The OIDC client object
| Field | Type | Description |
|---|---|---|
id |
string (oc_…) |
Internal row ID. Used for PATCH, rotate, and delete. |
accountId |
string (acc_…) |
Workspace that owns the client. null for global first-party clients. |
clientId |
string (oc_…) |
Public OAuth client_id. Sent on every /oidc/authorize request. |
name |
string | Display name. Shown on the consent screen. |
redirectUris |
string[] | Allowed redirect_uri values. Exact-match. |
scopes |
string[] | Scopes this client may request. |
isFirstParty |
boolean | A built-in Forjio client (e.g., plugipay-portal). Read-only. |
logoUrl |
string | null | Consent-screen logo. |
hasSecret |
boolean | false for public clients (PKCE-only). |
createdAt, updatedAt |
ISO 8601 |
The plaintext clientSecret appears only on create and rotate responses.
Public vs. confidential clients
| Client kind | public: true at create |
Authentication on /oidc/token |
|---|---|---|
| Server-side web app | no | client_id + client_secret (basic auth or post) |
| Browser SPA | yes | client_id + PKCE verifier (no secret) |
| Native / mobile app | yes | client_id + PKCE verifier |
| CLI | yes | client_id + PKCE verifier (device flow) |
| Confidential service-to-service | no | client_id + client_secret |
Huudis enforces PKCE for every client, public or confidential. Confidential clients additionally send their secret.
First-party vs. third-party
A first-party client is one Forjio operates — the Plugipay portal, the Storlaunch dashboard, etc. Their consent screen is skipped (users already trust the brand). They're visible in your workspace's client list only when you opt into the corresponding service via Services.
A third-party client is one you (or a customer of yours) registered. Users get a one-time consent screen the first time they sign in — "MejaStudio wants to access your email and profile; allow?" — with their decision recorded in Consents.
You cannot create first-party clients via the API. Huudis seeds them.
Events
| Event type | Fires on |
|---|---|
huudis.oidc.consent_granted.v1 |
A user consents to a client for the first time. |
OIDC client CRUD does not emit events — it's considered admin noise. The audit log carries oidc_client_registered, oidc_client_updated, oidc_client_secret_rotated, and oidc_client_deleted entries.
Next
- OIDC overview — the wire protocol your registered client speaks.
- Consents — per-user consent records.
- Identity providers — configure social sign-in for your workspace.