Skip to content

Runtime protocol

What your software and the runtime say to each other. Six requests, one signed envelope, and a set of refusal codes your users can be told about in your own words.

The signed envelope

Cryptographically signed authorization responses, verified under a key you pin.

Every accepted answer has the same shape. The SDK verifies the signature under the key you pinned, then decodes the payload and checks that it answers the request it made: the same challenge, application and environment, a server timestamp within thirty seconds of the client's clock, and for heartbeat and logout the session that was asked about. Only then are the claims exposed.

An accepted answerjson
{
  "key_id": "v1",
  "payload": "<base64url of the exact JSON bytes that were signed>",
  "signature": "<base64url Ed25519 signature over those bytes>"
}

The runtime is at https://auth.obsidian.rip; the same routes also answer at https://obsidian.rip/v1/auth/…. The current and any still-trusted keys are published at GET /v1/auth/keys so a rotation can be pinned ahead of time. Signing keys rotate with zero client downtime.

Endpoints

Each request carries a fresh challenge and a timestamp. Nonce and clock binding on every request; replays are refused before they touch the database.

Starting a sessionjson
POST https://auth.obsidian.rip/v1/auth/init
{
  "application_id": "…", "environment": "production",
  "challenge": "<32 random bytes, base64url>", "timestamp": 1789456123,
  "license": "ATL-…",                  // or "bridge_token": "…"
  "client_version": "1.0.0",
  "fingerprint": "…", "fingerprint_version": 1,
  "sdk_version": "javascript/0.1.0"
}
post/v1/auth/init

Initialize a licensed device session. Start a session with a license key or, after a hosted sign-in, a bridge token. The answer binds this device and carries the session, the entitlements and everything else the client needs.

application_idstring, uuid · required
environmentdevelopment | staging | production · required
challengestring · required
Unpadded canonical base64url of 16–64 cryptographically random bytes. Never reuse.
timestampinteger · required
Unix seconds within 30 seconds of server time.
licensestring · optional
A license key. Exactly one of license or bridge_token.
client_versionstring · required
fingerprintstring · required
fingerprint_version1 (constant) · required
sdk_versionstring · required
bridge_tokenstring · optional
A single-use, two-minute bridge token minted by POST /api/v1/hosted/bridge after a hosted sign-in, bound to that sign-in's license and to this device's fingerprint. Exactly one of license or bridge_token.
device_proof · optional
protection_epochinteger · optional
The generation of client protection this build belongs to. An application that has retired older generations refuses anything below its floor with EPOCH_OUTDATED, and judges every later heartbeat against the floor in force at that moment.

Answers: 200 Signed ACTIVE session. Includes rotating token, entitlement sources, 30-second expiry and 5-second heartbeat interval. · 400 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 403 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 409 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 429 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 503 Stable error code; fail closed. Use a fresh challenge for any subsequent request..

post/v1/auth/heartbeat

Recheck current policy and rotate session token. Every five seconds. The runtime re-reads the license, the customer, the product, the application and the device as they are now, rotates the token, and answers signed. A session that misses its thirty-second deadline expires.

application_idstring, uuid · required
environmentdevelopment | staging | production · required
challengestring · required
Unpadded canonical base64url of 16–64 cryptographically random bytes. Never reuse.
timestampinteger · required
Unix seconds within 30 seconds of server time.
session_idstring, uuid · required
tokenstring · required

Answers: 200 Signed ACTIVE session. Includes rotating token, entitlement sources, 30-second expiry and 5-second heartbeat interval. · 400 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 403 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 409 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 429 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 503 Stable error code; fail closed. Use a fresh challenge for any subsequent request..

post/v1/auth/refresh

Refresh using identical heartbeat rotation rules. The same rotation as a heartbeat, for a client that wants a fresh token outside the heartbeat cadence. The old token never rotates again.

application_idstring, uuid · required
environmentdevelopment | staging | production · required
challengestring · required
Unpadded canonical base64url of 16–64 cryptographically random bytes. Never reuse.
timestampinteger · required
Unix seconds within 30 seconds of server time.
session_idstring, uuid · required
tokenstring · required

