Skip to main content

Third-Party API Clients

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.

All endpoints use DPoP authentication. See the admin API client docs for managing clients through the admin API, and the API Clients guide for how API clients work.

note

Only top-level API clients can call these endpoints. Third-party (child) clients receive 401 Unauthorized or 403 Forbidden.

Authentication

All requests require three headers:

HeaderValue
AuthorizationDPoP <access_token>
DPoPA DPoP proof JWT (method matches the HTTP method, htu is scheme + host + path, no query string)
X-Client-KeyThe parent client's client_key

The access token must belong to a valid DPoP session for the parent client.

List clients

GET /xrpc/dev.happyview.listApiClients

Returns all API clients owned by the authenticated user.

Response: 200 OK

{
"clients": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"clientKey": "hvc_a1b2c3d4e5f6...",
"name": "My App",
"clientIdUrl": "https://myapp.example.com/client-metadata.json",
"clientUri": "https://myapp.example.com",
"redirectUris": ["https://myapp.example.com/callback"],
"clientType": "confidential",
"scopes": "atproto",
"allowedOrigins": [],
"isActive": true,
"createdAt": "2026-04-28T12:00:00Z"
}
]
}

Get a client

GET /xrpc/dev.happyview.getApiClient?id=<client_id>
ParameterTypeRequiredDescription
idstringyesThe client's UUID

Response: 200 OK

{
"client": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"clientKey": "hvc_a1b2c3d4e5f6...",
"name": "My App",
"clientIdUrl": "https://myapp.example.com/client-metadata.json",
"clientUri": "https://myapp.example.com",
"redirectUris": ["https://myapp.example.com/callback"],
"clientType": "confidential",
"scopes": "atproto",
"allowedOrigins": [],
"isActive": true,
"createdAt": "2026-04-28T12:00:00Z"
}
}

Returns 404 if the client doesn't exist or isn't owned by the authenticated user.

Create a client

POST /xrpc/dev.happyview.createApiClient
curl -X POST https://happyview.example.com/xrpc/dev.happyview.createApiClient \
-H "X-Client-Key: hvc_parent_key" \
-H "Authorization: DPoP eyJhbG..." \
-H "DPoP: eyJhbG..." \
-H "Content-Type: application/json" \
-d '{
"name": "My Third-Party App",
"clientIdUrl": "https://myapp.example.com/client-metadata.json",
"clientUri": "https://myapp.example.com",
"redirectUris": ["https://myapp.example.com/callback"],
"clientType": "confidential"
}'
FieldTypeRequiredDescription
namestringyesDisplay name for the client
clientIdUrlstringyesUnique OAuth client ID URL
clientUristringyesThe client's homepage URL
redirectUrisstring[]yesOAuth redirect URIs
scopesstringnoSpace-separated OAuth scopes (default "atproto")
clientTypestringno"confidential" or "public" (default "confidential")
allowedOriginsstring[]noCORS allowed origins (relevant for public clients)

Response: 201 Created

{
"client": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"clientKey": "hvc_a1b2c3d4e5f6...",
"name": "My Third-Party App",
"clientIdUrl": "https://myapp.example.com/client-metadata.json",
"clientUri": "https://myapp.example.com",
"redirectUris": ["https://myapp.example.com/callback"],
"clientType": "confidential",
"scopes": "atproto",
"allowedOrigins": [],
"isActive": true,
"createdAt": "2026-04-28T12:00:00Z"
},
"clientSecret": "hvs_f6e5d4c3b2a1..."
}

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.

Delete a client

POST /xrpc/dev.happyview.deleteApiClient
curl -X POST https://happyview.example.com/xrpc/dev.happyview.deleteApiClient \
-H "X-Client-Key: hvc_parent_key" \
-H "Authorization: DPoP eyJhbG..." \
-H "DPoP: eyJhbG..." \
-H "Content-Type: application/json" \
-d '{ "id": "550e8400-e29b-41d4-a716-446655440000" }'
FieldTypeRequiredDescription
idstringyesThe client's UUID

Response: 200 OK with {}

Returns 404 if the client doesn't exist or isn't owned by the authenticated user. Deleting a client cascades to all its children.

Errors

StatusErrorCause
400Invalid client_typeclient_type is not "confidential" or "public"
400invalid request bodyMissing required fields or malformed JSON
401requires DPoP authenticationAuthorization header is missing or doesn't use the DPoP scheme
401requires an API client keyX-Client-Key header is absent
401token_expiredThe access token has expired
401Invalid clientX-Client-Key doesn't match a known client
401child clients cannot manage API clientsThe calling client is itself a third-party (child) client
403Child clients cannot create API clientsThe calling client is itself a third-party (child) client
404API client not foundNo client with that ID owned by the authenticated user
409client_id_url already registeredAnother client already uses that clientIdUrl

Operational notes

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 cascades to all its children.

The 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.