API keys.
- STATUS
- Testnet
- CRATE
- tenzro-node
- STABILITY
- Stable
- REFERENCE
- api-keys
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.xyz 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.
What keys do not gate
A key gates operator-brokered resources — the ones where the operator holds an upstream credential and pays an upstream bill on the caller’s behalf. Canton is the archetype: the operator holds the participant-node credential and proxies ledger calls. Third-party chain RPC is the same shape.
The marketplace registries are permissionless. Registering an agent, an agent template, a skill, a tool or MCP server, a workflow template, or a knowledge source needs no API key and no operator approval — tenzro_registerAgent, tenzro_registerAgentTemplate, tenzro_registerSkill, tenzro_registerTool, tenzro_registerWorkflowTemplate and tenzro_registerKnowledge are open to anyone with a Tenzro DID. Serving a model via tenzro_serveModel is likewise open.
The listing declares its own price: free, or a TNZO amount that settles to the provider’s wallet on use, minus the protocol commission. No operator can refuse a listing, delist someone else’s resource, or gate discovery.
The one adjacent method that is admin-gated is tenzro_registerProvider, because it enrols the node itselfas a provider — a decision that belongs to whoever runs the node, not to a remote caller. Refusal at invocationtime is a different thing and stays available: a consumer’s delegation scope, spending policy, or approval policy can refuse a call their own controller did not authorize. That is consumer-side control, not registry admission.
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.
Tiers
A scope decides which surfaces a key reaches. A tier decides how much and whether it may write.
tier requests/minute mutating methods
free 60 refused
standard 600 allowed
priority 6,000 allowedfree is the default when tieris omitted at issuance, so a key never silently acquires write access. Each key gets its own sliding 60-second window — a noisy key exhausts its own budget, not a shared one.
The mutating methods the free tier refuses are the ones that change state on an operator-brokered upstream: the Canton ledger writes (command submission, party allocation, DAR upload, user rights, IDP create and delete, party watch, event consumption, receipt and workflow and obligation mirroring), tenzro_sponsorBridgeFee, and the three stable-asset methods (tenzro_registerStableAsset, tenzro_mintStableAsset, tenzro_redeemStableAsset). Reads are available at every tier.
A free key calling a mutating method gets -32004. Any key over its budget gets -32005, whose data carries retry_after_ms, requests_per_minute and tier, so a client can back off without parsing the message. JSON-RPC has no HTTP status, so 429 semantics live in the error code.
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 (e.g. the canton scope, matching tenzro_*Canton* and tenzro_*Daml*, or the issuer scope, matching the tenzro_*StableAsset issuance methods) 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.
Canton networks
A Canton network is a distinct ledger— distinct parties, distinct contracts, distinct assets. A key names the ones it reaches at issuance, in canton_networks: an array of devnet and/or mainnet. Fail-closed — omitted or empty means the key reaches no Canton network at all. Naming a network the node does not serve is refused at issuance with -32602.
On the JSON-RPC surface the target travels as a canton_network param. Resolution order: an explicit param wins; otherwise, if the key authorizes exactly one network, that one is used; otherwise the call is refused with -32602 naming the authorized set. An explicit param outside what the key authorizes is -32004.
curl -X POST https://rpc.tenzro.xyz \
-H "content-type: application/json" \
-H "X-Tenzro-Api-Key: $TENZRO_API_KEY" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tenzro_listDamlContracts",
"params": {"canton_network": "devnet"}
}'On the Canton MCP server the target travels as an X-Canton-Network header, because the gate sits above MCP’s JSON-RPC envelope and does not read tool arguments. Same resolution rules; unknown network is 400, unauthorized is 403. The other ecosystem MCP servers are open and take neither header.
An admin-token request skips the api-key gate entirely, so it is not bounded by any key’s canton_networks— but it still has to name the network it means, because there is no key to infer one from. The CLI surfaces this as --canton-network on both paths, and every SDK and Python client reads TENZRO_CANTON_NETWORK from the environment. An explicit value always wins over the environment.
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 holdsMCP 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.
Issuance example
Operator mints a Subject-class Canton key for a developer identified by a Tenzro DID:
curl -X POST https://rpc.tenzro.xyz \
-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.
Subject self-service
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.xyz \
-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.
It is also the entitlement self-read. Each row carries everything the node will enforce against the key, so a developer can determine what the key may do without asking the operator:
{
"keys": [
{
"key_id": "a1b2c3d4e5f60718",
"subject": "did:tenzro:machine:acme",
"scopes": ["canton"],
"class": "subject",
"tier": "standard",
"requests_per_minute": 600,
"allows_write": true,
"canton_networks": ["mainnet"],
"canton_user_id": "acme@clients",
"canton_identity_provider_id": null,
"can_act_as_parties": [],
"can_read_as_parties": [],
"allowed_templates": [],
"allowed_commands": [],
"active": true
}
],
"subject": "did:tenzro:machine:acme"
}A non-empty canton_networks alongside a null canton_user_id is node access without ledger access. The key authenticates and reaches the node, but it is bound to no Canton party, so command submission is refused. Two ways forward: the operator reissues the key with a canton_user_id, after which the node mints the tenant JWT server-side, or the caller presents a JWT from its own issuer in the X-Canton-Auth header.
The OAuth client material behind a per-tenant binding is never returned by this method.
Error codes
-32001 Admin gate failed (missing / wrong X-Tenzro-Admin-Token)
-32004 Subject gate failed, unknown key, revoked key, or ownership-probe collapse
Tier is read-only and the method mutates
Key authorizes no Canton network
Key does not authorize the named Canton network
-32005 Rate limit exceeded. data carries retry_after_ms,
requests_per_minute, tier
-32602 operator_protected without confirm_operator_protected
Unknown tier
Unknown Canton network, or one this node does not serve
Multi-network key called a Canton method without canton_network
Canton provisioning asked for on a key authorizing != 1 networkSDK lifecycle (Rust)
Full 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?;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);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_...