Tenzro
Security and verification

API keys.

Tenzro API keys are per-operatorcredentials issued by a node operator to gate access to operator-mediated upstreams on that node. Every operator runs their own issuance, key store, and revocation surface — there is no global Tenzro Labs key authority. Three key classes encode different sovereignty contracts between the operator and the key holder.
STATUS
Testnet
CRATE
tenzro-node
STABILITY
Stable
REFERENCE
api-keys
01

Per-operator sovereignty

Every Tenzro node operator that hosts mediated upstreams (e.g. Canton) holds their own admin token on their own node. That admin token is the root credential for minting and revoking API keys against that one node. Tenzro Labs is just one operator instance among many — the same three-class model applies whether you are calling rpc.tenzro.network or a self-hosted validator a community operator stood up.

Network-wide concerns (validator set, treasury, fee schedule, system contracts) are not in scope for the admin token. Those mutate only through on-chain governance.

02

Key classes

Subject— bound to a Tenzro DID (typically did:tenzro:human:<uuid>). The holder of the plaintext tnz_... can use the key and can revoke it themselves via tenzro_revokeMyApiKey. The operator can also revoke. This is the default class for end-user developer keys.

OperatorInternal— an operator-only credential used inside the operator’s own automation (e.g. cron jobs, sidecar services). No subject binding; no self-service revoke surface. Only the operator can list or revoke via the admin-gated methods.

OperatorProtected— a long-lived credential intended for operator infrastructure that shouldnot be revocable over RPC at all. Rotation is by updating the operator secret and restarting the node. Minting requires an explicit interlock (confirm_operator_protected: true) so the class can never be issued by accident.

03

Two headers, two gates

X-Tenzro-Api-Key presents a tnz_<base64url> token (any class). The node hashes it with SHA-256 and looks it up in CF_API_KEYS; scope-gated methods (currently the canton scope, matching tenzro_*Canton* and tenzro_*Daml*) accept the call if the key carries the matching scope.

X-Tenzro-Admin-Tokenpresents the operator’s admin token. It gates the issuance + admin-lifecycle methods. The admin token never leaves the node it was issued on and is not a network credential.

The two headers are independent and may both be present on a single request; admin-gated methods that also touch a scope-gated upstream will check both.

04

Methods

# Admin-gated (require X-Tenzro-Admin-Token)
tenzro_createApiKey          Mint a new key, returns plaintext exactly once
tenzro_listApiKeys           List all keys this operator has issued
tenzro_revokeApiKey          Revoke a key by id (any class except OperatorProtected)

# Subject-gated (require X-Tenzro-Api-Key, returns only that subject's keys)
tenzro_listMyApiKeys         List keys bound to the calling subject
tenzro_revokeMyApiKey        Revoke a Subject-class key the caller holds

MCP exposes the same five as tools: create_api_key, list_api_keys, revoke_api_key, list_my_api_keys, revoke_my_api_key. The MCP transport carries whichever of the two headers the caller provides and gates the tool dispatch the same way the JSON-RPC server does.

05

Issuance example

Operator mints a Subject-class Canton key for a developer identified by a Tenzro DID:

curl -X POST https://rpc.tenzro.network \
  -H "content-type: application/json" \
  -H "X-Tenzro-Admin-Token: $TENZRO_ADMIN_TOKEN" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tenzro_createApiKey",
    "params": {
      "label": "canton-prod-team",
      "subject": "did:tenzro:human:6e7b...",
      "scopes": ["canton"],
      "class": "subject",
      "canton_user_id": "<your-team-id>"
    }
  }'

# Response includes "key": "tnz_..." shown EXACTLY ONCE,
# plus "key_id" + "canton_user_id" for analytics + actAs forwarding.

The developer exports the returned token as TENZRO_API_KEY in their own environment and presents it on Canton calls. canton_user_id binds the key to a Canton User Management Service user id — the node resolves the user’s primaryParty and uses it as actAs on every Canton submit. Required for tenzro_submitDamlCommand and the wrapped canton_submit_commandpath: a Canton-scoped key without the binding is rejected up front, since the operator’s default party is never inherited. Per-tenant counters are keyed off the same binding. See Canton for the multi-tenant isolation model.

06

Subject self-revoke

A Subject-class key holder controls their own revocation without operator involvement — mirroring the property that a wallet owner can rotate their own credentials:

curl -X POST https://rpc.tenzro.network \
  -H "content-type: application/json" \
  -H "X-Tenzro-Api-Key: $TENZRO_API_KEY" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tenzro_revokeMyApiKey",
    "params": {"key_id": "ak_..."}
  }'

tenzro_listMyApiKeysreturns only keys whose subject matches the calling key’s subject. There is no cross-subject discovery surface — an ownership probe against a key the caller does not hold collapses to -32004.

07

Error codes

-32001   Admin gate failed (missing / wrong X-Tenzro-Admin-Token)
-32004   Subject gate failed, unknown key, revoked key, or ownership-probe collapse
-32602   Invalid params (e.g. operator_protected without confirm_operator_protected)
08

SDK lifecycle (Rust)

End-to-end operator → subject demo, mirroring the example in sdk/tenzro-sdk/examples/canton_api_key_lifecycle.rs:

use tenzro_sdk::{TenzroClient, config::SdkConfig};
use tenzro_sdk::api_key::{CreateApiKeyParams, KeyClass};
use std::env;

// Operator mints
let operator = TenzroClient::connect(SdkConfig::testnet()).await?;
let created = operator.api_key().create(CreateApiKeyParams {
    label: "canton-prod-alice".into(),
    subject: Some("did:tenzro:human:6e7b...".into()),
    scopes: vec!["canton".into()],
    class: KeyClass::Subject,
}).await?;
println!("plaintext: {}", created.key); // shown once

// Subject uses + self-revokes
// SAFETY: single-threaded example (Rust 2024)
unsafe {
    env::set_var("TENZRO_API_KEY", &created.key);
    env::remove_var("TENZRO_ADMIN_TOKEN");
}
let subject = TenzroClient::connect(SdkConfig::testnet()).await?;
let domains = subject.canton().list_domains().await?;
let mine = subject.api_key().list_mine().await?;
let revoked = subject.api_key().revoke_mine(&created.key_id).await?;
09

SDK lifecycle (TypeScript)

Same flow against the TS SDK, from sdk/tenzro-ts-sdk/examples/canton-api-key-lifecycle.ts:

import { TenzroClient } from "@tenzro/sdk";

const operator = TenzroClient.testnet();
const created = await operator.apiKey.create({
  label: "canton-prod-alice",
  subject: "did:tenzro:human:6e7b...",
  scopes: ["canton"],
  class: "subject",
});

process.env.TENZRO_API_KEY = created.key;
delete process.env.TENZRO_ADMIN_TOKEN;
const subject = TenzroClient.testnet();

const domains = await subject.rpc.call("tenzro_listCantonDomains", {});
const mine = await subject.apiKey.listMine();
const revoked = await subject.apiKey.revokeMine(created.key_id);
10

CLI lifecycle

# Operator
export TENZRO_ADMIN_TOKEN=...
tenzro auth create-key \
  --label canton-prod-alice \
  --subject did:tenzro:human:6e7b... \
  --scope canton \
  --class subject

tenzro auth list-keys
tenzro auth revoke-key --key-id ak_...

# Subject
export TENZRO_API_KEY=tnz_...
tenzro auth list-my-keys
tenzro auth revoke-my-key --key-id ak_...
Related
← All docs