Answers: 200 Signed ACTIVE session. Includes rotating token, entitlement sources, 30-second expiry and 5-second heartbeat interval. · 400 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 403 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 409 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 429 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 503 Stable error code; fail closed. Use a fresh challenge for any subsequent request..

post/v1/auth/logout

Terminate a session. Ends the session. The answer is a signed envelope whose authorization_state is TERMINATED.

application_idstring, uuid · required
environmentdevelopment | staging | production · required
challengestring · required
Unpadded canonical base64url of 16–64 cryptographically random bytes. Never reuse.
timestampinteger · required
Unix seconds within 30 seconds of server time.
session_idstring, uuid · required
tokenstring · required

Answers: 200 Signed TERMINATED acknowledgement; no token or active claims. · 400 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 403 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 409 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 429 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 503 Stable error code; fail closed. Use a fresh challenge for any subsequent request..

get/v1/auth/keys

Discover current public key metadata; clients must pin trust out of band. The signing key in use and every key clients may still trust, so a rotation can be pinned ahead of time. Trust is established out of band; a client must not adopt a key it reads here.

Answers: 200 Current signing key.

What an answer carries

Beyond the session itself, the init and heartbeat payloads carry what a client needs to behave without a second request.

session_id, token, expires_at
The session, the token to send with the next heartbeat (it rotates every time), and when the session lapses without one. Stop protected work at expires_at if the network is gone.
principal_id, device_id
The holder and the bound device, as stable identifiers.
entitlements, entitlement_sources
Every key resolved for this license, with the layer each value came from. Entitlements resolve through six layers with one implementation, and every value says where it came from.
release, rollout
On init: the newest published release on the license's channel with update_available, and a rollout policy when one applies. A required rollout below its target refuses with VERSION_BLOCKED.
configuration, configuration_revision, preferences
Typed remote configuration in the signed answer, with server-only values that never leave the control plane. Preferences are the holder's own values under your preference schema, never an entitlement.
seats
For a floating license: {max, used, available} in every accepted answer. Floating licensing with a seat budget that holds under concurrency.
heartbeat_interval, authorization_state
Five seconds, and ACTIVE; a logout answers a terminal payload marked TERMINATED.

Refusals

A refusal is a 403 with a code (a 400 for a malformed request, a 429 when a ceiling is reached).Denials can carry your reason, signed, so the client can show it. You can also give each code your own explanation and next step in the workspace; the portal and your software can show those instead of the platform's words.

