Overview
The Keys API adds 10 endpoints under /v1/keys/* for complete cryptographic key lifecycle management. Built on the Signal Protocol (X3DH) — the same protocol used by Signal, WhatsApp, and Wire — it enables true end-to-end encryption where the server never sees private keys.
https://api.anchora.co.in/v1 base URL and the same Authorization: Bearer YOUR_API_KEY authentication as existing endpoints.
Key Types
| Key Type | Algorithm | Purpose | Lifetime |
|---|---|---|---|
| Identity Key | Ed25519 |
Long-term identity — your "fingerprint" | Permanent (until revoked) |
| Signed Pre-Key | X25519 |
Enables offline key exchange | Weekly/monthly rotation |
| One-Time Pre-Keys | X25519 |
Forward secrecy per session | Single use, then deleted |
| Ephemeral Key | X25519 |
Per key-exchange, never stored | Immediate discard |
Use Cases
- Secure Messaging: End-to-end encrypted chat (like Signal/WhatsApp)
- Healthcare: HIPAA-compliant patient-doctor communication
- Banking: Secure document exchange between bank and client
- Legal: Encrypted contract sharing with tamper-proof audit trail
- IoT: Secure device-to-gateway encrypted telemetry
Algorithms
| Purpose | Algorithm | Key Size |
|---|---|---|
| Key exchange | X25519 (Curve25519 DH) |
32 bytes (256 bits) |
| Digital signatures | Ed25519 |
32 bytes public, 64 bytes private |
| Symmetric encryption | AES-256-GCM |
32 bytes (256 bits) |
| Key derivation | HKDF-SHA-256 |
Variable |
| Backup encryption | Argon2id + AES-256-GCM |
Password-derived |
Register Keys
Register a user's public keys. This is the "I exist and I'm ready to receive encrypted messages" announcement. Must be called before any key exchange can happen.
Request Body
curl -X POST https://api.anchora.co.in/v1/keys/register \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "user_abc123", "identityKey": "a1b2c3d4...64_hex_chars_ed25519_pubkey", "signedPreKey": { "keyId": "spk_a1b2c3d4e5f6g7h8", "publicKey": "f1e2d3c4...64_hex_chars_x25519_pubkey", "signature": "aa11bb22...128_hex_chars_ed25519_signature" }, "oneTimePreKeys": [ { "keyId": "opk_001", "publicKey": "11aa22bb...64_hex_chars" }, { "keyId": "opk_002", "publicKey": "44dd55ee...64_hex_chars" } ], "deviceId": "device_xyz", "deviceName": "iPhone 15" }'
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
userId | String | Yes | Your app-defined user ID |
identityKey | String | Yes | Ed25519 public key (64-char hex) |
signedPreKey | Object | Yes | X25519 signed pre-key with keyId, publicKey, and signature |
oneTimePreKeys | Array | Yes | 1-100 X25519 one-time pre-keys |
deviceId | String | No | Device identifier |
deviceName | String | No | Human-readable device name |
Response (201)
{
"success": true,
"message": "Keys registered successfully",
"data": {
"userId": "user_abc123",
"identityKeyFingerprint": "a1b2c3d4e5f6a1b2",
"signedPreKeyId": "spk_a1b2c3d4e5f6g7h8",
"oneTimePreKeysCount": 2,
"status": "active",
"registeredAt": "2026-03-08T10:00:00.000Z"
}
}
Get Key Bundle
Fetch a user's public key bundle for key exchange. Atomically consumes one one-time pre-key to ensure forward secrecy.
curl https://api.anchora.co.in/v1/keys/bundle/user_abc123 \ -H "Authorization: Bearer YOUR_API_KEY"
Response (200)
{
"success": true,
"data": {
"userId": "user_abc123",
"identityKey": "a1b2c3d4...64_hex",
"signedPreKey": {
"keyId": "spk_a1b2c3d4e5f6g7h8",
"publicKey": "f1e2d3c4...64_hex",
"signature": "aa11bb22...128_hex"
},
"oneTimePreKey": {
"keyId": "opk_001",
"publicKey": "11aa22bb...64_hex"
},
"remainingOneTimePreKeys": 1
}
}
"oneTimePreKey" will be null and a warning will indicate reduced forward secrecy. Replenish OPKs using the rotate endpoint.
Rotate Keys
Rotate the signed pre-key and/or replenish one-time pre-keys. Old signed pre-key is saved in rotation history for existing sessions.
curl -X POST https://api.anchora.co.in/v1/keys/rotate \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "user_abc123", "newSignedPreKey": { "keyId": "spk_new_001", "publicKey": "bb11cc22...64_hex", "signature": "ee44ff55...128_hex" }, "newOneTimePreKeys": [ { "keyId": "opk_050", "publicKey": "aa00bb11...64_hex" } ] }'
Response (200)
{
"success": true,
"message": "Keys rotated successfully",
"data": {
"signedPreKeyRotated": true,
"newSignedPreKeyId": "spk_new_001",
"oneTimePreKeysAdded": 1,
"totalOneTimePreKeysAvailable": 16,
"rotatedAt": "2026-03-08T12:00:00.000Z"
}
}
Backup Keys
Store an encrypted private key backup. The client encrypts the private keys locally using Argon2id + AES-256-GCM before sending. The server stores only an opaque blob it cannot decrypt.
curl -X POST https://api.anchora.co.in/v1/keys/backup \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "user_abc123", "encryptedBundle": "YWJjZGVm...base64_encrypted_blob", "encryptionMetadata": { "algorithm": "argon2id+aes-256-gcm", "argon2Params": { "timeCost": 3, "memoryCost": 65536, "parallelism": 4 }, "nonce": "a1b2c3d4e5f6a1b2c3d4e5f6", "salt": "f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4" }, "checksum": "e3b0c442...sha256_hex" }'
Response (200)
{
"success": true,
"message": "Backup stored successfully",
"data": {
"version": 1,
"storedAt": "2026-03-08T10:30:00.000Z"
}
}
Recover Keys
Retrieve the encrypted backup blob. The client decrypts it locally with the user's password. The server never knows the password and cannot read the keys.
curl -X POST https://api.anchora.co.in/v1/keys/recover \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "user_abc123" }'
Response (200)
{
"success": true,
"data": {
"encryptedBundle": "YWJjZGVm...base64_blob",
"encryptionMetadata": {
"algorithm": "argon2id+aes-256-gcm",
"argon2Params": { "timeCost": 3, "memoryCost": 65536, "parallelism": 4 },
"nonce": "a1b2c3d4e5f6a1b2c3d4e5f6",
"salt": "f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4"
},
"checksum": "e3b0c442...",
"version": 1,
"backedUpAt": "2026-03-08T10:30:00.000Z"
}
}
Revoke Keys
Mark a user's keys as revoked. Use when a device is lost, stolen, or compromised. Existing sessions are unaffected, but no new key exchanges can use these keys.
curl -X POST https://api.anchora.co.in/v1/keys/revoke \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "user_abc123", "reason": "Device compromised" }'
Response (200)
{
"success": true,
"message": "Keys revoked successfully",
"data": {
"userId": "user_abc123",
"status": "revoked",
"revokedAt": "2026-03-08T14:00:00.000Z",
"reason": "Device compromised"
}
}
Verify Key Status
Check if a user's key is valid and active. Returns status, fingerprint, and key health info. Use before sending sensitive data to confirm the recipient's keys are trustworthy.
curl https://api.anchora.co.in/v1/keys/verify/user_abc123 \ -H "Authorization: Bearer YOUR_API_KEY"
Response (200)
{
"success": true,
"data": {
"userId": "user_abc123",
"isValid": true,
"status": "active",
"identityKeyFingerprint": "a1b2c3d4e5f6a1b2",
"registeredAt": "2026-03-08T10:00:00.000Z",
"lastRotatedAt": "2026-03-08T12:00:00.000Z",
"oneTimePreKeysRemaining": 15,
"deviceName": "iPhone 15"
}
}
List Keys
List all registered keys for the project with filtering and pagination. Scoped to your project — you can only see keys within your own project.
curl "https://api.anchora.co.in/v1/keys/list?status=active&page=1&limit=20" \ -H "Authorization: Bearer YOUR_API_KEY"
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | String | all | Filter by status: active, revoked, expired, suspended |
page | Number | 1 | Page number |
limit | Number | 20 | Results per page (max 100) |
sortBy | String | createdAt | Sort field |
sortOrder | String | desc | asc or desc |
Response (200)
{
"success": true,
"data": [
{
"userId": "user_abc123",
"identityKeyFingerprint": "a1b2c3d4e5f6a1b2",
"status": "active",
"oneTimePreKeysRemaining": 15,
"registeredAt": "2026-03-08T10:00:00.000Z",
"lastRotatedAt": "2026-03-08T12:00:00.000Z",
"deviceName": "iPhone 15"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 45,
"totalPages": 3,
"hasNextPage": true
}
}
Establish Shared Secret
Server-assisted X3DH key agreement. The server facilitates the public key exchange and returns the responder's bundle. The actual 32-byte shared secret is computed entirely client-side — the server never knows it.
curl -X POST https://api.anchora.co.in/v1/keys/shared-secret \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "initiatorUserId": "user_alice", "responderUserId": "user_bob", "initiatorEphemeralKey": "cc11dd22...64_hex_x25519_ephemeral" }'
Response (200)
{
"success": true,
"data": {
"sessionId": "sess_a1b2c3d4...",
"responderBundle": {
"identityKey": "a1b2c3d4...64_hex",
"signedPreKey": {
"keyId": "spk_001",
"publicKey": "f1e2d3c4...64_hex",
"signature": "aa11bb22...128_hex"
},
"oneTimePreKey": {
"keyId": "opk_003",
"publicKey": "77001188...64_hex"
}
},
"expiresAt": "2026-03-09T10:00:00.000Z"
}
}
Audit Log
Complete audit trail of every key operation. Essential for security investigations, compliance requirements (HIPAA, SOC 2, GDPR), and debugging encryption issues.
curl "https://api.anchora.co.in/v1/keys/audit?userId=user_abc123&action=KEYS_ROTATED&limit=50" \ -H "Authorization: Bearer YOUR_API_KEY"
Query Parameters
| Parameter | Type | Description |
|---|---|---|
userId | String | Filter by user ID |
action | String | Filter by action type |
startDate | String | ISO date for range start |
endDate | String | ISO date for range end |
page | Number | Page number (default: 1) |
limit | Number | Results per page (default: 50) |
Audit Actions
| Action | Description |
|---|---|
KEYS_REGISTERED | New key set registered |
KEYS_ROTATED | Signed pre-key rotated |
KEYS_REVOKED | Keys marked as revoked |
BUNDLE_FETCHED | Someone fetched this user's bundle |
BACKUP_CREATED | First backup stored |
BACKUP_UPDATED | Backup replaced (new version) |
BACKUP_RECOVERED | Backup retrieved |
SHARED_SECRET_INITIATED | Key agreement started |
SHARED_SECRET_COMPLETED | Key agreement finished |
KEY_VERIFIED | Key validity checked |
PREKEY_CONSUMED | One-time pre-key used |
PREKEYS_REPLENISHED | New one-time pre-keys added |
Response (200)
{
"success": true,
"data": [
{
"action": "KEYS_ROTATED",
"resource": "USER_KEY",
"userId": "user_abc123",
"details": {
"newSignedPreKeyId": "spk_002",
"oneTimePreKeysAdded": 10
},
"ipAddress": "203.0.113.42",
"status": "SUCCESS",
"createdAt": "2026-03-08T12:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 12,
"totalPages": 1,
"hasNextPage": false
}
}
Rate Limits
| Endpoint Group | Limit | Notes |
|---|---|---|
All /v1/keys/* endpoints | 200 req/min | Per API key |
/v1/keys/bundle/:userId | 100 req/min | Stricter limit to prevent key enumeration |
Full SDK Example
Here's a complete example showing two users establishing an encrypted channel using the Anchora SDK:
const { AnchoraClient } = require('@anchora/sdk'); // === Setup Client === const client = new AnchoraClient({ apiKey: 'dcp_live_xxx', projectId: 'proj_123', baseURL: 'https://api.anchora.co.in' }); // === User A: Generate & Register Keys === const keysA = client.keys.generateKeySet(20); saveToSecureStorage('user_a_keys', keysA.privateKeys); await client.keys.registerKeys( 'user_a', keysA.registrationPayload, { deviceName: 'Alice iPhone' } ); // === User A: Establish Shared Secret with User B === const session = await client.keys.establishSharedSecret( 'user_a', 'user_b', keysA.privateKeys.identityKey ); // session.sharedSecret → 32-byte key (never sent to server) // === Encrypt & Anchor === const message = { text: "Hello Bob!" }; const encrypted = await EncryptionService.encrypt( message, session.sharedSecret ); const anchor = await client.anchor(message, { hashOnly: true }); // === Send to User B (via your transport) === sendToUserB({ encrypted, anchorHash: anchor.hash, sessionId: session.sessionId });
Error Responses
| Status | Error Code | Description |
|---|---|---|
400 | VALIDATION_ERROR | Invalid key format, missing required fields |
401 | UNAUTHORIZED | Missing or invalid API key |
404 | NOT_FOUND | User keys not found |
409 | CONFLICT | Active keys already exist for this user |
429 | RATE_LIMITED | Too many requests |
{
"success": false,
"error": "Active keys already exist for this user",
"errorCode": "CONFLICT"
}