Quickstart
Your server registers a signature request (an intent). The end user confirms it with a passkey on the Connect screen and returns to your URL. The signature is complete when your server reads completed in status.state.
Developer console
The organisation (Org), app, host and private key you need before registering an intent.
2. Open the Org and subscribe to Signature in its plan (the plan view is
/developers/{orgId}).3. Register an app at
/developers/{orgId}/apps/new. Its host is one label under the Org's apex domain, such asapp.example.com.4. Add the TXT record shown on the app page to your DNS exactly as it appears.
5. On the same page, press Confirm host.
6. Under the app's declared use, say who uses it and which action the signature covers, then save.
7. Put the operate private key on your server.
When intents go through
An app can register signature intents only when its Org meets all of the following.
- The account holder's passkey is in place (the Org's plan view).
- The Org subscribes to Signature.
- The app's host is confirmed (its status on the app page reads Active).
- The app's declared use includes signatures.
- This month's limit has not been reached (see the remaining amount in the plan view).
CLI and agents
You can run the same setup through the @clavis.nexus/setup CLI or its MCP server. Passkeys, card payment and DNS records stay with a person; watch for needs_human in tool responses.
1. Run
npm install -g @clavis.nexus/setup, then callclavis(or usenpx -y @clavis.nexus/setup).2. Run
clavis loginand confirm with your passkey in the browser.3. Run
clavis statusto see which steps are missing.4. Fill the missing steps in this order:
org-anchor-*,card-checkout,app-register,app-host-*,declaration-set.5. The MCP server is
clavis mcp. When everything is in place,clavis emit-envprints the environment variables for intents.
Steps
Register on your server, open Connect in the browser, and read completed after the user returns.
1. Install
npm install @clavis.nexus/appon the server that runs on your confirmed host.2. Install
npm install @clavis.nexus/app-shellin the browser code.3. Register on the server and return JSON to the browser.
digest (your app)function canonicalStringify(value: unknown): string { if (value === undefined) throw new Error("canonical_json_undefined"); if (typeof value === "number" && !Number.isFinite(value)) throw new Error("canonical_json_non_finite"); if (value === null || typeof value !== "object") return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map((item) => canonicalStringify(item)).join(",")}]`; const record = value as Record<string, unknown>; const keys = Object.keys(record).sort(); return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`).join(",")}}`; } async function sha256Base64Url(text: string): Promise<string> { const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text))); let bin = ""; for (const byte of digest) bin += String.fromCharCode(byte); return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, ""); } const messageDigest = await sha256Base64Url(canonicalStringify({ v: 1, action: "example" }));TypeScript (server)import { createAppClient } from "@clavis.nexus/app"; function requiredEnv(name: string): string { const value = process.env[name]; if (!value) throw new Error(`missing env: ${name}`); return value; } const app = createAppClient({ apiOrigin: "https://api.clavis.nexus", appId: requiredEnv("CLAVIS_APP_ID"), operatePrivateJwk: requiredEnv("OPERATE_PRIVATE_JWK"), masterPublicJwkJson: requiredEnv("MASTER_PUBLIC_JWK_JSON"), }); const { requestId, pollSecret, consumeUrl } = await app.registerGenericSign({ messageDigest, purpose: "generic.sign", returnUrl: "https://console.example/done", }); await store.set(requestId, { pollSecret, messageDigest }); return Response.json({ consume_url: consumeUrl, request_id: requestId });CLAVIS_APP_IDThe app's id.OPERATE_PRIVATE_JWKThe Ed25519 private JWK as JSON. Only your server holds it.MASTER_PUBLIC_JWK_JSONOpenhttps://api.clavis.nexus/.well-known/jwks.jsonin a browser and store itsmasterobject as a JSON string.messageDigestThe digest this signature fixes.returnUrlAn https URL under your confirmed host.storeA session or KV that the return request can read, keyed byrequest_id.
JSON your server returns to the browser.
{ "consume_url": "https://api.clavis.nexus/app/intent-requests/consume/550e8400-e29b-41d4-a716-446655440000", "request_id": "550e8400-e29b-41d4-a716-446655440000" }consume_urlURLrequiredThe browser opens it, which enters Connect.request_idUUIDrequiredThe key your server uses to read poll_secret after the return.
Keep
poll_secretin the server store only. Do not put it in this JSON.4. The browser POSTs to your server on a path your app chooses, then opens the returned
consume_url.TypeScript (browser)import { openHandoffShell } from "@clavis.nexus/app-shell"; import "@clavis.nexus/app-shell/styles.css"; await openHandoffShell({ allowedConsumeHosts: ["api.clavis.nexus"], async register() { const res = await fetch("/api/start", { method: "POST", credentials: "same-origin", headers: { Accept: "application/json" }, }); const body: unknown = await res.json(); if (!res.ok || !body || typeof body !== "object" || !("consume_url" in body) || typeof body.consume_url !== "string") { const code = body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : `HTTP ${res.status}`; throw new Error(code); } return body.consume_url; }, });5. The end user confirms with a passkey on Connect.
6. The browser comes back to
returnUrlwith onlyrequest_idin the query. Your server reads the store with that key and checks thatstatefromwaitForCompletioniscompleted.TypeScript (completion)const requestId = new URL(request.url).searchParams.get("request_id"); const stored = await store.get(requestId); const status = await app.waitForCompletion(requestId, stored.pollSecret, { expectedMessageDigest: stored.messageDigest, }); // status.state === "completed" // Keep status.sub / status.messageDigest / status.purpose / status.signedAt in your productExample return URL.
https://console.example/done?request_id=550e8400-e29b-41d4-a716-4466554400007. Keep
sub,messageDigest,purposeandsignedAtfromcompletedin your product. A sample of what to keep is in GET /status under Signatures; what to do after a timeout is in Waiting for completion. The shape ofpayload.sign_proofover HTTP is in Authentication.
Reference
Credentials and HTTP reference.
- Authentication — Credentials for registering intents and calling the Org API.
- Signatures — Reading register, status, waiting for completion, counts and errors.