Admin API

API Clients

API clients identify third-party applications that call HappyView's XRPC endpoints. Every request — authenticated or not — needs an X-Client-Key header (or client_key query param). Requests without one get 401 Unauthorized. The client key is HappyView's rate-limit bucket.

A single API client represents your application, not individual users. Create one client for your app and use the same client key across all instances. Users authenticate separately via OAuth — the client key identifies your app, not who is using it.

Each 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 for the client-side view, and the API Keys guide for how admin API keys differ from API clients.

const TOKEN = "hv_..."; // your API key
const headers = { Authorization: `Bearer ${TOKEN}` };

List API clients

GET /admin/api-clients

Requires api-clients:view. Returns clients ordered by created_at descending. Secrets are never returned.

interface ApiClient {
  id: string;
  client_key: string;
  name: string;
  client_id_url: string;
  client_uri: string;
  redirect_uris: string[];
  scopes: string;
  rate_limit_capacity: number;
  rate_limit_refill_rate: number;
  is_active: boolean;
  created_by: string;
  created_at: string;
  updated_at: string;
}

const response = await fetch("http://127.0.0.1:3000/admin/api-clients", {
  headers,
});
const data: ApiClient[] = await response.json();

Response: 200 OK

[
  {
    "id": "01J9...",
    "client_key": "hvc_a1b2c3...",
    "name": "My Game Client",
    "client_id_url": "https://example.com/client-metadata.json",
    "client_uri": "https://example.com",
    "redirect_uris": ["https://example.com/callback"],
    "scopes": "atproto",
    "rate_limit_capacity": 200,
    "rate_limit_refill_rate": 5.0,
    "is_active": true,
    "created_by": "did:plc:...",
    "created_at": "2026-04-13T12:00:00Z",
    "updated_at": "2026-04-13T12:00:00Z",
    "parent_client_id": null,
    "owner_did": null
  }
]

Create an API client

POST /admin/api-clients

Requires api-clients:create. Generates a client_key and client_secret. Store the secret — it won't be shown again.

interface ApiClient {
  id: string;
  client_key: string;
  client_secret: string;
  name: string;
  client_id_url: string;
}

const response = await fetch("http://127.0.0.1:3000/admin/api-clients", {
  method: "POST",
  headers: {
    ...headers,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "My Game Client",
    client_id_url: "https://example.com/client-metadata.json",
    client_uri: "https://example.com",
    redirect_uris: ["https://example.com/callback"],
    scopes: "atproto",
    rate_limit_capacity: 200,
    rate_limit_refill_rate: 5.0,
  }),
});
const data: ApiClient = await response.json();
FieldTypeRequiredDescription
namestringyesHuman-readable display name
client_id_urlstringyesURL to the client's published OAuth client metadata document
client_uristringyesThe client's home/landing URL
redirect_urisstring[]yesAllowed OAuth redirect URIs
scopesstringnoSpace-separated OAuth scopes (default "atproto")
rate_limit_capacityintegernoPer-client token bucket capacity. Falls back to DEFAULT_RATE_LIMIT_CAPACITY if unset
rate_limit_refill_ratenumbernoTokens added per second. Falls back to DEFAULT_RATE_LIMIT_REFILL_RATE if unset

Response: 201 Created

{
  "id": "01J9...",
  "client_key": "hvc_a1b2c3...",
  "client_secret": "hvs_d4e5f6...",
  "name": "My Game Client",
  "client_id_url": "https://example.com/client-metadata.json"
}

The new client is immediately registered with the OAuth registry and rate limiter, so it can authenticate without restarting HappyView.

Get an API client

GET /admin/api-clients/{id}

Requires api-clients:view. Returns the same shape as the list endpoint, or 404 Not Found.

Update an API client

PUT /admin/api-clients/{id}

Requires api-clients:edit. All fields are optional — only provided fields are changed. Updating either rate-limit field re-registers the client with the rate limiter using the new values.

await fetch("http://127.0.0.1:3000/admin/api-clients/01J9...", {
  method: "PUT",
  headers: {
    ...headers,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Renamed Client",
    rate_limit_capacity: 500,
  }),
});
FieldTypeDescription
namestringNew display name
client_uristringNew home URL
redirect_urisstring[]Replace the allowed redirect URIs
scopesstringReplace the OAuth scopes
rate_limit_capacityintegerNew bucket capacity. Pass null to clear the override
rate_limit_refill_ratenumberNew refill rate. Pass null to clear the override
is_activebooleanDisable (false) or re-enable (true) the client without deleting it

Response: 204 No Content

The OAuth registry is updated in place. The client_id_url is immutable — to change it, delete and recreate the client.

Delete an API client

DELETE /admin/api-clients/{id}

Requires api-clients:delete. Removes the client from the OAuth registry, the rate limiter, and the client identity store.

await fetch("http://127.0.0.1:3000/admin/api-clients/01J9...", {
  method: "DELETE",
  headers,
});

Response: 204 No Content