XRPC API
XRPC is the HTTP-based RPC protocol used by the atproto. HappyView dynamically registers XRPC endpoints based on your uploaded lexicons: query lexicons become GET /xrpc/{nsid} routes, procedure lexicons become POST /xrpc/{nsid} routes.
If a query or procedure lexicon has a Lua script attached, the script handles the request. Otherwise, HappyView uses built-in default behavior (described below).
Auth
- Queries (
GET /xrpc/{method}): unauthenticated - Procedures (
POST /xrpc/{method}): require DPoP authentication (Authorization: DPoP+DPoPproof header +X-Client-Key) - getProfile: requires auth
- uploadBlob: requires auth
Fixed endpoints
These endpoints are always available regardless of which lexicons are loaded.
Health check
GET /health
curl http://localhost:3000/health
Response: 200 OK with body ok
Get profile
GET /xrpc/app.bsky.actor.getProfile
Returns the authenticated user's profile, resolved from their PDS via PLC directory lookup.
curl http://localhost:3000/xrpc/app.bsky.actor.getProfile \
-H "X-Client-Key: $CLIENT_KEY" \
-H "Authorization: Bearer $TOKEN"
Response: 200 OK
{
"did": "did:plc:abc123",
"handle": "user.bsky.social",
"displayName": "User Name",
"description": "Bio text",
"avatarURL": "https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=did:plc:abc123&cid=bafyabc"
}
Upload blob
POST /xrpc/com.atproto.repo.uploadBlob
Proxies a blob upload to the authenticated user's PDS. Maximum size: 50MB.
curl -X POST http://localhost:3000/xrpc/com.atproto.repo.uploadBlob \
-H "X-Client-Key: $CLIENT_KEY" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: image/png" \
--data-binary @image.png
Response: proxied from the user's PDS.
Dynamic query endpoints
Query endpoints are generated from lexicons with type: "query". Without a Lua script, they support two built-in modes depending on whether a uri parameter is provided.
Single record
GET /xrpc/{method}?uri={at-uri}
curl "http://localhost:3000/xrpc/xyz.statusphere.listStatuses?uri=at%3A%2F%2Fdid%3Aplc%3Aabc%2Fxyz.statusphere.status%2Fabc123" \
-H "X-Client-Key: $CLIENT_KEY"
Response: 200 OK
{
"record": {
"uri": "at://did:plc:abc/xyz.statusphere.status/abc123",
"$type": "xyz.statusphere.status",
"status": "\ud83d\ude0a",
"createdAt": "2025-01-01T12:00:00Z"
}
}
Media blobs are automatically enriched with a url field pointing to the user's PDS.
List records
GET /xrpc/{method}?limit=20&cursor=<opaque>&did=optional
| Param | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Max records to return (max 100) |
cursor | string | --- | Opaque pagination cursor from a previous response |
did | string | --- | Filter records by DID |
curl "http://localhost:3000/xrpc/xyz.statusphere.listStatuses?limit=10&did=did:plc:abc" \
-H "X-Client-Key: $CLIENT_KEY"
Response: 200 OK
{
"records": [
{
"uri": "at://did:plc:abc/xyz.statusphere.status/abc123",
"status": "\ud83d\ude0a",
"createdAt": "2025-01-01T12:00:00Z"
}
],
"cursor": "MjAyNS0wMS0wMVQxMjowMDowMFp8YXQ6Ly9kaWQ6..."
}
The cursor field is an opaque string present only when more records exist. Pass it back as-is to fetch the next page.
Dynamic procedure endpoints
Procedure endpoints are generated from lexicons with type: "procedure". Without a Lua script, HappyView auto-detects create vs update based on whether the request body contains a uri field.
Create a record
POST /xrpc/{method}
When the body does not contain a uri field, a new record is created.
curl -X POST http://localhost:3000/xrpc/xyz.statusphere.setStatus \
-H "X-Client-Key: $CLIENT_KEY" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "\ud83d\ude0a", "createdAt": "2025-01-01T12:00:00Z" }'
HappyView proxies this to the user's PDS as com.atproto.repo.createRecord, then indexes the created record locally.
Update a record
When the body contains a uri field, the existing record is updated.
curl -X POST http://localhost:3000/xrpc/xyz.statusphere.setStatus \
-H "X-Client-Key: $CLIENT_KEY" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"uri": "at://did:plc:abc/xyz.statusphere.status/abc123",
"status": "\ud83c\udf1f",
"createdAt": "2025-01-01T13:00:00Z"
}'
HappyView proxies this to the user's PDS as com.atproto.repo.putRecord, then upserts the record locally.
Response for both: proxied from the user's PDS.
Errors
All error responses return JSON with an error field:
{
"error": "description of what went wrong"
}
| Status | Meaning | Common causes |
|---|---|---|
400 Bad Request | Invalid input | Missing required fields, malformed JSON, invalid AT URI |
401 Unauthorized | Authentication failed | Missing or invalid client identification or DPoP authentication |
404 Not Found | Method or record not found | XRPC method has no matching lexicon, or the requested record doesn't exist |
500 Internal Server Error | Server-side failure | Lua script error, database error, or upstream PDS failure |
Lua script errors
When a Lua script fails, the response is 500 with one of:
{"error": "script execution failed"}: syntax error, runtime error, or missinghandle()function{"error": "script exceeded execution time limit"}: the script hit the 1,000,000 instruction limit
The full error details are logged server-side but not exposed to the client. See Lua Scripting - Debugging for how to diagnose script issues.
PDS errors
When a procedure proxies a write to the user's PDS and the PDS returns an error, HappyView forwards the PDS response status code and body directly to the client.
Next steps
- Lua Scripting: Override the default query and procedure behavior with custom logic
- Lexicons: Understand how lexicons generate these endpoints
- Admin API: Manage lexicons and monitor your instance