This page fully specifies KeyLockr SSO, end-to-end encrypted AppData, optional Access Point (AP) authorization, and backend verification. Every required wire format, cryptographic rule, error, and example is included.
/v3 contract. No private SDK, client secret, or separate integration guide is required.
All external SSO requests send the effective app_tag. Without a custom tag, the value is the decimal Service ID, but it still travels in app_tag; there is no external svc_id or binding_type.
An SSO binding uses safe_id + app_tag + enc_pk as its persistent identity, while sign_pk is only the current login credential. AP bindings remain identified by safe_id + app_tag + sign_pk. An AppData namespace uses safe_id + app_tag.
The Safe always displays the title registered in MyDeveloper. Handshake name is only the physical device name and cannot spoof the service name.
The base identity for every registered App. Create your own login session from safe_id after Safe approval.
Optional E2E encrypted namespace. Clients for one Safe/tag share ciphertext while retaining separate keypairs.
Optional, approval-gated Access Point capability with full access or an explicit scope subset.
/kl/login/start.finish_secret and opaque assertion to its own backend /kl/login/finish.| Field | Owner | Rule |
|---|---|---|
title | Developer | Required and public; the authoritative App name shown by the Safe and third-party login pages. |
app_tag | Developer | Optional custom identity. Blank uses the decimal Service ID and may be replaced once later. A custom value must be globally unique, non-numeric, and non-reserved; after it is set, it is locked and every client/backend configuration must switch to the new tag. |
server_ips | Developer | One actual fixed backend egress IPv4 or IPv6 address per line for app_verify and sso_user_pubkey. Dual-stack services must add both addresses. Blank disables both; it does not allow all callers. |
data_approved | Developer | Allows new bindings to request app_data=true. |
ap_approved | Admin | Allows new bindings to request ap=true. |
ap_allowed_scopes | Admin | Maximum AP scopes; NULL permits a full-AP request. |
plugin_approved | Admin | Approves direct Mobile Safe access to a plugin backend. |
plugin_api_url | Admin | Approved plugin backend URL. |
sso_key_owner | Developer | Handshake key owner selected by the service owner: client (default) or backend. Only backend mode overrides the request enc_pk; it supports neither AP nor Delegated SSO. |
encrypt_pk_base64 | Developer | Optional third-party backend X25519 service key maintained by the service owner in MyDeveloper and always published by /svc_pk. It cannot be cleared while backend ownership or Delegated SSO is enabled. Client ownership may retain this key without changing the App handshake. |
auth_request_sign_pk_current_base64 | Developer | The only accepted current Ed25519 public key for delegated requests; standard Base64 decoding to exactly 32 bytes. After replacement, requests later delivered with the old key are rejected. |
mobile_return_urls | Developer | One exact canonical callback URL per line. Prefer a Universal Link/App Link; a native App's registered custom scheme, such as cing://sso/callback, is also permitted. No query, fragment, wildcard, or dangerous scheme. |
delegated_sso_enabled | Developer | Per-service switch, off by default; it can be enabled only when every required field is valid. |
An app_tag is a public identifier, not a client secret. App Attest is not part of this flow, and there is no app_attest_challenge API.
| Purpose | Production | Development |
|---|---|---|
| HTTP API | https://api.keylockr.app/v3 | https://apidev.keylockr.app/v3 |
| WebSocket | wss://api.keylockr.app/v3/ws | wss://apidev.keylockr.app/v3/ws |
| Server box pk | GET /key_enc | GET /key_enc |
| Server signing pk | GET /key_sign | GET /key_sign |
| Deployed build | GET /build_ver | GET /build_ver |
Key endpoints return a standard-Base64 32-byte public key. With client ownership, every physical App/Browser persistently retains its own X25519 box keypair and creates an Ed25519 signing keypair for the current login. After a successful normal logout, it must delete or replace the signing keypair while retaining the box keypair. Even when the third-party backend has a registered service key, the client never receives or uses the backend enc_sk. A backend-owned logical deployment persistently retains one shared X25519 box keypair and maintains a current signing credential per Safe; it registers the X25519 enc_pk in MyDeveloper. Transmit only public keys; private keys never leave their owner.
sign_pk, sign_sk = nacl.sign.keyPair() // 32-byte pk, 64-byte sk
enc_pk, enc_sk = nacl.box.keyPair() // 32-byte pk, 32-byte sk
enc_sk loses the original persistent SSO identity and requires QR authorization with a new enc_pk. Losing or deliberately deleting sign_sk invalidates only the current login credential: after approval with the retained encryption keypair, the new sign_pk reuses the same app_id and AppData. An app_id is not a bearer token./start must run immediately before the App shows a QR or opens the deep link. From one start_wall_time snapshot, the backend derives a 600-second challenge deadline and a 720-second retention deadline, using independent CSPRNG outputs for the public challenge and private finish_secret.
POST your-backend.example/kl/login/start
// Cache-Control: no-store
{
deeplink: string, // keylockr://sso-request?request=...
finish_secret: string, // private, 32-byte base64url
request_digest: string, // exact SHA-256(auth_request)
expires_at: number // Unix seconds
}
The signed request uses a fixed 11-field MessagePack claim, the current Ed25519 key, and its kid. The challenge is a public transaction ID, not an authorization credential; only the original App holding the finish_secret can finish. The request also signs the app_tag, return URL, capabilities, optional client public keys, device name, and AP scopes.
Delegated v1 accepts client ownership only and requires both client_sign_pk and client_enc_pk. Backend-owned services have no delegated-request signing credential in this wire version and must use the legacy advanced handshake below; an X25519 encrypt_pk_base64 is not a signing key.
finish_secret it cannot finish from the callback alone. The receiver must still verify the KL seal, box, every claim, request digest, and finish_secret; any failure must not call finish, create a session, or revoke a binding.POST your-backend.example/kl/login/finish
{
finish_secret: string,
backend_assertion: string
}
// After offline KL seal + box + claim verification and atomic Redis completion:
{
session_result: object,
safe_id: string,
user_nickname: string
}
Finish does not call KeyLockr or /app_verify. Pending/completed state lives only in Redis. An exact secret/assertion retry returns the same session; mismatches and expired replays return the same generic authentication failure. Before activating the session, the login client must show the verified nickname and full Safe ID for explicit confirmation. Safe ID, not nickname, is the stable identity.
KeyLockr Mobile internally calls app_delegated_info and app_delegated_approve. These Safe KPS actions are not third-party App/backend APIs.
| Limit | Fixed value |
|---|---|
auth_request | 4 KiB decoded |
return_url | 2 KiB UTF-8; no query/fragment |
| backend assertion / client result | 3 KiB / 12 KiB |
| success wrapper / encoded result / callback URL | 16 KiB / 21,846 chars / 24 KiB |
| request / assertion lifetime | 600 seconds / 120 seconds |
Delegated v1 does not permit data_deferred or a third-party App fallback to KeyLockr. Any inline overflow ends with app_auth_result_too_large. Rejection is exactly keylockr_result=cancel and must not call finish.
/app_verify compatibility flow. New mobile SSO must not silently fall back here after delegated failure.POST https://api.keylockr.app/v3/handshake
Content-Type: application/octet-stream
Accept: application/octet-stream
// MessagePack, not JSON and not KPS-encrypted
{
sign_pk: Uint8Array, // exactly 32 bytes
enc_pk?: Uint8Array, // 32 bytes for client ownership; ignored for backend
app_tag: "your_effective_tag",
name?: "Work MacBook", // required for AppData or AP
app_data?: true,
ap?: true,
requested_scopes?: ["2fa"] // AP only; non-empty when present
}
// -> { _res: "ok", tmp_id: string }
The sign_pk is always required and exactly 32 bytes. With sso_key_owner=client, enc_pk is required and exactly 32 bytes; KL does not override the request key even when encrypt_pk_base64 is registered. With sso_key_owner=backend, KL ignores any supplied or omitted enc_pk and uses only a valid registered 32-byte X25519 service key; a missing or invalid key never falls back.
| Request | Approval | name | Expected capabilities |
|---|---|---|---|
| SSO | valid service | optional | ["sso"] |
| SSO + AppData | data_approved | required | ["sso","app_data"] |
| SSO + AP | ap_approved | required | ["sso","ap"] |
| SSO + AppData + AP | both | required | ["sso","app_data","ap"] |
After trimming, name must contain 1–255 Unicode characters. With ap=true, omitting requested_scopes requests full AP; a restricted service must send a non-empty approved subset. Never send scopes without ap; only sso_key_owner=client may request AP, regardless of whether a separate service key exists.
The tmp_id and its temporary key/capability state live for 3600 seconds. Check _res before reading every HTTP response, even when its HTTP status is 200.
message = utf8("tmp.{tmp_id}.{unix_seconds}")
signed = nacl.sign(message, sign_sk) // 64-byte signature || message
sig = base64url_without_padding(signed)
wss://api.keylockr.app/v3/ws?sig={urlencoded_sig}
Use the NaCl signed-message form, not a detached signature. Timestamps are accepted from 10 minutes in the past through 3 minutes in the future.
sso://login?tmp_id={tmp_id}
keylockr://sso?tmp_id={tmp_id}
Both URI forms use the same tmp_id. Name, AppData, AP, and scopes live in server handshake state and never in the QR. Backend-owned must open connected with its persistent enc_sk before showing the QR; under explicit backend ownership, an untrusted caller cannot decrypt or continue KPS because its handshake enc_pk is replaced by the registered key.
keylockr://sso?tmp_id={tmp_id}&return_url={percent_encoded_absolute_uri}
The return_url must be an absolute URI with a valid scheme, is direct-link only, and must never be included in a QR. After approval, the Safe may append keylockr_result={base64url_without_padding(completion_kps)}; it is the complete server-signed app_auth_result KPS encrypted only for this client and is the same completion result delivered by the temporary WebSocket.
Accept exactly one keylockr_result. Base64url-decode it, verify the KPS seal, box, and timestamp, then exactly match tmp_id, identity, and capabilities; the URL itself is not a bearer token. The callback and WebSocket should share one waiter that settles on the first valid result and deduplicates later replays. If no result is present, fall back to the WebSocket.
The Safe rejects about, blob, content, data, facetime, facetime-audio, file, intent, javascript, keylockr, mailto, market, sms, sso, tel, telprompt, and every scheme matching itms* (for example itms-services).
// Decrypted KPS frame:
header.from = "app_auth_result"
body = {
_res: "ok",
status: "done",
tmp_id: string,
app_id: string,
safe_id: string,
capabilities: string[],
user_nickname: string,
user_enc_pk: Uint8Array,
ap_access?: { full_access: boolean, scopes: string[] },
account_key_for_ap?: Uint8Array,
data_filekey?: Uint8Array,
data_plain?: Uint8Array,
data_encrypted?: Uint8Array,
data_deferred?: true,
ver?: string
}
// Terminal completion when the frame cannot be compacted:
body = {
_res: "ok",
status: "error",
tmp_id: string,
code: "app_auth_result_too_large"
}
tmp_id exactly matching this handshake before interpreting status. On status=error, require a non-empty code and terminate. Only status=done may proceed to validate app_id/safe_id, capabilities, AppData ver, and AP fields.Completion may arrive through the temporary WebSocket or a native callback; there is no HTTP polling. After a brief disconnect, reconnect with the same keypair while tmp_id remains valid to replay the completed result.
The completion KPS shared by WebSocket and callback is limited to 12 KiB. data_deferred=true is set only when the Safe supplied a client filekey and AppData content was actually omitted; retain data_filekey and ver, then call app_get_data. Without a client filekey, an older Safe result that would exceed the limit omits only the currently unusable data_encrypted, retains data_plain and ver, and does not set data_deferred; call app_req_filekey for the filekey, then obtain the latest encrypted content from app_filekey_result or app_get_data. A result still over the limit after trimming terminates with app_auth_result_too_large.
The AP Account Key format is nonce(24) || nacl.box(AK, nonce, client_enc_pk, safe_enc_sk). Open it with user_enc_pk and the client enc_sk, require 32 bytes, and never log or upload it.
// Existing AP only: recover a locally lost Account Key.
action: "app_req_account_key"
body: {}
{
_res: "ok",
account_key_for_ap: Uint8Array,
safe_enc_pk: Uint8Array
}
// or { _res: "ok", status: "not_available" }
not_available means the binding never completed Account Key delivery; delete it and authorize again because there is no background repair. A non-AP binding receives app_not_ap.
Every WebSocket frame and POST /v3/ App action uses binary MessagePack KPS.
{
kps: {
id: "tmp.{tmp_id}" | "app.{app_id}",
app_ver: string,
box: Uint8Array,
n: Uint8Array, // exactly 24 random bytes
raw?: Uint8Array[]
},
seal: Uint8Array
}
inner = msgpack({
header: { ts: unix_seconds, to: action },
body: action_params
})
box = nacl.box(inner, n, server_enc_pk, client_enc_sk)
seal = nacl.sign(
sha256(canonical_msgpack_with_sorted_map_keys(kps)),
client_sign_sk
)
{
kps: {
box: Uint8Array,
n: Uint8Array,
raw?: Uint8Array[]
},
seal: Uint8Array
}
// After seal verification and box.open:
{
header: { ts: number, from: string },
body: {
_res: "ok" | "err",
code?: string,
msg?: string
}
}
Outer, kps, inner, and header are closed core schemas: reject unknown or duplicate fields, trailing values, invalid UTF-8, wrong types, and oversized values. A general action body is an extension boundary: validate _res, required fields, types, and conditions, then ignore unknown fields. Reject unknown action fields only when that action is explicitly documented as exact/closed.
The receive order is fixed: strictly decode outer/core without restoring raw, verify the seal and canonical KPS hash, validate the 24-byte nonce and open the box, strictly decode inner/header, recursively validate every field__ index, raw binary type, bound, and field/field__ collision, materialize the logical body, then validate the action schema and body._res. Abort on any failure.
Raw is only for already encrypted blobs: the seal protects integrity, but the box does not hide it. Even when an unknown extension field will be ignored, its placeholder must pass full validation before materialization.
header.ts is Unix seconds, not milliseconds, accepted from 10 minutes past through 3 minutes future.AppData actions use the app.{app_id} KPS identity. KeyLockr stores only an encrypted filekey and application ciphertext and never holds the plaintext filekey.
A current Safe's initial app_auth_result directly supplies data_filekey, ver, and existing data_encrypted when it fits the completion limit; a new namespace may be empty. After validation, open the filekey and use that data immediately. If data_deferred=true, call app_get_data directly. Neither path needs a second phone unlock. Use app_req_filekey only when an older Safe omitted data_filekey, the local key was later lost, or a separate re-unlock is required.
For the compatibility path, connect the App WebSocket with nacl.sign("app.{app_id}.{ts}", sign_sk) and register an app_filekey_result waiter before sending the action, so a fast approval cannot be missed.
action: "app_req_filekey"
body: {}
// Immediate or polling KPS response:
{
_res: "ok",
status: "done" | "safe_auth_required" | "denied",
data_filekey?: Uint8Array, // done only
ver?: string // done only
}
// App WebSocket completion:
header.from = "app_filekey_result"
body = {
_res: "ok",
status: "done" | "denied",
data_filekey?: Uint8Array,
data_enc?: Uint8Array,
ver?: string
}
A safe_auth_required status means the Safe has been notified. Wait for the push while WebSocket is available; use spaced polling only after disconnect or timeout. Polling done does not include data_enc, so call app_get_data next.
// MessagePack envelope; new data uses full names.
{
encFileKey: Uint8Array, // 48 bytes
nonceForKey: Uint8Array, // 24 bytes
nonceForData: Uint8Array, // 24 bytes; v1 read fallback only
apEncPk: Uint8Array // 32-byte sender pk
}
// Read-only legacy aliases:
{ k, nk, nd, p }
file_key = nacl.box.open(
encFileKey,
nonceForKey,
apEncPk,
client_enc_sk
)
// file_key must be exactly 32 bytes.
Readers normalize all four fields with fullName ?? legacyShortName and validate lengths. nonceForData is only for legacy v1 reads and must never be used for new writes.
action: "app_get_data"
body: {}
// KPS response:
{
_res: "ok",
file_id: string,
ver: string,
data_plain: Uint8Array,
data_encrypted: Uint8Array
}
The data_plain field is Safe-created MessagePack metadata {name, app_tag} and is not third-party mutable. Store application data only in data_encrypted.
// Every new write is v2:
nonce = randomBytes(24)
data_encrypted =
nonce || nacl.secretbox(msgpack(application_data), nonce, file_key)
// Read:
1. If length >= 40, try nonce-prefix v2 and verify its MAC.
2. Only if v2 fails, try the whole blob as v1 with nonceForData.
3. If both fail, abort. Never parse unauthenticated bytes.
action: "app_set_data"
body: {
ver: string, // exact latest value
data_enc: Uint8Array // complete v2 nonce-prefixed ciphertext
}
// KPS response:
{ _res: "ok", file_id: string, ver: string }
ver is mandatory. Each operation may read once after the initial write and send at most one more CAS write; a second conflict terminates. For a single-slot payload such as a 4S recovery key, an authoritative value exactly equal to the intended value is idempotent completion: adopt the latest ver without writing. A different value fails closed; never overwrite, concatenate, or choose by timestamp. Only a payload with a defined deterministic field-level merge may merge once and retry once.
action: "app_update"
body: {
name?: string,
name_enc?: Uint8Array
}
// -> { _res: "ok", name?: string, name_enc?: Uint8Array }
At least one of name or name_enc is required. An App may update only its own name, not AP scopes. name_enc uses the AP Account Key nonce(24) || secretbox format; an SSO/AppData client without the Account Key must not fabricate it with another key.
action: "app_logout"
body: {}
// -> { _res: "ok", status: "logged_out", app_id: string }
app_logout is SSO-only and authenticates the caller with the current sign_pk. On success, the server clears only that row's sign_pk; the app_id, enc_pk, AppData file, and capabilities remain. The caller then deletes or replaces its local signing keypair while retaining the encryption keypair. After the Safe approves the same enc_pk with a new sign_pk, the server reactivates that same row; every old-credential request fails while logged out. AP clients must not use this action.
action: "app_del"
body: {}
// -> { _res: "ok", id_deleted: string }
app_del permanently revokes the current app_id and is only for explicit user disconnection after a fully verified success; it is not normal logout or a failure-cleanup API. A delegated or alias response does not prove that this transaction created the binding, and the same SSO enc_pk may reuse an existing binding. Callback, finish, session persistence, or AppData failures clear only local pending/session state and never call app_logout or app_del automatically. Explicitly revoking the last binding referencing an AppData file deletes that file in the same transaction.POST /v3/app_verifyContent-Type: application/json
{
app_tag: "your_effective_tag",
app_id: string,
safe_id: string,
sign_pk: string // standard base64, same 32-byte Ed25519 pk as handshake
}
// Normal mismatch:
{ _res: "ok", valid: false }
// Verified:
{ _res: "ok", valid: true, nickname: string }
// Allowlist configuration/rejection:
{ _res: "err", code: "server_verification_disabled" | "ip_not_allowed",
request_ip: string }
This endpoint is for App/Browser claims with sso_key_owner=client. The caller must be in server_ips. Check _res first; an IP, configuration, or parameter error with _res=err is not valid=false. request_ip is the canonical IPv4/IPv6 used by KL; an invalid source is returned as an empty string rather than reflected. Add a non-empty value in MyDeveloper only after confirming it is your fixed egress. KL never returns the full allowlist. After verification, have your backend issue its own short-lived session; app_id is not a bearer token. The service may separately register encrypt_pk_base64 for data encrypted to the backend, but the App still uses only its own enc_pk/enc_sk; never give the backend enc_sk to the App.
POST /v3/sso_user_pubkeyContent-Type: application/json
{ app_tag: string, safe_id: string }
{ _res: "ok", found: false }
// or
{ _res: "ok", found: true, sign_pk: string } // standard base64 Ed25519 pk
Only an approved plugin backend with server_ips, plugin_approved, and plugin_api_url may call this endpoint. found=false intentionally does not reveal whether approval, binding, or a key is missing.
GET https://my.keylockr.app/svc_pk/{app_tag}{ _res: "ok", pk_base64: string }
This endpoint only publishes MyDeveloper encrypt_pk_base64; it neither publishes nor infers sso_key_owner. A non-empty value serves service-to-service profile/data encryption independently of plugin approval and handshake ownership. Only when sso_key_owner=backend is separately selected does KL ignore the request enc_pk and require the server to retain the matching enc_sk. Client ownership may keep a non-empty service key while the App handshake still uses its own key.
GET https://my.keylockr.app/svc_info/{urlencoded_app_tag}{ _res: "ok", title: string }
// Unknown or invalid identity:
{ _res: "err", code: "invalid svc" }
This public JSON endpoint lets third-party login pages retrieve the authoritative MyDeveloper display name. Put app_tag in one path segment using UTF-8 percent-encoding. The response contains only title; it does not return url, description, sso_key_owner, approval state, or private configuration. Treat the title as untrusted text, never inject it as HTML, and cache it for the lifetime of the current service configuration. On error, use your own product fallback name. The endpoint is IP-rate-limited; handle rate-limit responses with the standard try_later rules.
| Code/status | Required handling |
|---|---|
binding_key_invalid | Require sign_pk, and the client-owned request enc_pk, to be exactly 32 bytes. |
binding_sign_credential_conflict | Stop: the current sign_pk for this Safe/tag already belongs to another enc_pk identity, or a new signing credential attempted to claim an AP. |
binding_enc_identity_conflict | Stop: the enc_pk identity for this Safe/tag is already used by another binding or type; do not retry through another transport. |
sso_key_owner_invalid | Set MyDeveloper sso_key_owner to client or backend. |
sso_backend_key_required | Backend ownership requires a registered service encryption key. |
sso_backend_key_invalid | Correct the backend-owned service key to a standard-Base64 32-byte X25519 public key; do not fall back to the request key. |
sso_backend_key_ap_conflict | Backend ownership does not support AP; select client ownership or disable AP, then start a new handshake. |
binding_name_invalid | AppData/AP requires a 1–255 Unicode-character physical device name. |
sso_service_invalid | Use the effective app_tag, not a title or svc_id field. |
sso_data_not_approved | Enable AppData in MyDeveloper, then start a new handshake. |
ap_not_approved / ap_scope_not_approved | Request only an approved AP capability and scope set. |
binding_scope_invalid | Scopes require ap=true; when present they must be non-empty, non-blank, and unique. |
tmpid_expired | Create a new handshake, temporary WebSocket, and QR. |
data_expired | Calibrate Unix-second time with GET /clock, then retry. |
sign_invalid | Stop and ensure sign_sk matches the bound sign_pk. |
hash_mismatch | Fix sorted-key canonical MessagePack. |
app_404 / app_pkdata_invalid | Clear the local app_id and authorize again. |
app_action_not_allowed | The binding lacks that capability; do not retry to bypass it. |
app_credential_pending | Persistent invalidation is being replayed. Wait briefly and retry a limited number of times; never bypass it with an old session or cache. |
app_credential_stale | Stop the old KPS/WebSocket session and reconnect with the current credential; authorize again only if that credential is also invalid. |
server_error | A database or other backend dependency failed temporarily. Wait briefly and retry a bounded number of times with timeout and cancellation; do not log out or clear the keypair. |
app_logout_stale_credential | Concurrent reauthorization replaced the current credential; do not clear the new value, and complete only local logout. |
app_logout_not_sso | AP does not support normal SSO logout; retain its pairing keypair and use app_del only for permanent disconnection. |
app_not_ap | A non-AP binding cannot request an Account Key. |
app_file_not_found | An auth-only binding has no AppData; authorize again with app_data=true. |
file_ver_conflict | Read at most once; an equal single-slot value completes idempotently, a different value terminates, and a mergeable payload may write at most once more. |
server_verification_disabled | Configure non-empty server_ips in MyDeveloper; request_ip is the canonical observed source, or empty when it cannot be parsed. |
ip_not_allowed | Confirm request_ip is your fixed egress, then add that IPv4 or IPv6 address to server_ips. |
try_later N ... | Wait for the specified duration and retry only after a user action. |
try_later_auto N ... | Wait for the specified duration, then retry automatically. |
safe_auth_required | Wait for the filekey WebSocket push; poll only after disconnect or timeout. |
denied | Stop waiting and show that the Safe denied the request. |
Every wait and retry requires a local timeout and cancellation path. Reply to a KPS ping event with KPS pong. Never log private keys, the Account Key, filekeys, or decrypted AppData.
The authoritative closed JSON schema for /web/* alias login and registration is downloadable separately. Consumers must implement from the document rather than inspect KeyLockr source to infer fields.
| Endpoint/action | Encoding/identity | Purpose |
|---|---|---|
GET /v3/key_enc | plain base64 / public | Server X25519 public key. |
GET /v3/key_sign | plain base64 / public | Server Ed25519 public key. |
GET /v3/clock | JSON / public | Unix-second clock calibration. |
GET /v3/build_ver | JSON / public | Current deployed build, not cached. |
POST /v3/handshake | MessagePack / public | Create an App-capability authorization tmp. |
GET /v3/ws?sig= | KPS / signed query | Temporary or App WebSocket. |
POST /v3/ | KPS / app.* | Send an encrypted App action over HTTP. |
app_req_filekey | KPS / app.* | Request the AppData filekey for an older Safe or a later re-unlock. |
app_req_account_key | KPS / app.* AP | An authorized AP recovers its Account Key. |
app_get_data | KPS / app.* | Read AppData metadata, ciphertext, and version. |
app_set_data | KPS / app.* | Write AppData ciphertext with version CAS. |
app_update | KPS / app.* | Update the current client display name. |
app_logout | KPS / app.* SSO | Clear the current sign_pk while retaining the enc_pk, app_id, and AppData. |
app_del | KPS / app.* | Permanently revoke the current binding. |
ping / pong | KPS / current ID | Connection verification and keepalive. |
POST /v3/app_verify | JSON / service IP | Backend verification of an App/Safe binding. |
POST /v3/sso_user_pubkey | JSON / approved plugin IP | Approved plugin backend reads a bound Safe signing key. |
GET my.keylockr.app/svc_pk/{app_tag} | JSON / public | Third-party backend service encryption key, independent of ownership. |
GET my.keylockr.app/svc_info/{urlencoded_app_tag} | JSON / public | Public authoritative service title for third-party login pages. |
app_scan_info, app_add, safe_*, and general file_* actions belong to the Mobile Safe authorization/data flow and are not called by third-party SSO/AppData clients.
delegated.go is the recommended flow. It implements signed /start requests, a strict callback parser, offline KL assertion verification, the finish_secret check, and an atomic completion-store contract. The canonical fixture is shared by 9305, iOS, Android, and this example. main.go/kps.go/kps_decode.go/appdata.go retain only the legacy direct-client compatibility flow; delegated never falls back automatically.
# Delegated fixture and finish verification
env GOWORK=off go test ./...
# Legacy direct-client pure SSO compatibility example
go run . -app-tag YOUR_APP_TAG
# SSO + AppData read
go run . -app-tag YOUR_APP_TAG -app-data
# AppData read plus demo CAS write
go run . -app-tag YOUR_APP_TAG -app-data -write "hello"
# Restricted AP
go run . -app-tag YOUR_APP_TAG -ap -scopes 2fa
# Full AP
go run . -app-tag YOUR_APP_TAG -ap
The delegated example never calls app_verify. In the legacy compatibility command, app_verify succeeds only from a fixed egress IP allowed by server_ips. No example prints private keys, finish secrets, assertions, complete callbacks, the Account Key, filekeys, or decrypted data.
Completing this checklist covers every protocol requirement needed to implement and ship independently.