Secure storage and distribution of cryptographic keys in ATProto applications

ATP Keyserver API Reference#

Complete reference for all keyserver API endpoints.

Authentication#

Private endpoints require ATProto service auth token in Authorization header:

Authorization: Bearer {service_auth_token}

Obtain service auth tokens from user's PDS via com.atproto.server.getServiceAuth. See Encryption Protocol for details.

Common Headers#

Request Headers#

Authorization: Bearer {token}    # Required for protected endpoints
Content-Type: application/json   # Required for POST requests
User-Agent: {client_info}        # Recommended for logging

Response Headers#

Content-Type: application/json

Common Error Responses#

All endpoints may return these errors:

400 Bad Request#

Invalid request parameters or malformed input.

{
  "error": "Bad Request",
  "message": "Invalid DID format"
}

401 Unauthorized#

Missing or invalid authentication token.

{
  "error": "Unauthorized",
  "message": "Invalid or expired service auth token"
}

403 Forbidden#

Authenticated but not authorized for requested resource.

{
  "error": "Forbidden",
  "message": "Not a member of this group"
}

404 Not Found#

Requested resource does not exist.

{
  "error": "Not Found",
  "message": "Group not found"
}

500 Internal Server Error#

Unexpected server error.

{
  "error": "Internal Server Error",
  "message": "Database operation failed"
}

Public Endpoints#

Get Service Metadata#

Returns basic service information.

GET /

Authentication: None

Response: 200 OK

{
  "name": "@atpkeyserver/server",
  "version": "0.0.1"
}

Get Service DID Document#

Returns the service's DID document for ATProto service discovery.

GET /.well-known/did.json

Authentication: None

Response: 200 OK

{
  "@context": ["https://www.w3.org/ns/did/v1"],
  "id": "did:web:keyserver.example.com",
  "service": [
    {
      "id": "#atp_keyserver",
      "type": "AtpKeyserver",
      "serviceEndpoint": "https://keyserver.example.com/"
    }
  ]
}

Get Public Key#

Retrieve any user's public key without authentication. Used for encrypting messages to specific users.

GET /xrpc/dev.atpkeyserver.alpha.keypair.getPublicKey

Authentication: None

Query Parameters:

Parameter Type Required Description
did string Yes DID of user (must be did:web:* or did:plc:*)
version integer No Specific key version to retrieve (default: active version)

Response: 200 OK

{
  "publicKey": "a1b2c3d4e5f6...",
  "version": 1
}

Errors:

  • 400 - Invalid DID format or unsupported DID method
  • 404 - User not found or key version doesn't exist

Example:

curl "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.keypair.getPublicKey?did=did%3Aplc%3Aabc123"

Personal Key Management#

Get Personal Keypair#

Retrieve the authenticated user's complete Ed25519 keypair.

GET /xrpc/dev.atpkeyserver.alpha.keypair.getKeypair

Authentication: Required

Query Parameters:

Parameter Type Required Description
version integer No Specific key version to retrieve (default: active version)

Response: 200 OK

{
  "publicKey": "a1b2c3d4e5f6...",
  "privateKey": "9f8e7d6c5b4a...",
  "version": 2
}

Errors:

  • 401 - Invalid or expired auth token
  • 404 - Specified version not found

Example:

curl -H "Authorization: Bearer ${TOKEN}" \
  "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.keypair.getKeypair"

Rotate Personal Keypair#

Generate a new keypair version, marking the current version as revoked.

POST /xrpc/dev.atpkeyserver.alpha.keypair.rotate

Authentication: Required

Request Body:

{
  "reason": "suspected_compromise"
}
Field Type Required Description
reason string No Rotation reason: suspected_compromise, routine_rotation, user_requested (default)

Response: 200 OK

{
  "oldVersion": 1,
  "newVersion": 2,
  "rotatedAt": "2025-01-24T10:30:00.000Z"
}

Errors:

  • 401 - Invalid or expired auth token
  • 500 - Key generation or database error

Example:

curl -X POST \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"reason":"routine_rotation"}' \
  "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.keypair.rotate"

List Personal Key Versions#

List all versions of the user's keypair with status and timestamps.

GET /xrpc/dev.atpkeyserver.alpha.keypair.listVersions

Authentication: Required

Response: 200 OK

{
  "versions": [
    {
      "version": 2,
      "status": "active",
      "created_at": "2025-01-24T10:30:00.000Z",
      "revoked_at": null
    },
    {
      "version": 1,
      "status": "revoked",
      "created_at": "2025-01-20T08:00:00.000Z",
      "revoked_at": "2025-01-24T10:30:00.000Z"
    }
  ]
}

Field Definitions:

  • status: active (current version) or revoked (no longer active, compromised or rotated out)
  • created_at: ISO 8601 timestamp
  • revoked_at: ISO 8601 timestamp (null if still active)

Errors:

  • 401 - Invalid or expired auth token

Example:

curl -H "Authorization: Bearer ${TOKEN}" \
  "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.keypair.listVersions"

Get Key Access Logs#

Retrieve access logs for the authenticated user's keys.

GET /xrpc/dev.atpkeyserver.alpha.accessLogs.getLogs

