···318318- [Permissions](../guides/admin/permissions.md) — full list of permissions and what each one grants
319319- [API Keys](../guides/admin/api-keys.md) — create scoped admin API keys for automation
320320- [Admin API — API Clients](../reference/admin/api-clients.md) — register API clients and configure rate limits
321321-- [Self-Service API Clients](../reference/oauth/api-clients.md) — let third-party apps create child API clients programmatically
321321+- [Third-Party API Clients](../reference/oauth/api-clients.md) — let third-party apps manage their own API clients programmatically
+2-2
packages/docs/docs/reference/admin/api-clients.md
···6677Each client has an `hvc_`-prefixed client key and an `hvs_`-prefixed client secret. The secret is only returned at creation and is sha256-hashed in the database. Server-to-server callers pass the secret as `X-Client-Secret`. Browser callers use the `Origin` header, which is matched against the client's `client_uri`. Mismatches currently log warnings rather than rejecting the request, but rate limiting applies either way. See [Authentication — XRPC](../../getting-started/authentication.md#xrpc-api-client-identification) for the client-side view, and the [API Keys guide](../../guides/admin/api-keys.md) for how admin API keys differ from API clients.
8899-:::tip Self-service API clients
1010-Third-party apps can also create **child API clients** programmatically via the [self-service endpoint](../oauth/api-clients.md), without needing admin access.
99+:::tip Third-Party API Clients
1010+Third-party apps can also create, list, and delete their own API clients programmatically via the [XRPC API](../oauth/api-clients.md), without needing admin access.
1111:::
12121313```sh
+144-45
packages/docs/docs/reference/oauth/api-clients.md
···11-# OAuth API: Self-Service API Clients
11+# Third-Party API Clients
2233-Third-party applications can create child API clients on behalf of authenticated users via `POST /oauth/api-clients`. A child client is always tied to exactly one parent — the admin-created top-level API client that made the request. Only one level of nesting is allowed; child clients cannot create further children. Each child client gets its own rate limit bucket with instance default settings.
33+Third-party applications can manage their own API clients via the `dev.happyview.*` XRPC endpoints. A third-party client is always tied to exactly one parent — the admin-created top-level API client whose DPoP session made the request. Only one level of nesting is allowed; third-party clients cannot create further children. Each third-party client gets its own rate limit bucket with instance default settings.
4455-The endpoint uses [DPoP authentication](../../getting-started/authentication.md#authenticating-users-for-procedures). See the [admin API client docs](../admin/api-clients.md) for managing clients through the admin API, and the [API Clients guide](../../guides/features/api-clients.md) for an overview of how API clients work in HappyView.
55+All endpoints use [DPoP authentication](../../getting-started/authentication.md#authenticating-users-for-procedures). See the [admin API client docs](../admin/api-clients.md) for managing clients through the admin API, and the [API Clients guide](../../guides/features/api-clients.md) for how API clients work.
6677-## Create a child client
77+:::note
88+Only top-level API clients can call these endpoints. Third-party (child) clients receive `401 Unauthorized` or `403 Forbidden`.
99+:::
81099-```
1010-POST /oauth/api-clients
1111-```
1111+## Authentication
12121313-Requires three headers:
1313+All requests require three headers:
14141515| Header | Value |
1616| --------------- | ------------------------------------------------------------ |
1717| `Authorization` | `DPoP <access_token>` |
1818-| `DPoP` | A DPoP proof JWT (method: `POST`, htu: the full request URL) |
1818+| `DPoP` | A DPoP proof JWT (method matches the HTTP method, `htu` is scheme + host + path, no query string) |
1919| `X-Client-Key` | The parent client's `client_key` |
20202121-The access token must belong to a valid DPoP session for the parent client. The parent client's owner (its `created_by` DID) must exist in the HappyView `users` table.
2121+The access token must belong to a valid DPoP session for the parent client.
2222+2323+## List clients
2424+2525+```
2626+GET /xrpc/dev.happyview.listApiClients
2727+```
2828+2929+Returns all API clients owned by the authenticated user.
3030+3131+**Response**: `200 OK`
3232+3333+```json
3434+{
3535+ "clients": [
3636+ {
3737+ "id": "550e8400-e29b-41d4-a716-446655440000",
3838+ "clientKey": "hvc_a1b2c3d4e5f6...",
3939+ "name": "My App",
4040+ "clientIdUrl": "https://myapp.example.com/client-metadata.json",
4141+ "clientUri": "https://myapp.example.com",
4242+ "redirectUris": ["https://myapp.example.com/callback"],
4343+ "clientType": "confidential",
4444+ "scopes": "atproto",
4545+ "allowedOrigins": [],
4646+ "isActive": true,
4747+ "createdAt": "2026-04-28T12:00:00Z"
4848+ }
4949+ ]
5050+}
5151+```
5252+5353+## Get a client
5454+5555+```
5656+GET /xrpc/dev.happyview.getApiClient?id=<client_id>
5757+```
5858+5959+| Parameter | Type | Required | Description |
6060+| --------- | ------ | -------- | ----------------- |
6161+| `id` | string | yes | The client's UUID |
6262+6363+**Response**: `200 OK`
6464+6565+```json
6666+{
6767+ "client": {
6868+ "id": "550e8400-e29b-41d4-a716-446655440000",
6969+ "clientKey": "hvc_a1b2c3d4e5f6...",
7070+ "name": "My App",
7171+ "clientIdUrl": "https://myapp.example.com/client-metadata.json",
7272+ "clientUri": "https://myapp.example.com",
7373+ "redirectUris": ["https://myapp.example.com/callback"],
7474+ "clientType": "confidential",
7575+ "scopes": "atproto",
7676+ "allowedOrigins": [],
7777+ "isActive": true,
7878+ "createdAt": "2026-04-28T12:00:00Z"
7979+ }
8080+}
8181+```
8282+8383+Returns `404` if the client doesn't exist or isn't owned by the authenticated user.
8484+8585+## Create a client
8686+8787+```
8888+POST /xrpc/dev.happyview.createApiClient
8989+```
22902391```sh
2424-curl -X POST https://happyview.example.com/oauth/api-clients \
9292+curl -X POST https://happyview.example.com/xrpc/dev.happyview.createApiClient \
2593 -H "X-Client-Key: hvc_parent_key" \
2694 -H "Authorization: DPoP eyJhbG..." \
2795 -H "DPoP: eyJhbG..." \
2896 -H "Content-Type: application/json" \
2997 -d '{
3030- "name": "My Child App",
3131- "client_id_url": "https://child.example.com/client-metadata.json",
3232- "client_uri": "https://child.example.com",
3333- "redirect_uris": ["https://child.example.com/callback"],
3434- "client_type": "confidential"
9898+ "name": "My Third-Party App",
9999+ "clientIdUrl": "https://myapp.example.com/client-metadata.json",
100100+ "clientUri": "https://myapp.example.com",
101101+ "redirectUris": ["https://myapp.example.com/callback"],
102102+ "clientType": "confidential"
35103 }'
36104```
371053838-| Field | Type | Required | Description |
3939-| ----------------- | -------- | -------- | ------------------------------------------------ |
4040-| `name` | string | yes | Display name for the child client |
4141-| `client_id_url` | string | yes | Unique OAuth client ID URL |
4242-| `client_uri` | string | yes | The client's homepage URL |
4343-| `redirect_uris` | string[] | yes | OAuth redirect URIs |
4444-| `scopes` | string | no | Space-separated OAuth scopes (default `"atproto"`) |
4545-| `client_type` | string | no | `"confidential"` or `"public"` (default `"confidential"`) |
4646-| `allowed_origins` | string[] | no | CORS allowed origins |
106106+| Field | Type | Required | Description |
107107+| ----------------- | -------- | -------- | -------------------------------------------------------------- |
108108+| `name` | string | yes | Display name for the client |
109109+| `clientIdUrl` | string | yes | Unique OAuth client ID URL |
110110+| `clientUri` | string | yes | The client's homepage URL |
111111+| `redirectUris` | string[] | yes | OAuth redirect URIs |
112112+| `scopes` | string | no | Space-separated OAuth scopes (default `"atproto"`) |
113113+| `clientType` | string | no | `"confidential"` or `"public"` (default `"confidential"`) |
114114+| `allowedOrigins` | string[] | no | CORS allowed origins (relevant for public clients) |
4711548116**Response**: `201 Created`
4911750118```json
51119{
5252- "id": "550e8400-e29b-41d4-a716-446655440000",
5353- "client_key": "hvc_a1b2c3d4e5f6...",
5454- "client_secret": "hvs_f6e5d4c3b2a1...",
5555- "name": "My Child App",
5656- "client_id_url": "https://child.example.com/client-metadata.json",
5757- "client_type": "confidential"
120120+ "client": {
121121+ "id": "550e8400-e29b-41d4-a716-446655440000",
122122+ "clientKey": "hvc_a1b2c3d4e5f6...",
123123+ "name": "My Third-Party App",
124124+ "clientIdUrl": "https://myapp.example.com/client-metadata.json",
125125+ "clientUri": "https://myapp.example.com",
126126+ "redirectUris": ["https://myapp.example.com/callback"],
127127+ "clientType": "confidential",
128128+ "scopes": "atproto",
129129+ "allowedOrigins": [],
130130+ "isActive": true,
131131+ "createdAt": "2026-04-28T12:00:00Z"
132132+ },
133133+ "clientSecret": "hvs_f6e5d4c3b2a1..."
58134}
59135```
601366161-The `client_secret` is only present for confidential clients and is only returned in this response — store it securely. It is stored as a SHA-256 hash and cannot be retrieved again.
137137+The `clientSecret` is only present for confidential clients and is only returned in this response. It is stored as a SHA-256 hash and cannot be retrieved again.
138138+139139+## Delete a client
140140+141141+```
142142+POST /xrpc/dev.happyview.deleteApiClient
143143+```
144144+145145+```sh
146146+curl -X POST https://happyview.example.com/xrpc/dev.happyview.deleteApiClient \
147147+ -H "X-Client-Key: hvc_parent_key" \
148148+ -H "Authorization: DPoP eyJhbG..." \
149149+ -H "DPoP: eyJhbG..." \
150150+ -H "Content-Type: application/json" \
151151+ -d '{ "id": "550e8400-e29b-41d4-a716-446655440000" }'
152152+```
153153+154154+| Field | Type | Required | Description |
155155+| ----- | ------ | -------- | ----------------- |
156156+| `id` | string | yes | The client's UUID |
157157+158158+**Response**: `200 OK` with `{}`
159159+160160+Returns `404` if the client doesn't exist or isn't owned by the authenticated user. Deleting a client cascades to all its children.
6216163162## Errors
641636565-| Status | Error | Cause |
6666-| ------ | ---------------------------------------- | ------------------------------------------------------------------ |
6767-| 400 | `Invalid client_type` | `client_type` is not `"confidential"` or `"public"` |
6868-| 400 | `invalid request body` | Missing required fields or malformed JSON |
6969-| 401 | `Missing client identification` | `X-Client-Key` header is absent |
7070-| 401 | `DPoP authorization scheme required` | `Authorization` header doesn't start with `DPoP ` |
7171-| 401 | `DPoP proof header required` | `DPoP` header is absent |
7272-| 401 | `token_expired` | The access token has expired |
7373-| 401 | `Invalid client` | `X-Client-Key` doesn't match a known client |
7474-| 403 | `Child clients cannot create API clients` | The calling client is itself a child |
7575-| 403 | `Parent client owner not found` | The parent client's `created_by` DID is not in the `users` table |
7676-| 409 | `client_id_url already registered` | Another client already uses that `client_id_url` |
164164+| Status | Error | Cause |
165165+| ------ | ----------------------------------------- | ---------------------------------------------------------------- |
166166+| 400 | `Invalid client_type` | `client_type` is not `"confidential"` or `"public"` |
167167+| 400 | `invalid request body` | Missing required fields or malformed JSON |
168168+| 401 | `requires DPoP authentication` | `Authorization` header is missing or doesn't use the DPoP scheme |
169169+| 401 | `requires an API client key` | `X-Client-Key` header is absent |
170170+| 401 | `token_expired` | The access token has expired |
171171+| 401 | `Invalid client` | `X-Client-Key` doesn't match a known client |
172172+| 401 | `child clients cannot manage API clients` | The calling client is itself a third-party (child) client |
173173+| 403 | `Child clients cannot create API clients` | The calling client is itself a third-party (child) client |
174174+| 404 | `API client not found` | No client with that ID owned by the authenticated user |
175175+| 409 | `client_id_url already registered` | Another client already uses that `clientIdUrl` |
7717678177## Operational notes
791788080-Each child client gets its own rate limit bucket using the instance's default capacity and refill rate (`DEFAULT_RATE_LIMIT_CAPACITY` / `DEFAULT_RATE_LIMIT_REFILL_RATE`). Deactivating or deleting a parent via the [admin API](../admin/api-clients.md) cascades to all its children.
179179+Each third-party client gets its own rate limit bucket using the instance's default capacity and refill rate (`DEFAULT_RATE_LIMIT_CAPACITY` / `DEFAULT_RATE_LIMIT_REFILL_RATE`). Deactivating or deleting a parent via the [admin API](../admin/api-clients.md) cascades to all its children.
8118082181The admin API clients list (`GET /admin/api-clients`) returns `parent_client_id` and `owner_did` fields for each client and supports `?parent_id=` filtering. The dashboard's API Clients table shows these as "Parent Client" and "Owner" columns.