Quickstart

One round trip,
start to finish.

Request access, unlock with your passphrase and Secret Key, put a secret, get it back and issue a scoped machine credential. Same flow in every language. The crypto runs on your side, so the server only ever handles ciphertext.

1 request access 2 set up & unlock 3 put a secret 4 get it back 5 issue a machine credential
Real HTTP with the standard net/http. The crypto steps are described in prose, not as a callable method.
1Enrol

One call to the public enrollment endpoint mints an account shell and returns the one-time claim. No approval step, no body needed.

var base = "https://secrets.lab.stratus5.net"

res, _ := http.Post(base+"/v1/enroll", "application/json", strings.NewReader("{}"))
// { "account_id": "acct_...", "claim": "acct_...:tok_..." }   the claim is one-time
2Set up and unlock

Client-side crypto. Generate a 256-bit Secret Key and keep it as your Emergency Kit. Derive the Account Unlock Key as Argon2id(passphrase, salt) XOR HKDF-SHA256(SK, account_id, "auk"). Generate an X25519 account keypair, wrap the private half under the AUK, generate a first vault key and wrap it to the account public key. See the 2SKD section. Then post the public and wrapped material to init.

// after deriving verifier/pubkey/enc_privkey/wrapped_vk client-side:
body := `{ "claim":"acct_...:tok_...", "account_id":"acct_...",
  "account_pubkey":"<b64>", "enc_account_privkey":"<b64>",
  "kdf_salt":"<b64>", "auth_verifier_salt":"<b64>", "auth_verifier":"<b64>",
  "first_vault": { "vault_id":"vault_...", "wrapped_vk":"<b64>" } }`
http.Post(base+"/v1/accounts/init", "application/json", strings.NewReader(body))
// → { "account_id":"acct_..." }   the account is now active

To unlock a session, run the SRP-6a exchange: POST /v1/auth/srp/start with your account id and ephemeral a, derive the shared key from the returned salt, b and handle, then POST /v1/auth/srp/finish with the handle and m1. The passphrase and Secret Key never leave the client. The response carries a token you send as Authorization: Bearer <token> on later calls.

3Put a secret

Client-side crypto. Compute key_ref = HMAC-SHA256(index_key, normalize(name)) and encrypt the value with XChaCha20-Poly1305 under the vault key, with the AAD bound to account_id ∥ vault_id ∥ key_ref. Then PUT the ciphertext.

req, _ := http.NewRequest("PUT", base+"/v1/vaults/"+vaultID+"/secrets/"+keyRef,
    strings.NewReader(`{"ciphertext":"<b64>","nonce":"<b64>","alg":"xchacha20poly1305","protection":"aukwrap"}`))
req.Header.Set("content-type", "application/json")
req.Header.Set("authorization", "Bearer "+token)
http.DefaultClient.Do(req)
4Get it back

GET the same ref, then decrypt the returned blob client-side. A tampered ciphertext or wrong key fails the Poly1305 tag and decryption fails closed.

req, _ := http.NewRequest("GET", base+"/v1/vaults/"+vaultID+"/secrets/"+keyRef, nil)
req.Header.Set("authorization", "Bearer "+token)
got, _ := http.DefaultClient.Do(req)
// { "ciphertext":"<b64>", "nonce":"<b64>", "alg":"xchacha20poly1305", "protection":"aukwrap", "enc_key_name":"<b64>" }
// then: plaintext = xchacha20poly1305.Open(VK, nonce, ciphertext, aad)  // client-side
5Issue a scoped machine credential

Client-side crypto. Generate a machine key, derive auth_secret = HKDF(MK,"auth") and unwrap_key = HKDF(MK,"unwrap"), wrap the vault key under the unwrap key and compute Argon2id(auth_secret) as the verifier. The machine key is the one secret you give the consumer. See the machine credentials section.

req, _ := http.NewRequest("POST", base+"/v1/vaults/"+vaultID+"/credentials/machine",
    strings.NewReader(`{"credential_id":"mach_...","label":"ci-prod","auth_verifier_salt":"<b64>","auth_verifier":"<b64>","wrapped_vk":"<b64>"}`))