A refusal with a signed reasonjson
HTTP 403
{
  "error": { "code": "LICENSE_FROZEN", "message": "…", "request_id": "…" },
  "statement": { "key_id": "v1", "payload": "…", "signature": "…" }
}
// the statement's payload: { protocol_version: 1, request_id, session_id, application_id,
//   environment, server_timestamp, challenge, authorization_state: "DENIED", code, reason }
CodeWhyWhat the holder can do
CREDENTIAL_INVALIDThe license key is not one this application knows.Check the key for typing mistakes, or redeem it in the portal.
TOKEN_INVALIDThe session token did not match the session.Restart the application to sign in again.
SESSION_EXPIREDThe session went too long without a heartbeat.Restart the application to sign in again.
SESSION_REVOKEDThe session was ended because something about the license, device or account changed.Sign in again; if it happens again, check the license in the portal.
SESSION_TERMINATEDThe session was ended on purpose, by you from the portal or by the seller.Sign in again if it was not you.
LICENSE_INACTIVEThe license is revoked or banned.Nothing on their side; this is your decision.
SEATS_EXHAUSTEDEvery seat of this license is in use by a running session somewhere else.Close the application on another machine, or wait for its session to expire, then try again.
LICENSE_FROZENThe license is frozen: its time is held and it cannot be used until it is unfrozen.Unfreeze it in the portal, if the seller allows that, or contact the seller.
LICENSE_EXPIREDThe license's time has run out.Add time to it in the portal, or buy a new key.
LICENSE_UNREDEEMEDThis application requires a key to be claimed before it works.Redeem the key in the portal, then sign in.
ORGANIZATION_INACTIVEThe seller's account is not active.Nothing on their side; this is your decision.
APPLICATION_INACTIVEThe seller has paused this application for everyone.Nothing on their side; this is your decision.
APPLICATION_MAINTENANCEThe application is in maintenance: running sessions continue, new ones wait.Nothing on their side; this is your decision.
ENVIRONMENT_INVALIDThe application asked for an environment that does not exist here.Nothing on their side; this is your decision.
VERSION_BLOCKEDThe version of the application you are running is not allowed any more.Update the application.
CUSTOMER_BANNEDYour account has been banned by the seller.Nothing on their side; this is your decision.
ENTITLEMENT_INVALIDThe product this license is for is no longer offered.Nothing on their side; this is your decision.
DEVICE_LIMIT_REACHEDThe license is already in use on as many devices as it allows.Reset a device in the portal, or sign in from one already registered.
DEVICE_NOT_ENROLLEDThis application requires a device key, and this machine has not enrolled one.Run the application on this machine to enrol it, then sign in again.
DEVICE_PROOF_INVALIDThis machine could not prove it holds the key it enrolled.Reinstall or re-enrol on this machine. A copied installation cannot sign in on another computer.
DEVICE_TRUST_INSUFFICIENTThis machine's device key is weaker than this application accepts.Nothing on their side; this is your decision.
DEVICE_KEY_IN_USEThat device key is already enrolled to another machine on this licence.Nothing on their side; this is your decision.
EPOCH_OUTDATEDThis build belongs to a generation of client protection the seller has retired.Update to the current build.
LEASE_UNAVAILABLEThis application does not issue offline leases.Nothing on their side; this is your decision.
LEASE_REQUIRES_DEVICE_KEYAn offline lease is bound to this machine's device key, and none is enrolled.Enrol a device key, then request the lease again.
DEVICE_BLOCKEDThis device has been blocked by the seller.Nothing on their side; this is your decision.
RISK_DENIEDThe seller's policy denies this sign-in.Nothing on their side; this is your decision.
RATE_LIMITEDToo many requests in a short time.Wait a minute and try again.
CHALLENGE_STALEThe device's clock is more than thirty seconds off.Set the device's clock correctly and try again.
CHALLENGE_REUSEDA request was repeated with the same one-time challenge.Restart the application; if it persists, update it.
BRIDGE_INVALIDThe sign-in hand-off to the application is unknown or has expired.Sign in again in the application.
BRIDGE_REPLAYEDA sign-in hand-off was used a second time; the session it started has been ended.Sign in again in the application. If you did not do this, change your password.
BRIDGE_MISMATCHThe sign-in hand-off was for a different device.Sign in again on this device.
BRIDGE_REVOKEDThe hosted sign-in behind the hand-off was signed out.Sign in again in the application.

The SDK adds its own refusals when an answer fails verification: INVALID_ENVELOPE, UNTRUSTED_KEY, SIGNATURE_INVALID, RESPONSE_BINDING_INVALID, SESSION_BINDING_INVALID and SESSION_INVALID. None of these came from the runtime, and none carries a reason.

The event stream

Live, signed notifications to running clients. The heartbeat stays the authority.

GET /v1/auth/stream opens a server-sent event stream bound to one session, authorized with the same proof a heartbeat carries. It begins with a retry hint and a hello frame, then delivers session.ended (the session was terminated, the license revoked or frozen, the device blocked or reset, the customer banned) and config.changed (something about the license, its entitlements, the product, configuration, releases or announcements changed) as they happen, and ends with bye when a second stream replaces it or the client falls behind.

Subscribinghttp
GET https://auth.obsidian.rip/v1/auth/stream
Authorization: Bearer <the current session token>
X-Obsidian-Session: <session id>
X-Obsidian-Application: <application id>
X-Obsidian-Environment: production

retry: 3200
event: hello
data: {"key_id":"v1","payload":"…","signature":"…"}

event: session.ended
data: {"key_id":"v1","payload":"…","signature":"…"}   // payload.action, .reason, .sequence

Every frame's data is a signed envelope, verified and bound to the session before its type is read. A push carries no claim: on session.ended the client asks the heartbeat, and it is the heartbeat's refusal that clears the session; on config.changed nothing changes until you re-read what you cached. A severed stream changes nothing either; the session expires at the deadline it already had. The JavaScript SDK's subscribe does all of this; see the SDK examples.