Authentication: Required

Query Parameters:

Parameter Type Required Description
limit integer No Maximum number of log entries to return (default: 50)

Response: 200 OK

{
  "logs": [
    {
      "version": 2,
      "accessed_at": "2025-01-24T10:30:00.000Z",
      "ip": "192.0.2.1",
      "user_agent": "Mozilla/5.0..."
    },
    {
      "version": 1,
      "accessed_at": "2025-01-22T08:15:00.000Z",
      "ip": "192.0.2.1",
      "user_agent": "Mozilla/5.0..."
    }
  ]
}

Field Definitions:

  • version: Key version that was accessed
  • accessed_at: ISO 8601 timestamp
  • ip: IP address of requester (null if not available)
  • user_agent: User agent string (null if not available)

Errors:

  • 400 - Invalid limit parameter
  • 401 - Invalid or expired auth token

Example:

curl -H "Authorization: Bearer ${TOKEN}" \
  "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.accessLogs.getLogs?limit=100"

Account Management#

Delete Account#

⚠️ WARNING: This action is irreversible and implements cryptographic erasure of ALL your data.

Delete all encryption keys associated with the authenticated user's account.

What the Keyserver Deletes:

  • All asymmetric keypair versions
  • All symmetric group keys owned by the user
  • All group memberships
  • All access logs

What the Keyserver Does NOT Delete:

  • Encrypted posts from your PDS
  • Encrypted content from relays
  • Cached content on other users' devices

Client Responsibility:

Clients SHOULD delete encrypted posts from the user's PDS before calling this endpoint:

// 1. Delete encrypted posts from PDS (optional but recommended)
for (const post of userEncryptedPosts) {
  await agent.com.atproto.repo.deleteRecord({
    repo: userDid,
    collection: 'app.bsky.feed.post',
    rkey: post.rkey
  })
}

// 2. Delete keys from keyserver (required for GDPR)
await fetch('/xrpc/dev.atpkeyserver.alpha.account.delete', { ... })

Consequence: All content encrypted with these keys becomes permanently unreadable (cryptographic erasure), even if the encrypted ciphertext persists somewhere on the network. This satisfies GDPR right to erasure while preserving network integrity.

POST /xrpc/dev.atpkeyserver.alpha.account.delete

Authentication: Required

Request Body:

{
  "confirmation": "DELETE_ALL_MY_DATA"
}
Field Type Required Description
confirmation string Yes Must be exactly "DELETE_ALL_MY_DATA" to confirm deletion

Response: 200 OK

{
  "keys": 5,
  "groups": 2,
  "memberships": 8,
  "accessLogs": 1234
}

Field Definitions:

  • keys: Number of key versions deleted (all versions across all time)
  • groups: Number of groups deleted (owned by user, affects all members)
  • memberships: Number of group memberships removed (groups owned by others)
  • accessLogs: Number of access log entries deleted

Errors:

  • 400 - Invalid confirmation string (must be exactly "DELETE_ALL_MY_DATA")
  • 401 - Invalid or expired auth token
  • 500 - Server error during deletion

Important Warnings:

  1. Keys deleted, not content: This endpoint deletes keys only. Clients should delete encrypted posts from PDS separately.
  2. Encrypted content becomes unreadable: All posts/messages encrypted with deleted keys become permanently unreadable
  3. Affects other users: Groups you own will be deleted, affecting all members
  4. Content may persist: Encrypted ciphertext may remain on relays/caches but becomes useless random bytes
  5. No recovery: This action cannot be undone

GDPR Compliance:

This implements cryptographic erasure, which is recognized by EU data protection authorities as effective deletion. The keyserver deletes encryption keys, making encrypted content computationally infeasible to decrypt.

Even if encrypted ciphertext persists on the network (e.g., relays don't honor deletion notices), it is no longer "personal data" under GDPR without the decryption keys.

Example:

curl -X POST \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"confirmation": "DELETE_ALL_MY_DATA"}' \
  "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.account.delete"

Response:

{
  "keys": 3,
  "groups": 1,
  "memberships": 5,
  "accessLogs": 847
}

Group Key Management#

Get Group Key#

Retrieve a group's symmetric key for encryption/decryption.

GET /xrpc/dev.atpkeyserver.alpha.group.getKey

Authentication: Required

Authorization: User must be group owner or member

Query Parameters:

Parameter Type Required Description
group_id string Yes Group identifier in format {owner_did}#{group_name}
version integer No Specific key version (default: active version)

Response: 200 OK

{
  "groupId": "did:plc:abc123#followers",
  "secretKey": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "version": 2
}

Field Definitions:

  • secretKey: Hex-encoded 32-byte (256-bit) XChaCha20-Poly1305 key
  • version: Key version number

Errors:

  • 400 - Missing or invalid group_id parameter
  • 401 - Invalid or expired auth token
  • 403 - User not a member of group
  • 404 - Group or version not found

Example:

curl -H "Authorization: Bearer ${TOKEN}" \
  "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.group.getKey?group_id=did%3Aplc%3Aabc123%23followers"

Rotate Group Key#

Generate a new group key version, marking the current version as revoked. Only group owner can perform this action.