req.Header.Set("content-type", "application/json")
req.Header.Set("authorization", "Bearer "+token)
http.DefaultClient.Do(req)
// the consumer then POSTs { credential_id, auth_secret } to /v1/auth/machine for a vault-scoped session
Real HTTP with java.net.http.HttpClient. The crypto steps are described in prose, not as a callable method.
1Enrol

One call to the public enrollment endpoint mints an account shell and returns the one-time claim. No approval step, no body needed.

var http = HttpClient.newHttpClient();
var base = "https://secrets.lab.stratus5.net";

var enroll = HttpRequest.newBuilder(URI.create(base + "/v1/enroll"))
    .header("content-type", "application/json")
    .POST(BodyPublishers.ofString("{}"))
    .build();
var res = http.send(enroll, BodyHandlers.ofString());
// { "account_id": "acct_...", "claim": "acct_...:tok_..." }   the claim is one-time
2Set up and unlock

Client-side crypto. Generate a 256-bit Secret Key and keep it as your Emergency Kit. Derive the Account Unlock Key as Argon2id(passphrase, salt) XOR HKDF-SHA256(SK, account_id, "auk"). Generate an X25519 account keypair, wrap the private half under the AUK, generate a first vault key and wrap it to the account public key. See the 2SKD section. Then post the public and wrapped material to init.

// after deriving verifier/pubkey/enc_privkey/wrapped_vk client-side:
var body = """
  { "claim":"acct_...:tok_...", "account_id":"acct_...",
    "account_pubkey":"<b64>", "enc_account_privkey":"<b64>",
    "kdf_salt":"<b64>", "auth_verifier_salt":"<b64>", "auth_verifier":"<b64>",
    "first_vault": { "vault_id":"vault_...", "wrapped_vk":"<b64>" } }""";
http.send(HttpRequest.newBuilder(URI.create(base + "/v1/accounts/init"))
    .header("content-type", "application/json")
    .POST(BodyPublishers.ofString(body)).build(), BodyHandlers.ofString());
// → { "account_id":"acct_..." }   the account is now active

To unlock a session, run the SRP-6a exchange: POST /v1/auth/srp/start with your account id and ephemeral a, derive the shared key from the returned salt, b and handle, then POST /v1/auth/srp/finish with the handle and m1. The passphrase and Secret Key never leave the client. The response carries a token you send as Authorization: Bearer <token> on later calls.

3Put a secret

Client-side crypto. Compute key_ref = HMAC-SHA256(index_key, normalize(name)) and encrypt the value with XChaCha20-Poly1305 under the vault key, with the AAD bound to account_id ∥ vault_id ∥ key_ref. Then PUT the ciphertext.

http.send(HttpRequest.newBuilder(URI.create(base + "/v1/vaults/" + vaultId + "/secrets/" + keyRef))
    .header("content-type", "application/json")
    .header("authorization", "Bearer " + token)
    .PUT(BodyPublishers.ofString(
       "{\"ciphertext\":\"<b64>\",\"nonce\":\"<b64>\",\"alg\":\"xchacha20poly1305\",\"protection\":\"aukwrap\"}"))
    .build(), BodyHandlers.ofString());
4Get it back

GET the same ref, then decrypt the returned blob client-side. A tampered ciphertext or wrong key fails the Poly1305 tag and decryption fails closed.

var got = http.send(HttpRequest.newBuilder(URI.create(base + "/v1/vaults/" + vaultId + "/secrets/" + keyRef))
    .header("authorization", "Bearer " + token)
    .GET().build(), BodyHandlers.ofString());
// { "ciphertext":"<b64>", "nonce":"<b64>", "alg":"xchacha20poly1305", "protection":"aukwrap", "enc_key_name":"<b64>" }
// then: plaintext = XChaCha20-Poly1305.open(VK, nonce, ciphertext, aad)  // client-side
5Issue a scoped machine credential

