Guide
Everything you need to put Obsidian behind a product: pick an SDK, run a session, keep the right credential in the right place, and wire up the pieces around it. The two references, the management API and the runtime protocol, have their own pages.
Getting started
Obsidian has two planes. The control plane is where you and your team work: applications, products, licenses, resellers, releases, webhooks. You reach it from the workspace or with a management credential over the management API. The runtime is what your software talks to: it starts a session for a license key, checks it every five seconds, and signs every answer. Your code reaches it through an SDK, which speaks the runtime protocol.
The first integration is four steps:
- In the workspace, create an application and a product, and issue a license. Note the application id and the runtime public key shown on the application page.
- Add the SDK for your language (below) and construct a client with the runtime origin, the application id, the environment and the pinned public key.
- Call
initializewith the license key and a device fingerprint. Read entitlements from the claims that come back. - Heartbeat every five seconds. A refusal clears the session; stop protected work when it does.
Pin the public key through your own build or configuration. The runtime publishes its keys at /v1/auth/keys for rotation, but trust is established out of band, never by downloading a key at run time.
SDKs
Six SDKs (JavaScript, Python, Go, Rust, C#, C++) held to one contract by one set of vectors.
Every SDK verifies the Ed25519 envelope under the key you pin, binds the answer to the request it made, checks the session's shape, and shows a denial reason only when it arrived signed. The examples below are trimmed from the SDK sources and use their real methods.
npm install ./sdks/javascript # @obsidian/sdk, from the repositoryimport { ObsidianClient, DenialError } from "@obsidian/sdk";
const client = new ObsidianClient({
url: RUNTIME_ORIGIN, // https://auth.obsidian.rip
applicationId: APPLICATION_ID,
environment: "production",
publicKey: PINNED_ED25519_PUBLIC_KEY, // pinned out of band, never fetched
onState: (claims, error) => showAccessState(Boolean(claims), error?.message),
});
try {
await client.initialize({
license, // or { bridge_token } after a hosted sign-in
client_version: "1.0.0",
fingerprint,
fingerprint_version: 1,
});
client.startHeartbeats(); // every 5 s; a refusal clears client.session
const stream = client.subscribe({
onSessionEnded: (push) => console.log("ended:", push.action, push.reason),
onConfigChanged: () => reloadWhatYouCached(),
});
if (client.session?.entitlements["feature.pro"] === true) unlockPro();
} catch (e) {
if (e instanceof DenialError) show(e.code, e.reason); // reason only if signed
}Trimmed from sdks/javascript/src/index.ts; the full example is under sdks/javascript/examples and runs against the runtime in the suite where the toolchain is available.
What each SDK does
| Capability | JavaScript | Python | Go | Rust | C# | C++20 |
|---|---|---|---|---|---|---|
| Contract vectors pass | yes | yes | yes | yes | yes | yes |
| Signed envelope verification (Ed25519) | yes | yes | yes | yes | yes | yes |
| Binding: challenge, app, env, session, clock | yes | yes | yes | yes | yes | yes |
| Signed denial reasons | yes | yes | yes | yes | yes | yes |
| Key rotation (several pinned keys) | yes | yes | yes | yes | yes | yes |
| init, heartbeat, logout | yes | yes | yes | yes | yes | yes |
| Bridge token init (hosted login) | yes | yes | yes | yes | yes | yes |
| Live notification stream | yes | no | no | no | no | no |
| Heartbeat scheduler | yes | no | no | no | no | no |
| Generation guard and cancellation | yes | no | no | no | no | no |
Only the JavaScript SDK runs the heartbeat timer itself; the others expose heartbeat() for your application to call every five seconds on its own thread or loop. Transports: fetch, the Python standard library, net/http, ureq, HttpClient, and an injected transport for C++ (a libcurl one is provided). The SDKs are distributed from the repository rather than from package registries.
A session, end to end
- Initialize. The client sends the application id, the environment, a fresh 32-byte challenge, a timestamp, the license key (or a bridge token from a hosted sign-in), its version and a device fingerprint. The runtime checks the license, the customer, the product, the application's availability, the version floor and the device cap, binds the device if there is room, and answers with a signed session.
- Read the claims. After verification the SDK exposes
entitlements(with the layer each value came from),expires_at, the newest release on the license's channel, typed configuration, and for a floating license the seat budget. - Heartbeat. Every five seconds the client sends the session id, the current token and a fresh challenge. The runtime re-reads the whole policy, rotates the token, and answers signed. A session that misses its thirty-second deadline expires.
- Refusal. Any refusal carries a code. When you gave a reason (a termination, a revocation, a freeze), it arrives in a statement signed with the runtime key and bound to that very request; the SDK shows it only when it verifies.
- Logout. Ends the session; the answer is signed and marked terminated.
Optional: subscribe (JavaScript) opens the event stream, so a running client learns at once that its session ended or its configuration changed. Live, signed notifications to running clients. The heartbeat stays the authority.
Which credential goes where
The rule is where each credential may live, not what it can do.
| Credential | Proves | Lives in | Never in |
|---|---|---|---|
| License key | The holder's right to run this software | The end user's machine | A server you run, logs, analytics |
| Bridge token | A live hosted sign-in, for one device | The end user's machine, for two minutes, once | Anywhere after it is used |
| Session token | A running session | The client's memory | Disk, logs, another process |
| Management credential | Your organization's authority | Your servers, CI secrets, an operator's CLI | Any client an end user runs, any binary that ships, any browser |
| Hosted access token | A holder, to one of your applications | The application's process, short-lived | Persistent storage beyond its lifetime |
A runtime SDK therefore holds a license key or a bridge token and a session token, nothing else. None of the SDK examples takes a management credential.
Hosted login
Hosted login: Authorization Code + PKCE, with license gating and refresh-token reuse detection.
The hosted page offers every sign-in method you enabled (key, password, magic link, Discord, passkey) under your brand and, on a custom host, your hostname. Approval requires an active license of the holder's for the client's application and, if you name one, an entitlement. The ID token is EdDSA under the runtime key you already pin.
# 1. Send the browser to the hosted sign-in. PKCE S256 is required.
GET /api/v1/hosted/authorize?client_id=…&redirect_uri=…
&code_challenge=…&code_challenge_method=S256&state=…
# 2. Exchange the code for tokens.
POST /api/v1/hosted/token
{ "grant_type": "authorization_code", "code": "…", "code_verifier": "…",
"redirect_uri": "…", "client_id": "…" }
# 3. Bridge into a runtime session on this device. Single use, two minutes.
POST /api/v1/hosted/bridge Authorization: Bearer <access token>
→ { "bridge_token": "…", "expires_in": 120 }
client.initialize({ bridge_token, client_version, fingerprint, fingerprint_version: 1 })A hosted sign-in can bridge into a runtime session on one device with a single-use, two-minute token, so a desktop application never handles a password.
Webhooks and the feed
Every change writes an event in the same transaction, and a single relay gives events an exact sequence. You can have them pushed (webhooks) or pull them (the feed), or both.
Webhooks
Webhooks that are signed, retried, inspectable and replayable.
POST https://your.example/hooks/obsidian
Content-Type: application/json
X-Obsidian-Event: 7d1c… # the event id
X-Obsidian-Delivery: 2a9e… # this attempt's delivery id
X-Obsidian-Timestamp: 1789456123
X-Obsidian-Signature: v1=<hex HMAC-SHA256 over "timestamp.delivery.body">
# two v1= values while a secret rotates
{"id":"7d1c…","action":"license.revoked","resource_id":"…","organization_id":"…",
"application_id":"…","environment":"production","actor_type":"member","actor_id":"…",
"details":{…},"request_id":"…","created_at":"…","sequence":8811}Verify X-Obsidian-Signature by computing HMAC-SHA256 over timestamp.delivery.body with your endpoint's secret. While a secret rotates, the header carries two v1= values; accept either. Answer 2xx quickly; anything else is retried with backoff and, after the retries, dead-lettered where you can inspect and replay it.
The feed
Event catch-up from an exact, gap-free sequence. No webhook required.
GET /api/v1/events/feed?after=<cursor>&limit=100
Authorization: Bearer <management credential>
{"items":[…],"cursor":"…","watermark":8812,"has_more":false}Cursors are opaque and bound to your organization, your credential's application and the action filter you asked for. Store the cursor from each page and continue from it; ?after=latest starts at the present.
Release delivery
Private object storage; five-minute authorized downloads; every grant recorded.
- Release channels per license, reported in the runtime answer.
- Downloads can require a current entitlement.
- SHA-256 and approved provenance on every file; per-holder watermarked builds, sealed and revocable.
A holder downloads through the portal or a hosted flow; each download is an immutable grant recording the file, the license, the holder, the address and the agent, and the signed URL behind it lives five minutes.
The vault
Secrets your software fetches at run time instead of shipping: handed only to a live session that resolves the gate, and never to anything else.
Hold a secret in the workspace (Application, Configuration, Vault): the key that decrypts your asset bundle, a token to your own backend, a value that unlocks a feature. Give it a gate if you want one, and only a license that resolves that entitlement receives it. Your software asks for it at run time.
// The software does not ship the key to its own assets. It asks
// for it, and only a live session that resolves the gate is given one.
const held = await client.vault();
const bundleKey = held["assets.bundle_key"].value; // { value, version }
await openAssetBundle(bundleKey); // nothing works without it
// A session that has been revoked since it started gets a refusal here,
// not a stale value: the whole policy is re-checked at the moment you ask.The answer is a signed envelope bound to the asking session, like every other runtime answer, so what it carries cannot have been put there by anything but the runtime. Fetching is a read: your token does not rotate and your deadline does not move, so you can ask at the moment a feature is opened rather than holding the value for the whole session.
Why this is the strongest thing you can do
A licence check your software makes on its own machine can be patched. A value your software does not have, cannot derive, and cannot work without is a different problem for an attacker: they have to obtain a real session to get it, and you can rotate it the moment you see it leak. Put the thing your product actually needs behind the vault and a cracked build is an empty one.
Honest limit: once a value reaches a legitimate session it is in that machine's memory while the software uses it, and a determined attacker on that machine can read it. Rotation and per-gate scoping are what keep an extracted value narrow and short-lived. This raises the cost of a crack; it does not make one impossible, and we do not claim otherwise.
Making a crack expensive
Obsidian keeps authorization under continuous server authority, and the protocol gives a network attacker nothing: answers cannot be forged, requests cannot be replayed, tokens rotate away, sessions cannot be lifted between applications. What no authorization server can do is stop someone who owns the machine from patching a binary that decides entirely on its own whether to run. So design so that the patch is worth as little as possible.
- Verify, and keep verifying. Do not fork the SDK to skip the pinned-key check, and pin the key through your build rather than fetching it at run time.
- Read features from the signed entitlements. They ride inside the signed payload, not beside it, so a flipped boolean yields a build with nothing in it.
- Put what you can behind the vault. Assets, keys, backend tokens: anything your software cannot function without and cannot reconstruct.
- Keep server work on the server. Anything computed behind a hosted session cannot be patched out of a client.
- Gate your downloads, and ship per-holder builds. Obtaining the artifact is then itself gated, and a leaked copy names where it came from.
- Heartbeat, and act on refusals. Stop protected work when a heartbeat is refused or its deadline passes. Never cache a claim as permanent.
- Do not treat the licence key as a secret from the user. It is theirs and they have it. The protection is the server's continued authority over the session, not the key's secrecy.
The CLI
A CLI for the management API, credential from the environment, never an argument.
export OBSIDIAN_URL=https://obsidian.rip
export OBSIDIAN_CREDENTIAL=… # from the environment, never an argument
npm run cli -- licenses issue --app <id> --product <id> --email a@b --days 30
npm run cli -- licenses list --app <id> --status active
npm run cli -- licenses keys --app <id> --id <license>
npm run cli -- feed --after latest
npm run cli -- limits
npm run cli -- openapi
npm run cli -- call GET /applicationscall reaches any route on the management API as the credential; openapi prints the reference the API serves.