POST /xrpc/dev.atpkeyserver.alpha.group.rotateKey

Authentication: Required

Authorization: User must be group owner

Request Body:

{
  "group_id": "did:plc:abc123#followers",
  "reason": "suspected_compromise"
}
Field Type Required Description
group_id string Yes Group identifier
reason string No Rotation reason (default: user_requested)

Response: 200 OK

{
  "groupId": "did:plc:abc123#followers",
  "oldVersion": 1,
  "newVersion": 2,
  "rotatedAt": "2025-01-24T10:30:00.000Z"
}

Errors:

  • 400 - Missing or invalid group_id
  • 401 - Invalid or expired auth token
  • 403 - User not group owner
  • 404 - Group not found

Example:

curl -X POST \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"group_id":"did:plc:abc123#followers","reason":"routine_rotation"}' \
  "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.group.rotateKey"

List Group Key Versions#

List all versions of a group key with status and timestamps. Owner and members can view.

GET /xrpc/dev.atpkeyserver.alpha.group.listVersions

Authentication: Required

Authorization: User must be group owner or member

Query Parameters:

Parameter Type Required Description
group_id string Yes Group identifier

Response: 200 OK

{
  "groupId": "did:plc:abc123#followers",
  "versions": [
    {
      "version": 2,
      "status": "active",
      "created_at": "2025-01-24T10:30:00.000Z",
      "revoked_at": null
    },
    {
      "version": 1,
      "status": "revoked",
      "created_at": "2025-01-20T08:00:00.000Z",
      "revoked_at": "2025-01-24T10:30:00.000Z"
    }
  ]
}

Errors:

  • 400 - Missing or invalid group_id
  • 401 - Invalid or expired auth token
  • 403 - User not a member of group
  • 404 - Group not found

Example:

curl -H "Authorization: Bearer ${TOKEN}" \
  "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.group.listVersions?group_id=did%3Aplc%3Aabc123%23followers"

Group Membership Management#

Add Group Member#

Add a user to a group, granting them access to the group key. Only group owner can perform this action.

POST /xrpc/dev.atpkeyserver.alpha.group.addMember

Authentication: Required

Authorization: User must be group owner

Request Body:

{
  "group_id": "did:plc:abc123#followers",
  "member_did": "did:plc:xyz789"
}
Field Type Required Description
group_id string Yes Group identifier
member_did string Yes DID of user to add

Response: 200 OK

{
  "groupId": "did:plc:abc123#followers",
  "memberDid": "did:plc:xyz789",
  "status": "added"
}

Errors:

  • 400 - Missing or invalid parameters
  • 401 - Invalid or expired auth token
  • 403 - User not group owner
  • 404 - Group not found
  • 409 - Member already in group

Example:

curl -X POST \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"group_id":"did:plc:abc123#followers","member_did":"did:plc:xyz789"}' \
  "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.group.addMember"

Remove Group Member#

Remove a user from a group, revoking their access to the group key. Only group owner can perform this action.

POST /xrpc/dev.atpkeyserver.alpha.group.removeMember

Authentication: Required

Authorization: User must be group owner

Request Body:

{
  "group_id": "did:plc:abc123#followers",
  "member_did": "did:plc:xyz789"
}
Field Type Required Description
group_id string Yes Group identifier
member_did string Yes DID of user to remove

Response: 200 OK

{
  "groupId": "did:plc:abc123#followers",
  "memberDid": "did:plc:xyz789",
  "status": "removed"
}

Errors:

  • 400 - Missing or invalid parameters
  • 401 - Invalid or expired auth token
  • 403 - User not group owner
  • 404 - Group not found or member not in group

Example:

curl -X POST \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"group_id":"did:plc:abc123#followers","member_did":"did:plc:xyz789"}' \
  "https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.group.removeMember"

Rate Limits#

Rate limits are not currently implemented in this version, but in future versions a rate limit system may look like this:

  • Public endpoints: 100 requests/minute per IP
  • Authenticated endpoints: 1000 requests/minute per DID
  • Key rotation: 10 requests/hour per DID
  • Member management: 100 requests/hour per DID

The response headers this system will use are the following:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1706097600

Pagination#

Currently, pagination is not implemented for list endpoints. All results are returned in a single response. Future versions may add cursor-based pagination for:

  • /xrpc/dev.atpkeyserver.alpha.accessLogs.getLogs
  • /xrpc/dev.atpkeyserver.alpha.keypair.listVersions
  • /xrpc/dev.atpkeyserver.alpha.group.listVersions

Versioning#

Protocol Version: dev.atpkeyserver.alpha

This is a development version of the API. Breaking changes may occur before version 1.0 is released.

API Changelog:

  • 2025-01-24: Added account deletion endpoint (GDPR compliance)
  • 2025-01-24: Added access log endpoint with 90-day retention
  • 2025-01-23: Added key versioning support
  • 2025-01-22: Added group member management
  • 2025-01-20: Initial release

SDKs and Libraries#

Official:

Community:

  • (Submit your implementation via PR to be listed here)

Support#

For API questions or issues:

For a guide on security vulnerabilities, see SECURITY.md.