Client-side crypto. Generate a machine key, derive auth_secret = HKDF(MK,"auth") and unwrap_key = HKDF(MK,"unwrap"), wrap the vault key under the unwrap key and compute Argon2id(auth_secret) as the verifier. The machine key is the one secret you give the consumer. See the machine credentials section.

http.send(HttpRequest.newBuilder(URI.create(base + "/v1/vaults/" + vaultId + "/credentials/machine"))
    .header("content-type", "application/json")
    .header("authorization", "Bearer " + token)
    .POST(BodyPublishers.ofString(
       "{\"credential_id\":\"mach_...\",\"label\":\"ci-prod\",\"auth_verifier_salt\":\"<b64>\",\"auth_verifier\":\"<b64>\",\"wrapped_vk\":\"<b64>\"}"))
    .build(), BodyHandlers.ofString());
// the consumer then POSTs { credential_id, auth_secret } to /v1/auth/machine for a vault-scoped session
Real HTTP with the built-in fetch. The crypto steps are described in prose, not as a callable method.
1Enrol

One call to the public enrollment endpoint mints the account and returns the one-time claim. No approval, no body.

const base = "https://secrets.lab.stratus5.net";

const r = await fetch(`${base}/v1/enroll`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: "{}",
});
const { account_id, claim } = await r.json(); // claim is one-time, consumed by init
2Set up and unlock

Client-side crypto. Generate a 256-bit Secret Key as your Emergency Kit. Derive the AUK as Argon2id(passphrase, salt) XOR HKDF-SHA256(SK, account_id, "auk"). Build the X25519 account keypair, wrap its private half under the AUK, make a first vault key wrapped to the account public key. See the 2SKD section.

await fetch(`${base}/v1/accounts/init`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    claim, account_id,
    account_pubkey: pubB64, enc_account_privkey: encPrivB64,
    kdf_salt: kB64, auth_verifier_salt: avsB64, auth_verifier: avB64,
    first_vault: { vault_id: vaultId, wrapped_vk: wvkB64 },
  }),
});
// → { account_id }   the account is now active

For a session, run the SRP-6a exchange: POST /v1/auth/srp/start with a, derive the shared key from the returned salt, b and handle, then POST /v1/auth/srp/finish with the handle and m1. The response carries a token you send as Authorization: Bearer <token>. Neither secret leaves the process.

3Put a secret

Client-side crypto. Compute key_ref = HMAC-SHA256(index_key, normalize(name)) and encrypt with XChaCha20-Poly1305 under the vault key, AAD bound to account_id ∥ vault_id ∥ key_ref. Then PUT the ciphertext.

await fetch(`${base}/v1/vaults/${vaultId}/secrets/${keyRef}`, {
  method: "PUT",
  headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
  body: JSON.stringify({ ciphertext: ctB64, nonce: nonceB64, alg: "xchacha20poly1305", protection: "aukwrap" }),
});
4Get it back

GET the ref, then decrypt client-side. Altered ciphertext fails the tag and decryption fails closed.

const g = await fetch(`${base}/v1/vaults/${vaultId}/secrets/${keyRef}`, {
  headers: { authorization: `Bearer ${token}` },
});
const blob = await g.json(); // { ciphertext, nonce, alg, protection, enc_key_name }
// plaintext = xchacha20poly1305_open(VK, blob.nonce, blob.ciphertext, aad)  // client-side
5Issue a scoped machine credential

Client-side crypto. Generate the machine key, derive HKDF(MK,"auth") and HKDF(MK,"unwrap"), wrap the vault key under the unwrap key and store Argon2id(auth_secret) as the verifier. The machine key is the only secret the consumer needs. See the machine credentials section.

await fetch(`${base}/v1/vaults/${vaultId}/credentials/machine`, {
  method: "POST",
  headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
  body: JSON.stringify({
    credential_id: "mach_...", label: "ci-prod",
    auth_verifier_salt: mavsB64, auth_verifier: mavB64, wrapped_vk: mwvkB64,
  }),
});
// consumer posts { credential_id, auth_secret } to /v1/auth/machine for a vault-scoped session
Real HTTP with System.Net.Http.HttpClient. The crypto steps are described in prose, not as a callable method.
1Enrol

One call mints the account and returns the one-time claim. No approval, no body.

var http = new HttpClient { BaseAddress = new Uri("https://secrets.lab.stratus5.net") };

var res = await http.PostAsync("/v1/enroll",
    new StringContent("{}", Encoding.UTF8, "application/json"));
// { "account_id": "acct_...", "claim": "acct_...:tok_..." }   claim is one-time
2Set up and unlock

Client-side crypto. Generate a 256-bit Secret Key as your Emergency Kit. Derive the AUK as Argon2id(passphrase, salt) XOR HKDF-SHA256(SK, account_id, "auk"). Build the X25519 account keypair, wrap the private half under the AUK, make a first vault key wrapped to the account public key. See the 2SKD section.

var body = """
  { "claim":"acct_...:tok_...", "account_id":"acct_...",
    "account_pubkey":"<b64>", "enc_account_privkey":"<b64>",
    "kdf_salt":"<b64>", "auth_verifier_salt":"<b64>", "auth_verifier":"<b64>",
    "first_vault": { "vault_id":"vault_...", "wrapped_vk":"<b64>" } }""";
await http.PostAsync("/v1/accounts/init",
    new StringContent(body, Encoding.UTF8, "application/json"));
// → { "account_id":"acct_..." }   the account is now active

For a session, run the SRP-6a exchange against /v1/auth/srp/start and /v1/auth/srp/finish: send your account id and a, derive the shared key from the returned salt, b and handle, then send the handle and m1. The response carries a token you send as Authorization: Bearer <token>. The passphrase and Secret Key never leave the client.

3Put a secret

Client-side crypto. Compute key_ref = HMAC-SHA256(index_key, normalize(name)) and encrypt with XChaCha20-Poly1305 under the vault key, AAD bound to account_id ∥ vault_id ∥ key_ref. Then PUT the ciphertext.

http.DefaultRequestHeaders.Add("authorization", $"Bearer {token}");
await http.PutAsync($"/v1/vaults/{vaultId}/secrets/{keyRef}",
    new StringContent(
        "{\"ciphertext\":\"<b64>\",\"nonce\":\"<b64>\",\"alg\":\"xchacha20poly1305\",\"protection\":\"aukwrap\"}",
        Encoding.UTF8, "application/json"));
4Get it back

GET the ref and decrypt client-side. Tampered ciphertext fails the Poly1305 tag and decryption fails closed.

var g = await http.GetStringAsync($"/v1/vaults/{vaultId}/secrets/{keyRef}");
// { "ciphertext":"<b64>", "nonce":"<b64>", "alg":"xchacha20poly1305", "protection":"aukwrap", "enc_key_name":"<b64>" }
// plaintext = XChaCha20Poly1305.Open(VK, nonce, ciphertext, aad);  // client-side
5Issue a scoped machine credential

Client-side crypto. Generate the machine key, derive HKDF(MK,"auth") and HKDF(MK,"unwrap"), wrap the vault key under the unwrap key and store Argon2id(auth_secret) as the verifier. Hand the machine key to the consumer. See the machine credentials section.

await http.PostAsync($"/v1/vaults/{vaultId}/credentials/machine",
    new StringContent(
        "{\"credential_id\":\"mach_...\",\"label\":\"ci-prod\",\"auth_verifier_salt\":\"<b64>\",\"auth_verifier\":\"<b64>\",\"wrapped_vk\":\"<b64>\"}",
        Encoding.UTF8, "application/json"));
// consumer posts { credential_id, auth_secret } to /v1/auth/machine for a vault-scoped session

Once you can put and get a secret, prove the guarantee for yourself. The testing section walks a round-trip assert and a tamper assert against a local instance. The full endpoint reference is in the docs.