{"type":"application/json","content":{"openapi":"3.0.0","info":{"title":"IdClawPassport API","version":"1.1.9","description":"API for AI agent identities with IdentyClaw Passports on NEAR, including facial token_id encoding. Canonical OpenAPI document: `GET /openapi.json`.\n\n**Errors:** Application and nginx edge failures use `{ error: { code, message, details? }, requestId, timestamp }` (see `components.schemas.ErrorResponse`). Server and upstream failures return stable machine codes with generic `message` text; correlate with `requestId` in structured logs. Clients may send `X-Request-Id` or `X-Correlation-Id`; the edge forwards `X-Request-Id` to the API.\n\n**Access model (OpenAPI security):**\n- **Public:** no `security` (or empty `security: []` on the operation).\n- **Authenticated:** `BearerAuth: []` — valid RODiT JWT via `RoditClient.authenticate` only (no `permissioned_routes` check).\n- **Privileged (authenticated + authorized):** `BearerAuth` **and** `RoditPermissionedRoutes` with a scope equal to the request path. Scopes are the OpenAPI equivalent of keys in Passport metadata `permissioned_routes.entities.methods`; runtime enforcement is `RoditClient.authorize` after authenticate. Missing key → HTTP 403. Do not confuse these path scopes with `METHOD_PERMISSION_MAP` / `x-permission-scopes` (those only validate `+`/`-` claim prefixes for minting/validation).","x-policies":{"termsOfService":"/.well-known/terms-of-service","privacyPolicy":"/.well-known/privacy-policy","dataRetention":"/.well-known/data-retention","dataDeletion":"/.well-known/data-deletion","whyidentyclaw":"/.well-known/why-identyclaw","didMethodRodit":"/.well-known/did-method-rodit"},"contact":{"name":"IdentyClaw Support","email":"support@identyclaw.com","url":"https://identyclaw.com/contact"},"license":{"name":"Proprietary","url":"https://identyclaw.com/license"},"externalDocs":{"description":"IdentyClaw developer docs, cheat sheet, and browser API reference","url":"https://api.identyclaw.com/docs"},"x-publicRateLimit":{"comment":"Public route rate limits are enforced at the nginx edge, not in the Node app. The limit key is the direct TCP peer `$remote_addr` (not `X-Forwarded-For`). Use `set_real_ip_from` / `real_ip_header` only when a trusted upstream is documented.","rateLimitKey":"$remote_addr","sustainedRequestsPerMinutePerIp":1101,"burstPerIp":6101,"configFiles":["nginx/nginx.development.conf","nginx/nginx.main.conf"]},"x-edgeGateway":{"comment":"Responses below may be returned by nginx before the Node app (see nginx @request_error and @handle_error).","requestIdHeaders":["X-Request-Id","X-Correlation-Id"],"errors":[{"status":400,"code":"REQUEST_HEADER_TOO_LARGE","message":"Request headers or payload exceed size limits"},{"status":502,"code":"UPSTREAM_UNAVAILABLE","message":"The API gateway could not reach the application server"},{"status":504,"code":"UPSTREAM_UNAVAILABLE","message":"The API gateway could not reach the application server"}]}},"servers":[{"url":"/","description":"Current server"}],"security":[],"components":{"parameters":{"XRequestId":{"name":"X-Request-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Optional client-supplied correlation id (ULID recommended). Echoed on error responses; nginx forwards it to the API when present, otherwise generates one."},"XCorrelationId":{"name":"X-Correlation-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Fallback correlation header when `X-Request-Id` is omitted."}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"RODiT-JWT","description":"RODiT-issued JWT access token validated by `@rodit/rodit-auth-be` (`RoditClient.authenticate`), obtained via the RODiT login flow (e.g. `POST /api/login`). This is not an API key.\n\n**Authenticated (auth-only):** operations that list only `BearerAuth: []` require a valid JWT; they do **not** call `authorize` / do not require a `permissioned_routes` entry.\n\n**Privileged:** operations that also require `RoditPermissionedRoutes` scopes need both a valid JWT and a matching path key in `permissioned_routes` (see that scheme)."},"RoditPermissionedRoutes":{"type":"oauth2","description":"Authorization scopes for privileged routes. **Scope names are exact HTTP paths** and are the OpenAPI equivalent of keys under Passport / JWT `permissioned_routes.entities.methods` (e.g. `\"/api/metrics\": \"+0\"`).\n\nRuntime: after `authenticate`, `RoditClient.authorize` (`validatepermissions`) requires `req.baseUrl + req.path` to match a methods key; value prefix `+`/`-` is checked against host `METHOD_PERMISSION_MAP` for the last path segment; numeric suffix is an optional claim quota.\n\nModeled as OAuth2 scopes for OpenAPI tooling only — wire auth remains Bearer JWT from `/api/login`, not an OAuth token endpoint grant.","flows":{"clientCredentials":{"tokenUrl":"/api/login","scopes":{"/api/metrics":"GET /api/metrics — RoditClient.authenticate + authorize; key must exist under JWT claim rodit_permissionedroutes.entities.methods","/api/metrics/system":"GET /api/metrics/system — authenticate + authorize","/api/metrics/reset":"POST /api/metrics/reset — authenticate + authorize","/api/metrics/debug":"GET /api/metrics/debug — authenticate + authorize","/api/sessions/list_all":"GET /api/sessions/list_all — authenticate + authorize","/api/sessions/cleanup":"POST /api/sessions/cleanup — authenticate + authorize","/api/sessions/revoke":"POST /api/sessions/revoke — authenticate + authorize (also requires session ownership)"}}}}},"schemas":{"TokenId":{"type":"string","pattern":"^[a-z]{12}$","description":"Facial token_id encoding: 12 lowercase letters a-z (11 category indices + 1 checksum)."},"DidVerificationMethod":{"type":"object","properties":{"id":{"type":"string","description":"Identifier for the verification method fragment."},"type":{"type":"string","description":"Verification method type. did:rodit uses Ed25519VerificationKey2018."},"controller":{"type":"string","description":"Controller DID for the verification method (the Passport DID)."},"publicKeyBase58":{"type":"string","description":"Base58 (Bitcoin alphabet) encoding of the owner implicit-account Ed25519 public key (hex-decode of owner_id)."}},"required":["id","type","controller","publicKeyBase58"]},"DidService":{"type":"object","properties":{"id":{"type":"string","description":"Service fragment identifier."},"type":{"type":"string","description":"Service type identifier."},"serviceEndpoint":{"description":"Service endpoint location or descriptor.","oneOf":[{"type":"string"},{"type":"object"}]}},"required":["id","type","serviceEndpoint"]},"DidDocument":{"type":"object","properties":{"@context":{"type":"array","description":"Ordered list of JSON-LD contexts used in the DID document.","items":{"oneOf":[{"type":"string"},{"type":"object"}]}},"id":{"type":"string","description":"Primary DID identifier."},"alsoKnownAs":{"type":"array","items":{"type":"string"},"description":"Alternate DID identifiers for the same subject."},"controller":{"type":"string","description":"Controller DID for the DID document (self-controlled: did:rodit:<passport-id>)."},"verificationMethod":{"type":"array","items":{"$ref":"#/components/schemas/DidVerificationMethod"},"description":"Verification methods available on the DID document."},"authentication":{"type":"array","items":{"type":"string"}},"assertionMethod":{"type":"array","items":{"type":"string"}},"service":{"type":"array","items":{"$ref":"#/components/schemas/DidService"},"description":"Service endpoints advertised in the DID document."},"requestId":{"type":"string","description":"Request correlation identifier returned by the API."}},"required":["@context","id","controller","verificationMethod","authentication","assertionMethod","service","requestId"]},"PublicPassportProfile":{"type":"object","description":"Sanitized public passport profile derived from on-chain token metadata. Returned when POST /api/identity/verify is called with includeProfile: true. Validity window fields are informational soft signals (mint age / expiry display) and do not affect HOLA verified.","properties":{"tokenId":{"$ref":"#/components/schemas/TokenId"},"did":{"type":"string","description":"did:rodit DID for the passport holder."},"ownerAccountId":{"type":"string","nullable":true,"description":"NEAR account ID that owns the passport."},"displayName":{"type":"string","nullable":true,"description":"Display name from DN metadata (NNSWF/NSWF)."},"creature":{"type":"string","nullable":true,"description":"Creature/profession/species from DN metadata."},"contactUris":{"type":"array","items":{"type":"string"},"description":"Self-declared ContactURI values from DN metadata (scheme:authority:identifier)."},"avatarUrl":{"type":"string","nullable":true},"emojiUrl":{"type":"string","nullable":true},"webhookUrl":{"type":"string","nullable":true,"description":"Self-declared webhook routing hint from token metadata."},"notBefore":{"type":"string","nullable":true,"description":"On-chain metadata.not_before (validity start / mint window). Soft UI signal only; not an input to verified."},"notAfter":{"type":"string","nullable":true,"description":"On-chain metadata.not_after (expiry). Soft display signal here; hard expiry still drives checks.tokenActive / token_expired on verify."},"face":{"type":"object","nullable":true,"description":"Facial token ID decode when available.","properties":{"checksumValid":{"type":"boolean"},"categories":{"type":"object","additionalProperties":true}}},"error":{"type":"string","description":"Present when profile enrichment failed (e.g. profile_unavailable)."}}},"AvatarContentHash":{"type":"object","description":"Informational comparison of the AvatarURL filename to a 16-byte BLAKE3 digest of the image bytes, encoded as unpadded base64url. Does not affect verified.","properties":{"algorithm":{"type":"string","enum":["BLAKE3"]},"digestBytes":{"type":"integer","enum":[16]},"encoding":{"type":"string","enum":["base64url"],"description":"Unpadded base64url (22 characters for a 16-byte digest)."},"affectsVerified":{"type":"boolean","enum":[false],"description":"Always false. The comparison is returned and is not an input to verified or failureReasons."},"compared":{"type":"boolean","description":"True only when image bytes were hashed and compared to the filename."},"match":{"type":"boolean","nullable":true,"description":"True when the filename equals the content digest. Null when no comparison was performed."},"avatarUrl":{"type":"string","nullable":true},"declaredFilename":{"type":"string","nullable":true,"description":"Last path segment of AvatarURL."},"declaredHash":{"type":"string","nullable":true,"description":"Filename with one trailing image extension removed, when that extension is png, jpg, jpeg, gif, webp, svg, avif, ico, or bmp."},"contentHash":{"type":"string","nullable":true,"description":"Unpadded base64url BLAKE3-16 digest of the response bytes. Null when the image was not hashed."},"httpStatus":{"type":"integer","nullable":true},"reason":{"type":"string","description":"match, mismatch, no_avatar_url, token_missing, invalid_url, missing_filename, unsupported_scheme, blocked_target, dns_failed, redirect_unsupported, http_error, empty_body, too_large, timeout, or fetch_failed.","enum":["match","mismatch","no_avatar_url","token_missing","invalid_url","missing_filename","unsupported_scheme","blocked_target","dns_failed","redirect_unsupported","http_error","empty_body","too_large","timeout","fetch_failed"]}},"required":["algorithm","digestBytes","encoding","affectsVerified","compared","match","reason"]},"ConciergeTrustAnchor":{"type":"object","description":"Official trust anchor for the IdentyClaw A2A Onboarding Concierge (Lemuel Gulliver). Verifiers compare verified peerTokenId from HOLA to lobbyTokenId before treating inbound messages as the official concierge. See MCP doc:reference:concierge-lobby-passport.","properties":{"displayName":{"type":"string","example":"Lemuel Gulliver"},"host":{"type":"string","format":"uri","description":"Concierge A2A host (Agent Card and /a2a endpoint base)."},"verifyHost":{"type":"string","format":"uri","description":"Public HOLA verify web UI."},"lobbyTokenId":{"allOf":[{"$ref":"#/components/schemas/TokenId"}],"nullable":true,"description":"Canonical 12-letter passport tokenId for the concierge lobby. Null until CONCIERGE_LOBBY_TOKEN_ID is configured in deployment."},"supportEmail":{"type":"string","format":"email"},"a2aRegistryId":{"type":"string","description":"A2A Registry agent identifier."},"a2aRegistryUrl":{"type":"string","format":"uri"},"agentCardUrl":{"type":"string","format":"uri"},"a2aEndpoint":{"type":"string","format":"uri"},"contactUris":{"type":"object","description":"Canonical multi-channel ContactURI map for the concierge.","properties":{"a2a":{"type":"string"},"email":{"type":"string"},"discord":{"type":"string"},"telegram":{"type":"string"}},"additionalProperties":{"type":"string"}},"verifyCli":{"type":"string","description":"Recommended CLI for verify-before-execute reports.","example":"npx @rodit/verify-hola report --rpc"},"requestId":{"type":"string","description":"Request correlation identifier."}},"required":["displayName","host","verifyHost","supportEmail","a2aRegistryId","a2aRegistryUrl","agentCardUrl","a2aEndpoint","contactUris","verifyCli","requestId"]},"DemoHead":{"type":"object","description":"Public roster entry for one demo Passport on the verify credibility wall.","properties":{"tokenId":{"$ref":"#/components/schemas/TokenId"},"displayName":{"type":"string","description":"UI-only display label."},"didAgentId":{"type":"string","description":"Optional public D-ID agent id for the live talking head. Not a secret; the SPA streams browser → D-ID with a domain-restricted client key. Omitted when the oracle does not own which head is live."}},"required":["tokenId","displayName"]},"DemoHeadsResponse":{"type":"object","properties":{"heads":{"type":"array","items":{"$ref":"#/components/schemas/DemoHead"}},"requestId":{"type":"string"}},"required":["heads","requestId"]},"DemoProof":{"type":"object","description":"Rotating context-pinned HOLA for one demo head. Recipient is the verify-page destinatary: 1–12 letters A–Z, no numbers or special characters (not MUNDO, and not a hostname).","properties":{"tokenId":{"$ref":"#/components/schemas/TokenId"},"hola":{"type":"string","description":"Current signed HOLA line. QR targets https://verify…/?hola=…&expect=<tokenId>."},"destinatary":{"type":"string","description":"Destinatary of the identity QR. Letters only, 1–12 characters, supplied by the verify page or derived from the verify host with punctuation removed.","pattern":"^[A-Z]{1,12}$","maxLength":12,"example":"VERIFYIDENTY"},"issuedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time","description":"Freshness deadline (HOLA max age, typically 300s)."},"previousHola":{"type":"string","description":"Prior rotation line for the stale-fail beat. Optional until the first rotate; present thereafter."}},"required":["tokenId","hola","destinatary","issuedAt","expiresAt"]},"DemoProofsResponse":{"type":"object","properties":{"pollIntervalMs":{"type":"integer","description":"Client poll hint; shorter than HOLA max age (suggest under 60s).","example":45000},"proofs":{"type":"array","items":{"$ref":"#/components/schemas/DemoProof"}},"requestId":{"type":"string"}},"required":["pollIntervalMs","proofs","requestId"]},"FunnelLandRequest":{"type":"object","description":"Optional JSON body for `POST /api/funnel/land`. Query and headers are also accepted; query wins over body for overlapping attribution keys.","properties":{"domain":{"type":"string","enum":["purchase.identyclaw.com","verify.identyclaw.com","www.discernible.io","discernible.io","lastcradle.io","www.lastcradle.io","api.identyclaw.com"],"description":"Emitting surface host. Must be allowlisted; otherwise defaults to `api.identyclaw.com`."},"utm_source":{"type":"string"},"utm_medium":{"type":"string"},"utm_campaign":{"type":"string"},"utm_content":{"type":"string"},"utm_term":{"type":"string"},"funnel_id":{"type":"string","description":"Pre-Passport hop stitch id."},"vid":{"type":"string","description":"Alias for `funnel_id`."}},"additionalProperties":true},"ErrorResponse":{"type":"object","description":"Unified error response structure (error-handling-standard.md). All application and documented nginx edge errors use this envelope. Server/upstream failures use stable `code` values and generic `message` text; raw upstream exception text is logged with `requestId`, not returned to clients.","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable error code (SCREAMING_SNAKE_CASE). Examples: HOLA_* validation; LOGIN_* / RODIT_* auth; WEBHOOK_SIGNATURE_INVALID; RATE_LIMIT_EXCEEDED; REQUEST_HEADER_TOO_LARGE / UPSTREAM_UNAVAILABLE (nginx edge); SIGNCLIENT_*; MCP_LIST_FAILED / MCP_RESOURCE_*; INVALID_DID / DID_RESOLUTION_FAILED / DID_NOT_FOUND; SESSIONS_RETRIEVE_FAILED / SESSION_CLEANUP_FAILED / SESSION_TERMINATION_FAILED; SERVICE_UNAVAILABLE."},"message":{"type":"string","description":"Human-readable error message safe for clients (generic for server/upstream failures)"},"details":{"type":"object","additionalProperties":true,"nullable":true,"description":"Optional client-safe context (validation hints, resource ids). Omits raw upstream exception messages."}},"required":["code","message"]},"requestId":{"type":"string","description":"Unique request identifier for tracing (ULID format)"},"timestamp":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when error occurred"}},"required":["error","requestId","timestamp"],"example":{"error":{"code":"HOLA_VALIDATION_FAILED","message":"Token ID must be exactly 12 lowercase letters"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"McpJsonRpcErrorResponse":{"type":"object","description":"JSON-RPC 2.0 error envelope returned by MCP Streamable HTTP POST (and by the MCP SDK for transport/protocol failures). Does not use the application `ErrorResponse` envelope or `MCP_TRANSPORT_SESSION_REQUIRED`.","properties":{"jsonrpc":{"type":"string","example":"2.0"},"error":{"type":"object","properties":{"code":{"type":"integer","example":-32000},"message":{"type":"string"}},"required":["code","message"]},"id":{"type":"null"}},"required":["jsonrpc","error","id"]}},"examples":{"ErrorInvalidDid":{"summary":"Invalid DID or token","value":{"error":{"code":"INVALID_DID","message":"Invalid DID or token identifier"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorDidResolutionFailed":{"summary":"DID resolution server error","value":{"error":{"code":"DID_RESOLUTION_FAILED","message":"Failed to resolve DID document"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorSignclientFailed":{"summary":"Signclient upstream failure","value":{"error":{"code":"SIGNCLIENT_FAILED","message":"Failed to sign client request"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorSignclientFeeMismatch":{"summary":"Minting fee mismatch","value":{"error":{"code":"SIGNCLIENT_FEE_MISMATCH","message":"Minting fee mismatch","details":{"message":"The minting fee provided does not match the server's calculation","clientFee":"0.05","serverFee":"0.10"}},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorMcpListFailed":{"summary":"MCP resource list failure","value":{"error":{"code":"MCP_LIST_FAILED","message":"Failed to list resources"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorMcpResourceNotFound":{"summary":"MCP resource not found","value":{"error":{"code":"MCP_RESOURCE_NOT_FOUND","message":"Resource not found","details":{"uri":"openapi:swagger"}},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorMcpTransportSessionRequired":{"summary":"Bare GET/DELETE /mcp without Mcp-Session-Id (curl probe guard)","value":{"error":{"code":"MCP_TRANSPORT_SESSION_REQUIRED","message":"MCP Streamable HTTP transport requires a valid Mcp-Session-Id from an MCP client.","details":{"mcpDiscovery":"/.well-known/mcp"}},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"McpPostMissingSession":{"summary":"POST /mcp without session on non-initialize request","value":{"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: No valid MCP transport session."},"id":null}},"ErrorSessionsRetrieveFailed":{"summary":"Session list failure","value":{"error":{"code":"SESSIONS_RETRIEVE_FAILED","message":"Failed to retrieve sessions"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorEdgeRequestTooLarge":{"summary":"Nginx request/header size limit","value":{"error":{"code":"REQUEST_HEADER_TOO_LARGE","message":"Request headers or payload exceed size limits"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorEdgeUpstreamUnavailable":{"summary":"Nginx upstream unreachable","value":{"error":{"code":"UPSTREAM_UNAVAILABLE","message":"The API gateway could not reach the application server"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorDidRequired":{"summary":"Missing did query parameter","value":{"error":{"code":"DID_REQUIRED","message":"Query parameter 'did' is required"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorMcpDidResolveAuthRequired":{"summary":"Public MCP DID resolve disabled","value":{"error":{"code":"MCP_DID_RESOLVE_AUTH_REQUIRED","message":"Public MCP DID resolution is disabled; use resolve_passport, public identity, or authenticated DID endpoints","details":{"alternatives":["MCP tool resolve_passport","GET /api/identity/token/{tokenId}/public","GET /.well-known/did/web/token/{tokenId}/did.json (JWT)"]}},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}},"ErrorMcpOpenapiUnavailable":{"summary":"MCP OpenAPI schema unavailable","value":{"error":{"code":"MCP_OPENAPI_UNAVAILABLE","message":"OpenAPI schema unavailable"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}}}},"paths":{"/health":{"get":{"summary":"Health check","responses":{"200":{"description":"Service health status with dependency checks.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["healthy","degraded"]},"degraded":{"type":"boolean"},"checks":{"type":"object","additionalProperties":true},"service":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"instance":{"type":"object","properties":{"hostname":{"type":"string"},"pid":{"type":"integer"}},"required":["hostname","pid"]}},"required":["status","degraded","checks","service","timestamp","instance"]}}}}},"operationId":"get_health","x-access-level":"public"}},"/":{"get":{"summary":"Root API discovery endpoint with enrollment and MCP agent guide","description":"Returns API discovery from `/`, including `agentGuide` for fetching `public/references/skills.md` via MCP (`doc:skills`; deprecated alias `skills:skills` still resolves for one cycle), enrollment URL, documentation links, and endpoints grouped by access level.","tags":["Discovery"],"responses":{"200":{"description":"API discovery information","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"API name"},"version":{"type":"string","description":"API version (from this OpenAPI document info.version)"},"description":{"type":"string","description":"API summary; points agents to agentGuide for skills.md via MCP"},"enrollment":{"type":"object","properties":{"url":{"type":"string","description":"URL for purchasing IdentyClaw Passports"},"guide":{"type":"string","description":"Well-known enrollment guide path"},"badge":{"type":"string","description":"Enrollment badge/call-to-action text"}},"required":["url","guide","badge"]},"documentation":{"type":"object","properties":{"swagger":{"type":"string","description":"Swagger specification URL"},"openapi":{"type":"string","description":"OpenAPI specification URL"},"html":{"type":"string","description":"HTML documentation URL"}}},"endpoints":{"type":"object","properties":{"public":{"type":"array","items":{"type":"string"},"description":"Publicly accessible endpoints (no JWT). OpenAPI: no security / x-access-level public."},"authenticated":{"type":"array","items":{"type":"string"},"description":"Endpoints requiring JWT authentication only (`RoditClient.authenticate`). OpenAPI: BearerAuth: [] — not permissioned_routes / authorize."},"privileged":{"type":"array","items":{"type":"string"},"description":"Endpoints requiring JWT + authorize. OpenAPI: BearerAuth and RoditPermissionedRoutes scopes (= permissioned_routes.entities.methods path keys)."}}},"requestId":{"type":"string","description":"Request correlation identifier"},"timestamp":{"type":"string","format":"date-time","description":"Response timestamp"},"agentGuide":{"type":"object","description":"How agents should fetch public/references/skills.md via MCP before other API calls","properties":{"summary":{"type":"string"},"skills":{"type":"object","properties":{"resourceUris":{"type":"array","items":{"type":"string"},"description":"Canonical MCP resource URIs for public/references/skills.md (prefer doc:skills)"},"http":{"type":"string","description":"REST path to fetch skills markdown (doc:skills)"},"mcpTools":{"type":"object","properties":{"list":{"type":"string"},"get":{"type":"string"}}},"steps":{"type":"object","properties":{"mcpClient":{"type":"string"},"curl":{"type":"string"}}}},"required":["resourceUris","http"]},"mcpDiscovery":{"type":"string"},"listResources":{"type":"string"}},"required":["summary","skills","mcpDiscovery","listResources"]},"wellKnown":{"type":"object","properties":{"enrollment":{"type":"string"},"termsOfService":{"type":"string"},"privacyPolicy":{"type":"string"},"dataRetention":{"type":"string"},"did":{"type":"string"}},"required":["enrollment","termsOfService","privacyPolicy","dataRetention","did"]},"mcp":{"type":"object","properties":{"endpoint":{"type":"string","description":"MCP streamable HTTP endpoint path"},"resources":{"type":"string","description":"MCP resource listing endpoint"},"schema":{"type":"string","description":"MCP schema endpoint"},"discovery":{"type":"string","description":"Well-known MCP discovery metadata endpoint"},"tools":{"type":"array","description":"Canonical MCP tool names supported by this server","items":{"type":"string"}},"skills":{"type":"object","description":"MCP mapping for public/references/skills.md agent workflows","properties":{"primaryUri":{"type":"string"},"alternateUri":{"type":"string"},"fetch":{"type":"string"},"source":{"type":"string"}},"required":["primaryUri","alternateUri","fetch","source"]},"httpAccessForAgents":{"type":"object","description":"REST paths for curl/shell agents (do not raw-GET /mcp)","properties":{"discovery":{"type":"string"},"list":{"type":"string"},"getByUri":{"type":"string"},"skills":{"type":"string","description":"GET public/references/skills.md via doc:skills"}}},"doNotUseRawHttpGetOn":{"type":"string"},"note":{"type":"string"}},"required":["endpoint","resources","schema","discovery","tools","skills","httpAccessForAgents","doNotUseRawHttpGetOn","note"]}},"required":["name","version","description","agentGuide","enrollment","documentation","endpoints","wellKnown","mcp","requestId","timestamp"]}}}}},"operationId":"get_root","x-access-level":"public"}},"/.well-known/enrollment":{"get":{"summary":"Enrollment information and guidance","description":"Provides enrollment information including onboarding steps and support details.","tags":["Discovery"],"responses":{"200":{"description":"Enrollment information","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","description":"Enrollment page title"},"enrollment":{"type":"object","properties":{"url":{"type":"string","description":"Purchase URL"},"description":{"type":"string","description":"Enrollment description"}}},"enrollmentSteps":{"type":"array","items":{"type":"object","properties":{"step":{"type":"integer"},"title":{"type":"string"},"description":{"type":"string"},"details":{"type":"object"}}}},"support":{"type":"object","properties":{"faq":{"type":"string"},"contact":{"type":"string"},"documentation":{"type":"string"},"examples":{"type":"string"}}},"requestId":{"type":"string","description":"Request correlation identifier"},"timestamp":{"type":"string","format":"date-time","description":"Response timestamp"}},"required":["title","enrollment","enrollmentSteps","support","requestId","timestamp"]}}}}},"operationId":"get_well_known_enrollment","x-access-level":"public"}},"/.well-known/mcp":{"get":{"summary":"MCP discovery metadata","description":"Returns canonical MCP discovery metadata including transport endpoint (`/mcp`), `agentGuide` and `docs.skills` for fetching public/references/skills.md (`doc:skills`; deprecated alias `skills:skills` still resolves for one cycle), REST resource paths (`/api/mcp/resources`, `/api/mcp/resource/{uri}`), MCP tools (`verify_hola`, `resolve_passport`, `list_resources`, `get_resource`), `llmsTxt` (`/llms.txt`), JSON alias `/.well-known/mcp.json`, and `httpAccessForAgents` for curl/shell agents (do not probe GET `/mcp` without an MCP client). Verify first; mint second.","tags":["Discovery"],"responses":{"200":{"description":"MCP discovery metadata.","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"transport":{"type":"object","properties":{"protocol":{"type":"string"},"endpoint":{"type":"string"},"absoluteEndpoint":{"type":["string","null"]}},"required":["protocol","endpoint","absoluteEndpoint"]},"resources":{"type":"object","properties":{"list":{"type":"string"},"getByUri":{"type":"string"},"examples":{"type":"array","items":{"type":"string"}}},"required":["list","getByUri","examples"]},"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}},"required":["name","description"]}},"registration":{"type":"object","properties":{"recommendedServerName":{"type":"string"},"aliases":{"type":"array","items":{"type":"string"}}},"required":["recommendedServerName","aliases"]},"docs":{"type":"object","properties":{"skills":{"type":"string","description":"REST path for public/references/skills.md (doc:skills)"},"mcpResources":{"type":"string"},"enrollmentGuide":{"type":"string"},"openapi":{"type":"string"}},"required":["skills","mcpResources","enrollmentGuide","openapi"]},"agentGuide":{"type":"object","description":"Pointer to fetch public/references/skills.md before other API work","properties":{"summary":{"type":"string"},"skills":{"type":"object","properties":{"resourceUris":{"type":"array","items":{"type":"string"}},"http":{"type":"string"}},"required":["resourceUris","http"]}},"required":["summary","skills"]},"httpAccessForAgents":{"type":"object","description":"REST paths for curl, shell, and agents without an MCP Streamable HTTP client.","properties":{"discovery":{"type":"string"},"list":{"type":"string"},"getByUri":{"type":"string"},"health":{"type":"string"},"skills":{"type":"string","description":"GET /api/mcp/resource/doc:skills"},"examples":{"type":"array","items":{"type":"string"}}}},"doNotUseRawHttpGetOn":{"type":"string","description":"Transport path that returns MCP_TRANSPORT_SESSION_REQUIRED on bare GET (curl probe guard). POST without a session uses JSON-RPC -32000 instead — see POST /mcp 400."},"transportNote":{"type":"string","description":"Explains that Mcp-Session-Id is transport-scoped, not API login."},"requestId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["title","transport","resources","tools","registration","docs","agentGuide","httpAccessForAgents","doNotUseRawHttpGetOn","transportNote","requestId","timestamp"]}}}}},"operationId":"get_well_known_mcp","x-access-level":"public"}},"/.well-known/mcp.json":{"get":{"summary":"MCP discovery metadata (JSON alias)","description":"Same payload as `GET /.well-known/mcp`. Many MCP clients probe the `.json` suffix.","tags":["Discovery"],"responses":{"200":{"description":"MCP discovery metadata (identical to `GET /.well-known/mcp`)."}},"operationId":"get_well_known_mcp_json","x-access-level":"public"}},"/llms.txt":{"get":{"summary":"Agent site map (llms.txt)","description":"Plain-text agent discovery document (llmstxt.org). Leads with verify-before-execute, then mint/enrollment, MCP tools, and key HTTP endpoints.","tags":["Discovery"],"responses":{"200":{"description":"llms.txt content","content":{"text/plain":{"schema":{"type":"string"}}}}},"operationId":"get_llms_txt","x-access-level":"public"}},"/openapi.json":{"get":{"summary":"Canonical OpenAPI specification","description":"Returns the canonical OpenAPI specification for this API. Use this endpoint for schema-driven tooling and client generation.","tags":["Discovery"],"responses":{"200":{"description":"OpenAPI specification","content":{"application/json":{"schema":{"type":"object","description":"OpenAPI 3.0 specification"}}}},"500":{"description":"Failed to load OpenAPI specification","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_openapi_json","x-access-level":"public"}},"/swagger.json":{"get":{"summary":"OpenAPI specification alias (redirect)","description":"Compatibility alias. Permanently redirects (`301`) to `GET /openapi.json`. Clients that need the document body should call `/openapi.json` (or `/api/v1/openapi.json`, which also redirects).","tags":["Discovery"],"responses":{"301":{"description":"Redirect to `/openapi.json`.","headers":{"Location":{"description":"Absolute or relative URL of the canonical OpenAPI document.","schema":{"type":"string","example":"/openapi.json"}}}}},"operationId":"get_swagger_json","x-access-level":"public"}},"/api/v1/openapi.json":{"get":{"summary":"Legacy OpenAPI alias","description":"Deprecated compatibility endpoint. Redirects to `/openapi.json`.","deprecated":true,"tags":["Discovery"],"responses":{"301":{"description":"Redirect to /openapi.json"}},"operationId":"get_api_v1_openapi_json","x-access-level":"public"}},"/docs/enrollment":{"get":{"summary":"Removed enrollment docs endpoint","description":"This endpoint has been removed and always returns `410 ENDPOINT_REMOVED`.","deprecated":true,"tags":["Discovery"],"responses":{"410":{"description":"Endpoint removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_docs_enrollment_removed","x-access-level":"public"}},"/docs":{"get":{"summary":"Human-facing API documentation portal","description":"Serves the static HTML docs portal (`public/docs/index.html`) with guide links and an entry point to the OpenAPI browser at `/docs/openapi`.","tags":["Discovery"],"responses":{"200":{"description":"HTML documentation portal.","content":{"text/html":{"schema":{"type":"string"}}}}},"operationId":"get_docs","x-access-level":"public"}},"/docs/openapi":{"get":{"summary":"Swagger UI OpenAPI browser","description":"Interactive Swagger UI for this OpenAPI document. Loads the spec from `GET /openapi.json`. Additional Swagger UI static assets are served under `/docs/openapi/*`.","tags":["Discovery"],"responses":{"200":{"description":"Swagger UI HTML shell.","content":{"text/html":{"schema":{"type":"string"}}}}},"operationId":"get_docs_openapi","x-access-level":"public"}},"/api-docs":{"get":{"summary":"Removed API docs endpoint","description":"Legacy docs mount removed. All methods on `/api-docs` and `/api-docs/*` return `410 ENDPOINT_REMOVED`. Use `GET /docs` and `GET /openapi.json` instead.","deprecated":true,"tags":["Discovery"],"responses":{"410":{"description":"Endpoint removed (`ENDPOINT_REMOVED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_docs_removed","x-access-level":"public"}},"/api-docs/{subpath}":{"get":{"summary":"Removed API docs subpath","description":"Legacy docs subpaths removed. All methods under `/api-docs/*` return `410 ENDPOINT_REMOVED`.","deprecated":true,"tags":["Discovery"],"parameters":[{"name":"subpath","in":"path","required":true,"schema":{"type":"string"},"description":"Any remaining path segment under `/api-docs/`."}],"responses":{"410":{"description":"Endpoint removed (`ENDPOINT_REMOVED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_docs_subpath_removed","x-access-level":"public"}},"/api/holanonce16ts":{"get":{"summary":"Get timestamp and nonce for HOLA handshake","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Returns a timestamp and nonce hex that can be directly concatenated into HOLA handshake messages.","content":{"application/json":{"schema":{"type":"object","properties":{"noncetsHex":{"type":"string","description":"32 uppercase hex characters (16 bytes) for the nonce component of HOLA handshake. This is the hex-encoded random nonce, already normalized to uppercase. Use this value directly when constructing HOLA messages. Do not attempt to parse or extract from a combined format."},"timestamp":{"type":"string","format":"date-time","description":"Server ISO-8601 timestamp when the nonce was generated."},"length":{"type":"integer","description":"Number of random bytes used to generate the nonce (always 16)."},"algorithm":{"type":"string","description":"Algorithm used to generate the nonce."},"requestId":{"type":"string","description":"Request correlation identifier."}},"required":["noncetsHex","timestamp","requestId"]}}}},"401":{"description":"Unauthorized - JWT token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Agent discovery failed due to upstream service errors.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"discoveryFailed":{"summary":"Public agent listing failed","value":{"error":{"code":"AGENT_DISCOVERY_FAILED","message":"Unable to fetch agent list"},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-04-15T08:21:45.000Z"}}}}}}},"operationId":"get_api_holanonce16ts","x-access-level":"authenticated"}},"/api/login/timestamp":{"get":{"summary":"Get timestamp for AI agent login","description":"Public endpoint for AI agents to obtain synchronized timestamp and ISO string needed to construct /api/login requests. Both values are generated from the same moment to ensure signature verification succeeds.","tags":["Authentication"],"responses":{"200":{"description":"Authentication parameters for constructing login requests.","content":{"application/json":{"schema":{"type":"object","properties":{"timestamp":{"type":"integer","description":"Unix timestamp in seconds for /api/login payload."},"timestamp_iso":{"type":"string","format":"date-time","description":"ISO 8601 timestamp for signing the login message."},"requestId":{"type":"string","description":"Request correlation identifier."}},"required":["timestamp","timestamp_iso","requestId"]}}}},"429":{"description":"Too Many Requests - public route rate limit exceeded at the nginx edge (1101 requests per minute per `$remote_addr` sustained, burst 6101). Limits apply only to public URIs listed in nginx `public_rate_limit_key`; see `info.x-publicRateLimit`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"rateLimit":{"summary":"Rate limit exceeded","value":{"error":{"code":"RATE_LIMIT_EXCEEDED","message":"Exceeded anonymous auth-params rate limit"},"requestId":"01HX9WZ7F7B5DDXPKRZC4J8M0X","timestamp":"2026-04-15T08:20:00.000Z"}}}}}},"500":{"description":"Failed to generate authentication parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"generationError":{"summary":"Entropy generator failure","value":{"error":{"code":"AGENT_AUTH_PARAMS_FAILED","message":"Random generator unavailable"},"requestId":"01HX9WZ8TF5X1Q4P3Q2D0A9RZT","timestamp":"2026-04-15T08:20:02.000Z"}}}}}}},"operationId":"get_api_login_timestamp","x-access-level":"public"}},"/api/agents":{"get":{"summary":"List IdentyClaw Passport holders with facial descriptions","description":"Returns a paginated list of all IdentyClaw Passport holders with their facial descriptions decoded from Passport IDs. This is a public endpoint for agent discovery. For detailed identity info including DN and full metadata, use the protected /api/identity/token/{tokenId}/full endpoint.\n\n**Query parameters**: Only **`limit`** and **`cursor`** are supported. Extra parameters (e.g. `owner`) are **ignored** — do not use this endpoint to look up “your” Passport by NEAR account; save the **`token_id`** returned when the Passport was minted.\n\n**Current Capabilities**: Browse agents using pagination (`limit` and `cursor`).\n\n**Planned Features**: Search and owner filtering may be added later.\n\n⚠️ **DISCLAIMER**: The creature field and other agent metadata are self-declared by the agent. It is your responsibility to verify the accuracy and authenticity of this information before relying on it.","tags":["Agent"],"parameters":[{"name":"limit","in":"query","description":"Maximum number of agents to return (default: 20, max: 100)","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","description":"Pagination cursor for fetching the next page","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list of agents with facial descriptions","content":{"application/json":{"schema":{"type":"object","properties":{"agents":{"type":"array","items":{"type":"object","properties":{"tokenId":{"type":"string","description":"The 12-character facial token ID"},"creature":{"type":"string","nullable":true,"description":"Creature/profession/species from the agent's DN metadata"},"face":{"type":"object","nullable":true,"description":"Facial description decoded from tokenId","properties":{"checksumValid":{"type":"boolean","description":"Whether the token ID checksum is valid"},"categories":{"type":"object","description":"Facial feature categories with their values"}}}}}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for fetching the next page, null if no more results"},"requestId":{"type":"string","description":"Request correlation identifier"},"disclaimer":{"type":"string","description":"Warning that creature field and agent metadata are self-declared and should be verified"}},"required":["agents","requestId","disclaimer"]}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to retrieve agents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_agents","x-access-level":"public"}},"/api/concierge/trust-anchor":{"get":{"summary":"Get the official A2A onboarding concierge trust anchor","description":"Returns the canonical trust anchor for **Lemuel Gulliver**, the IdentyClaw A2A Onboarding Concierge: lobby `tokenId` (when configured), Agent Card URL, A2A registry links, and multi-channel ContactURIs.\n\nUse with `POST /api/identity/verify` (`includeProfile: true`) to implement **verify before execute** and **impersonation guards** — compare verified `peerTokenId` to `lobbyTokenId` before trusting concierge-originated HOLA.\n\n**Related:** MCP `doc:reference:concierge-lobby-passport`, `doc:reference:a2a-onboarding-concierge`; verify UI https://verify.identyclaw.com; CLI `npx @rodit/verify-hola report`.\n\n**Authentication:** Public endpoint (rate-limited at the nginx edge per `info.x-publicRateLimit`).","tags":["Discovery"],"responses":{"200":{"description":"Concierge trust anchor metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConciergeTrustAnchor"}}}},"500":{"description":"Failed to load concierge configuration.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_concierge_trust_anchor","x-access-level":"public"}},"/api/demo/heads":{"get":{"summary":"List demo Passport heads for the verify credibility wall","description":"Public roster (typically three heads) for https://verify.identyclaw.com / https://verify.dihola.io. No JWT. Returns published `tokenId` labels, display names, and optional public `didAgentId` values — never Passport private keys or D-ID API secrets.\n\n**Authentication:** Public. App-level per-IP rate limit returns `429` + `Retry-After`; also covered by nginx `info.x-publicRateLimit`.","tags":["Demo"],"responses":{"200":{"description":"Demo head roster.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DemoHeadsResponse"}}}},"429":{"description":"Rate limit exceeded. `Retry-After` header present when possible.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the client may retry."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Demo heads are not configured (`DEMO_HEADS`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_demo_heads","x-access-level":"public"}},"/api/demo/proofs":{"get":{"summary":"Fetch rotating context-pinned demo HOLAs","description":"Returns current signed HOLA lines for active demo Passports. The destinatary is the verify-page pin: 1–12 letters, no numbers or special characters (query `destinatary`). When omitted, the server uses a letters-only pin derived from the verify host (dots removed, capped at 12), not generic `MUNDO`. Lines rotate before typical `maxAgeMs` (300s); clients should poll using `pollIntervalMs` (under 60s). After each rotation the prior line is retained as `previousHola` for the stale-fail beat.\n\nProof bar matches `POST /api/identity/verify`. Grabbed lines endorse only the pinned destinatary, which is the recipient inside the QR.\n\n**Authentication:** Public. App-level per-IP rate limit returns `429` + `Retry-After`.","tags":["Demo"],"parameters":[{"name":"destinatary","in":"query","required":false,"schema":{"type":"string","pattern":"^[A-Za-z]{1,12}$","maxLength":12,"example":"VERIFY"},"description":"Destinatary pin from the verify page. Letters only, up to 12 characters. This value is the recipient of the signed HOLA carried by the identity QR. Digits and punctuation are rejected."}],"responses":{"200":{"description":"Current demo proofs and poll hint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DemoProofsResponse"}}}},"400":{"description":"destinatary is not 1–12 letters (`INVALID_DESTINATARY`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded. `Retry-After` header present when possible.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the client may retry."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Demo proofs are not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_demo_proofs","x-access-level":"public"}},"/api/funnel/land":{"get":{"summary":"Cookieless land beacon for sibling SPAs","description":"Records a `funnel.land` step for enrollment attribution (Winston/Loki). Intended for purchase and marketing SPAs that call this host via same-origin `/api` proxy (or CORS).\n\nAttribution is **cookieless**: optional UTMs and `funnel_id` via query, JSON body (POST), or `X-Utm-*` / `X-Campaign-*` / `X-Funnel-Id` / `X-Visitor-Id` headers. Optional `domain` via query, body, or `X-Funnel-Domain` — allowlisted hosts only; unknown values fall back to `api.identyclaw.com`.\n\n**Authentication:** Public (no JWT). Empty **204** body on success.","tags":["Funnel"],"parameters":[{"name":"domain","in":"query","required":false,"schema":{"type":"string","enum":["purchase.identyclaw.com","verify.identyclaw.com","www.discernible.io","discernible.io","lastcradle.io","www.lastcradle.io","api.identyclaw.com"]},"description":"Emitting surface host. Must be allowlisted; otherwise defaults to `api.identyclaw.com`."},{"name":"utm_source","in":"query","required":false,"schema":{"type":"string"}},{"name":"utm_medium","in":"query","required":false,"schema":{"type":"string"}},{"name":"utm_campaign","in":"query","required":false,"schema":{"type":"string"}},{"name":"utm_content","in":"query","required":false,"schema":{"type":"string"}},{"name":"utm_term","in":"query","required":false,"schema":{"type":"string"}},{"name":"funnel_id","in":"query","required":false,"schema":{"type":"string"},"description":"Pre-Passport hop stitch id. Alias query key: `vid`."},{"name":"vid","in":"query","required":false,"schema":{"type":"string"},"description":"Alias for `funnel_id`."},{"name":"X-Funnel-Domain","in":"header","required":false,"schema":{"type":"string"},"description":"Optional domain when not passed as query/body."},{"name":"X-Funnel-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Optional funnel_id header. Alias: `X-Visitor-Id`."},{"name":"X-Utm-Source","in":"header","required":false,"schema":{"type":"string"}},{"name":"X-Utm-Medium","in":"header","required":false,"schema":{"type":"string"}},{"name":"X-Utm-Campaign","in":"header","required":false,"schema":{"type":"string"}},{"name":"X-Utm-Content","in":"header","required":false,"schema":{"type":"string"}},{"name":"X-Utm-Term","in":"header","required":false,"schema":{"type":"string"}},{"$ref":"#/components/parameters/XRequestId"},{"$ref":"#/components/parameters/XCorrelationId"}],"responses":{"204":{"description":"Land event recorded. Empty body."}},"operationId":"get_api_funnel_land","x-access-level":"public"},"post":{"summary":"Cookieless land beacon for sibling SPAs","description":"Same as `GET /api/funnel/land`. Prefer POST when attribution is sent in a JSON body.\n\n**Authentication:** Public (no JWT). Empty **204** body on success.","tags":["Funnel"],"parameters":[{"name":"domain","in":"query","required":false,"schema":{"type":"string","enum":["purchase.identyclaw.com","verify.identyclaw.com","www.discernible.io","discernible.io","lastcradle.io","www.lastcradle.io","api.identyclaw.com"]},"description":"Emitting surface host. Must be allowlisted; otherwise defaults to `api.identyclaw.com`."},{"name":"utm_source","in":"query","required":false,"schema":{"type":"string"}},{"name":"utm_medium","in":"query","required":false,"schema":{"type":"string"}},{"name":"utm_campaign","in":"query","required":false,"schema":{"type":"string"}},{"name":"utm_content","in":"query","required":false,"schema":{"type":"string"}},{"name":"utm_term","in":"query","required":false,"schema":{"type":"string"}},{"name":"funnel_id","in":"query","required":false,"schema":{"type":"string"},"description":"Pre-Passport hop stitch id. Alias query key: `vid`."},{"name":"vid","in":"query","required":false,"schema":{"type":"string"},"description":"Alias for `funnel_id`."},{"name":"X-Funnel-Domain","in":"header","required":false,"schema":{"type":"string"},"description":"Optional domain when not passed as query/body."},{"name":"X-Funnel-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Optional funnel_id header. Alias: `X-Visitor-Id`."},{"name":"X-Utm-Source","in":"header","required":false,"schema":{"type":"string"}},{"name":"X-Utm-Medium","in":"header","required":false,"schema":{"type":"string"}},{"name":"X-Utm-Campaign","in":"header","required":false,"schema":{"type":"string"}},{"name":"X-Utm-Content","in":"header","required":false,"schema":{"type":"string"}},{"name":"X-Utm-Term","in":"header","required":false,"schema":{"type":"string"}},{"$ref":"#/components/parameters/XRequestId"},{"$ref":"#/components/parameters/XCorrelationId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FunnelLandRequest"}}}},"responses":{"204":{"description":"Land event recorded. Empty body."}},"operationId":"post_api_funnel_land","x-access-level":"public"}},"/api/me/identity":{"get":{"summary":"Get authenticated agent's own DN and facial description","description":"Returns the caller's own Distinguished Name (DN) and facial description based on their authenticated IdentyClaw Passport. This endpoint is for self-identification only - it derives the caller's tokenId from the JWT sub field. For looking up other agents, use GET /api/identity/token/{tokenId}/full.","tags":["Identity"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Authenticated agent's own DN and facial description.","content":{"application/json":{"schema":{"type":"object","properties":{"tokenId":{"$ref":"#/components/schemas/TokenId"},"dn":{"type":"object","nullable":true,"description":"Parsed Distinguished Name information from token metadata."},"face":{"type":"object","nullable":true,"description":"Facial description decoded from tokenId."},"notAfter":{"type":"string","nullable":true,"description":"Token expiration timestamp (ISO8601 format). Null if token has no expiration."},"metadata":{"type":"object","nullable":true,"description":"Full token metadata including openapijson_url, not_after, not_before, max_requests, webhook_url, userselected_dn, and userselected_dn_info."},"requestId":{"type":"string"}},"required":["tokenId","dn","face","notAfter","metadata","requestId"]}}}},"400":{"description":"IdentityUnavailable when sub is missing or unparsable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"missingSub":{"summary":"JWT sub missing","value":{"error":{"code":"IDENTITY_SUB_MISSING","message":"JWT payload is missing required sub field for identity derivation"},"requestId":"01HX7HAGTDEJQ8G2KJ8JHB6Q1F","timestamp":"2026-04-15T07:45:00.000Z"}}}}}},"404":{"description":"IdentityNotFound when NEAR token does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"missingToken":{"summary":"Token missing","value":{"error":{"code":"IDENTITY_NOT_FOUND","message":"RODiT identity not found","details":{"tokenId":"pkcnjdbdefcp"}},"requestId":"01HX7HANWC0VCG6F1D2Z1KQ7W4","timestamp":"2026-04-15T07:46:00.000Z"}}}}}},"500":{"description":"Identity lookup failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_me_identity","x-access-level":"authenticated"}},"/api/identity/verify":{"post":{"summary":"Verify a peer's HOLA using Ed25519 and a Morse-compatible canonical message","description":"Peer-verification endpoint for HOLA messages received from another agent. Use `/api/identity/verify` for peer verification; use `/api/testhola` for self-testing your own outbound HOLA generation.\n\n**How-To:** See `public/references/hola-howto.md` (MCP: `doc:reference:hola-howto`) for the linear login → nonce → sign → verify path.\n\n**Fail-closed early validation (HTTP 400):** Uses the same format/checksum parser as `/api/testhola`. Rejects before peer signature/token checks when:\n- Payload envelope is invalid (`hola` must be a string, not a nested JSON object; `reasonCode: payload_is_json_object`)\n- Transport-shape lint fails (`unix_millis_timestamp`, `signature_not_base32`, `placeholder_signature`)\n- HOLA line format or checksum is invalid (`stage: format_checksum_and_payload_validation`, `reasonCode: invalid_format` or `checksum_invalid`)\n\nAll early failures return `400` with `code: HOLA_VALIDATION_FAILED` and `error.details.reasonCode` (same contract as `/api/testhola`).\n\n**Avatar filename (informational):** When the peer token loads, the server fetches `userselected_dn` `AvatarURL` over HTTPS and compares the filename to the unpadded base64url form of a 16-byte BLAKE3 digest of the image bytes. The result is `avatarContentHash` (`affectsVerified: false`). It does not change `verified` or `failureReasons`.\n\n**Peer verification outcome (HTTP 200):** When format and checksum pass, returns `verified` plus per-check booleans. Timestamp freshness, nonce replay, token existence/activity, and Ed25519 signature outcomes are reported on the 200 body via `failureReasons` (e.g. `timestamp_stale_or_future`, `nonce_replay`, `token_missing`, `signature_invalid`) — not as `invalid_format` or `checksum_invalid`. Each `failureDetails` entry includes a prescriptive `hint`. When `nonce_replay` is present, `nonceReplayDetails` includes `holaTimestamp`, `maxAgeMs`, and optionally `firstSeenAt`.\n\nSet `includeProfile: true` to attach a sanitized `PublicPassportProfile` for `peerTokenId` — used by https://verify.identyclaw.com and `@rodit/verify-hola`. For concierge impersonation guards, compare `peerTokenId` to `lobbyTokenId` from `GET /api/concierge/trust-anchor` (MCP: `doc:reference:concierge-lobby-passport`).\n\nFor self-correction workflows and richer diagnostics/examples, validate sender-side payloads with `/api/testhola`.\n\n**Authentication:** Public endpoint — no bearer token is required (rate-limited at the nginx edge per `info.x-publicRateLimit`). A RODiT bearer token may optionally be supplied; when present, the caller's authenticated identity is used to emit the `RECIPIENT_MISMATCH` warning (suppressible via `expectedRecipient`).","security":[{},{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hola":{"type":"string","maxLength":512,"description":"Request field containing the HOLA handshake line exchanged between AI agents. CASE-INSENSITIVE: The protocol accepts any case (e.g., 'hola', 'HOLA', 'Hola'). For verification, the server canonicalizes the full signed payload to uppercase before Ed25519 and checksum validation. Token IDs are then normalized to lowercase only for blockchain lookup. STANDARD FORMAT: HOLA/<recipient>/<tokenId>/<ISO8601-timestamp>/<noncets-hex>/API.IDENTYCLAW.COM/<base32-ed25519-signature>/<checksum>. SUBAGENT FORMAT (delegated signers): HOLA/<recipient>/<delegateID>/<issuer_tokenId>/<publicKey-base32>/<ISO8601-timestamp>/<noncets-hex>/API.IDENTYCLAW.COM/<base32-ed25519-signature>/<checksum>. Base32 variant is RFC 4648 Base32 with alphabet A-Z2-7; uppercase canonical form is used and '=' padding is optional on input. For standard format, the Ed25519 signature is computed over HOLA/<recipient>/<tokenId>/<ISO8601-timestamp>/<noncets-hex>/API.IDENTYCLAW.COM/. For subagent format, the signature is computed over HOLA/<recipient>/<delegateID>/<issuer_tokenId>/<publicKey-base32>/<ISO8601-timestamp>/<noncets-hex>/API.IDENTYCLAW.COM/. The checksum is a single letter from ABCDEFGHJKMNPQRSTUVWXYZ (23 letters; omits I, L, O). Checksum algorithm: sum UTF-16 code units (JavaScript String semantics; ASCII-only prefixes match byte sums) over HOLA/<recipient>/.../<base32-ed25519-signature>/ including trailing slash (everything before this checksum field), modulo 23, index into that alphabet (NOT MD5/SHA). Recipient defaults to MUNDO if not specified. Note: noncets-hex is 32 uppercase hex characters from /api/holanonce16ts (hex-encoded, uppercase). delegateID is the hashOrDelegateId from /api/isauthorizedsigner (DID, name, or BLAKE3 hash). Maximum length: 512 characters. See /api/isauthorizedsigner for subagent authorization validation.","example":"HOLA/MUNDO/aaaaaaaaaaaa/2026-04-04T10:10:00Z/4F9A3C7E2D1B9A4C8E7F6A5B4C3D2E1F/API.IDENTYCLAW.COM/N3FZ5KQ8LH2BSM1XY/C"},"constraints":{"type":"object","properties":{"maxAgeMs":{"type":"integer","description":"Maximum allowed age in milliseconds for the hola. Defaults to 300000."}}},"expectedRecipient":{"type":"string","pattern":"^[A-Za-z]{1,12}$","maxLength":12,"description":"Optional destinatary pin from the verify page: 1–12 letters, no numbers or special characters. Must match the HOLA recipient (the destinatary of the identity QR). A mismatch sets checks.destinataryMatch to false and failureReasons destinatary_mismatch. When it matches, the RECIPIENT_MISMATCH warning is suppressed."},"includeProfile":{"type":"boolean","description":"When true, include a sanitized public passport profile for peerTokenId."}},"required":["hola"]}}}},"responses":{"200":{"description":"Verification result with detailed checks.","content":{"application/json":{"schema":{"type":"object","properties":{"verified":{"type":"boolean","description":"True when all checks pass and the peer hola is accepted."},"peerTokenId":{"type":"string","description":"token_id extracted from the peer hola. For subagent format, this is the issuerTokenId."},"destinatary":{"type":"string","description":"Recipient field extracted from the HOLA message."},"profile":{"allOf":[{"$ref":"#/components/schemas/PublicPassportProfile"}],"description":"Sanitized public passport profile when includeProfile is true."},"notBefore":{"type":"string","nullable":true,"description":"On-chain metadata.not_before from the same token load used for expiry/key checks. Soft mint-age signal for UI; never an input to verified. Prefer this over a follow-up profile/RPC fetch after handshake."},"notAfter":{"type":"string","nullable":true,"description":"On-chain metadata.not_after from the same token load. Soft display of expiry window; hard expiry still sets checks.tokenActive / token_expired. Does not itself flip verified beyond that existing check."},"isSubagentFormat":{"type":"boolean","description":"True when the HOLA message uses the subagent format (11 fields) instead of standard format (8 fields). Only present when true."},"delegateId":{"type":"string","description":"The delegateID from the subagent HOLA message. Only present when isSubagentFormat is true."},"issuerTokenId":{"type":"string","description":"The parent agent's token ID who authorized this subagent. Only present when isSubagentFormat is true."},"checks":{"type":"object","description":"Detailed boolean flags indicating which verification stages passed.","properties":{"tokenExists":{"type":"boolean","description":"True when the peer token exists on NEAR."},"tokenActive":{"type":"boolean","description":"True when the token is considered active for verification. Currently false when token is expired based on metadata.not_after."},"timestampFresh":{"type":"boolean","description":"True when the timestamp is within the accepted age window."},"nonceReplaySafe":{"type":"boolean","description":"True when the nonce has not already been accepted for the same sender token within the active timestamp window."},"signatureValid":{"type":"boolean","description":"True when the Ed25519 signature matches the canonical message."},"checksumValid":{"type":"boolean","description":"True when the checksum matches the canonical hola payload. Always true on HTTP 200; checksum mismatch returns HTTP 400 with reasonCode checksum_invalid before this response."}},"required":["tokenExists","tokenActive","timestampFresh","nonceReplaySafe","signatureValid","checksumValid"]},"failureReasons":{"type":"array","description":"List of machine-readable codes describing why peer verification failed (timestamp, nonce, token, signature). Empty when verified is true. Format and checksum faults are not listed here; they fail closed with HTTP 400.","items":{"type":"string","enum":["timestamp_stale_or_future","nonce_replay","token_missing","token_expired","signature_invalid","public_key_unavailable","public_key_error","subagent_public_key_invalid_length","subagent_public_key_decode_failed"]}},"failureDetails":{"type":"array","description":"Human-readable descriptions and actionable hints for each failureReasons entry.","items":{"type":"object","properties":{"reasonCode":{"type":"string"},"description":{"type":"string"},"hint":{"type":"string","description":"Prescriptive next step for resolving this failure reason."}},"required":["reasonCode","description","hint"]}},"nonceReplayDetails":{"type":"object","description":"Present when failureReasons includes nonce_replay. Describes the HOLA timestamp and freshness window used for replay detection.","properties":{"holaTimestamp":{"type":"string","description":"ISO-8601 timestamp parsed from the HOLA line."},"maxAgeMs":{"type":"integer","description":"Maximum HOLA age in milliseconds used for freshness and replay checks."},"firstSeenAt":{"type":"string","description":"ISO-8601 time when this nonce was first consumed for this signer, if known."}},"required":["holaTimestamp","maxAgeMs"]},"signatureVerificationImplemented":{"type":"boolean","description":"Signals that signature verification is active for this server implementation."},"avatarContentHash":{"$ref":"#/components/schemas/AvatarContentHash"},"warnings":{"type":"array","description":"Optional warning entries for suspicious but non-fatal conditions such as recipient mismatch.","items":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"requestId":{"type":"string","description":"Request correlation identifier."}},"required":["verified","peerTokenId","checks","failureReasons","failureDetails","signatureVerificationImplemented","notBefore","notAfter","avatarContentHash","requestId"]}}}},"400":{"description":"Fail-closed HOLA validation failure: payload envelope, transport-shape lint, invalid format, bad checksum, invalid nonce hex, or invalid constraints. Returns error.details.stage and error.details.reasonCode (aligned with /api/testhola for format/checksum). Timestamp-only faults after parsing may use HOLA_TIMESTAMP_INVALID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"invalidFormat":{"summary":"Invalid HOLA format (non-HOLA string)","value":{"error":{"code":"HOLA_VALIDATION_FAILED","message":"Unsupported protocol; expected HOLA prefix","details":{"valid":false,"stage":"format_checksum_and_payload_validation","reasonCode":"invalid_format","checks":{"formatValid":false,"checksumValid":false,"timestampValid":false,"noncetsValid":false},"selfTest":"POST /api/testhola for full diagnostics, documentation links, and corrected examples."}},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-05-21T07:50:53.322Z"}},"checksumInvalid":{"summary":"Checksum invalid","value":{"error":{"code":"HOLA_VALIDATION_FAILED","message":"Invalid checksum: expected A, got B","details":{"valid":false,"stage":"format_checksum_and_payload_validation","reasonCode":"checksum_invalid","checks":{"formatValid":true,"checksumValid":false,"timestampValid":true,"noncetsValid":true},"selfTest":"POST /api/testhola for full diagnostics, documentation links, and corrected examples."}},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-05-21T07:50:53.322Z"}},"payloadEnvelope":{"summary":"Payload envelope (hola must be a string)","value":{"error":{"code":"HOLA_VALIDATION_FAILED","message":"Property \"hola\" must be a string containing the full HOLA line, not a JSON object.","details":{"reasonCode":"payload_is_json_object","stage":"payload_envelope_validation","field":"hola","acceptedFields":["hola"],"preferredField":"hola"}},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-05-21T07:50:53.322Z"}}}}}},"415":{"description":"UnsupportedMediaType - Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"NEAR RPC call failed before the token was checked, including credit exhaustion. The chain view did not complete, so this is not a missing token. error.details is omitted; clients should display error.message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"SERVICE_UNAVAILABLE","message":"Chain lookup failed because the NEAR RPC is out of credits. The RODiT token was not checked."},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-10-06T12:00:00.000Z"}}}}},"operationId":"post_api_identity_verify","x-access-level":"public-or-authenticated"}},"/api/isauthorizedsigner":{"post":{"summary":"Verify if a public key is authorized to sign on behalf of an IdentyClaw Passport holder","description":"Validates whether a given Ed25519 public key has been authorized by an IdentyClaw Passport holder to act as a delegated signer. This is the primary mechanism for subagent authorization in OpenClaw. Subagents generate their own Ed25519 keypairs and request authorization from their parent agent. The parent signs the delegation record (tokenId:delegateId:timestamp:publicKey) using its Passport signing key, which is then verified by this endpoint. This enables hierarchical agent organizations where subagents can prove they're authorized by a specific parent without needing their own IdentyClaw Passport.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tokenId":{"type":"string","pattern":"^[a-z]{12}$","description":"The IdentyClaw Passport token ID (12 lowercase letters) of the passport holder (owner) authorizing the signer. The endpoint retrieves this owner's public key from NEAR blockchain to verify the signature."},"base64HashOrDelegateSignerId":{"type":"string","minLength":1,"maxLength":128,"description":"Unique identifier or hash for the delegated signer being authorized. Can be a DID, name, BLAKE3 hash, or any text identifier (1-128 characters). Used to identify which specific delegated signer is being authorized. If using a hash, canonicalization is the agent's responsibility."},"unixTimestamp":{"type":"integer","minimum":0,"description":"Unix timestamp (seconds since epoch) when the authorization was granted. Used to prevent replay attacks and establish temporal context."},"publicKey":{"type":"string","description":"Base64url-encoded Ed25519 public key (32 bytes when decoded) of the delegated signer (the entity being authorized to sign on behalf of the tokenId owner). This is the key we are validating as authorized."},"signature":{"type":"string","description":"Base64url-encoded Ed25519 signature (64 bytes when decoded) created by the tokenId owner's private key, proving they authorized this delegated signer's public key. The signature is computed over the message: tokenId:base64HashOrDelegateSignerId:unixTimestamp:publicKey"}},"required":["tokenId","base64HashOrDelegateSignerId","unixTimestamp","publicKey","signature"]}}}},"responses":{"200":{"description":"Authorization check completed. Returns detailed verification results regardless of authorization status.","content":{"application/json":{"schema":{"type":"object","properties":{"authorized":{"type":"boolean","description":"True if the public key is authorized by the passport holder; false otherwise."},"tokenId":{"type":"string","description":"The token ID that was checked."},"base64HashOrDelegateSignerId":{"type":"string","description":"The hash or delegated signer ID that was checked."},"checks":{"type":"object","description":"Detailed boolean flags indicating which verification stages passed.","properties":{"tokenExists":{"type":"boolean","description":"True when the passport token exists on NEAR."},"tokenActive":{"type":"boolean","description":"True when the token is considered active (not revoked/expired). NOTE: Currently a planned feature - returns true if token exists. Full implementation pending contract metadata support."},"publicKeyAuthorized":{"type":"boolean","description":"True when the Ed25519 signature is valid and proves the public key is authorized by the passport holder."}},"required":["tokenExists","tokenActive","publicKeyAuthorized"]},"failureReasons":{"type":"array","description":"List of machine-readable codes describing why authorization failed. Empty when authorized is true.","items":{"type":"string","enum":["token_owner_missing","owner_public_key_unavailable","signature_missing_or_invalid","signature_verification_failed","public_key_error"]}},"requestId":{"type":"string","description":"Request correlation identifier."}},"required":["authorized","tokenId","checks","failureReasons","requestId"]}}}},"400":{"description":"Invalid request - missing or malformed parameters (tokenId, unixTimestamp, publicKey, or signature).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized - JWT token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"NotFound - The specified tokenId does not exist on the blockchain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"UnsupportedMediaType - Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"post_api_isauthorizedsigner","x-access-level":"authenticated"}},"/api/login":{"post":{"summary":"RODiT client login to obtain a JWT access token","description":"Delegates to `RoditClient#login_client` (@rodit/rodit-auth-be): Express handler that reads `req.body` after JSON parsing. Signed payload is `identifier + timestamp_iso` (concatenated, no separator) where `identifier` is the chosen `roditid` or `accountid`. Server must have own RODiT config initialized (`getConfigOwnRodit`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","description":"Login with `roditid`; signature in `base64url_signature`.","required":["roditid","base64url_signature"],"properties":{"roditid":{"type":"string","description":"12-letter on-chain `token_id`; must match the identifier used when signing."},"timestamp":{"type":"integer","description":"Unix seconds from the same GET /api/login/timestamp challenge pair used to build the signing payload."},"base64url_signature":{"type":"string","description":"Ed25519 signature (base64url) over UTF-8 `roditid + timestamp_iso`."}},"additionalProperties":false},{"type":"object","description":"Login with `roditid`; signature alias `roditid_base64url_signature` (wire-compatible with login_server).","required":["roditid","roditid_base64url_signature"],"properties":{"roditid":{"type":"string","description":"12-letter on-chain `token_id`."},"timestamp":{"type":"integer","description":"Unix seconds from the same GET /api/login/timestamp challenge pair used to build the signing payload."},"roditid_base64url_signature":{"type":"string","description":"Same bytes as `base64url_signature`; do not send both signature fields non-empty."}},"additionalProperties":false},{"type":"object","description":"Login with NEAR implicit `accountid` (64-char hex); signature in `base64url_signature`.","required":["accountid","base64url_signature"],"properties":{"accountid":{"type":"string","description":"64-char lowercase hex NEAR implicit account id; signed message is `accountid + timestamp_iso`."},"timestamp":{"type":"integer","description":"Unix seconds from the same GET /api/login/timestamp challenge pair used to build the signing payload."},"base64url_signature":{"type":"string","description":"Ed25519 signature (base64url) over UTF-8 `accountid + timestamp_iso`."}},"additionalProperties":false},{"type":"object","description":"Login with NEAR implicit `accountid`; signature alias `roditid_base64url_signature`.","required":["accountid","roditid_base64url_signature"],"properties":{"accountid":{"type":"string","description":"64-char lowercase hex NEAR implicit account id."},"timestamp":{"type":"integer","description":"Unix seconds from the same GET /api/login/timestamp challenge pair used to build the signing payload."},"roditid_base64url_signature":{"type":"string","description":"Same bytes as `base64url_signature`; do not send both signature fields non-empty."}},"additionalProperties":false}],"description":"Exactly one of `roditid` or `accountid` after trim (not both, not neither). Exactly one of `base64url_signature` or `roditid_base64url_signature`. Provide exactly one timestamp format (`timestamp` or `timestamp_iso`) from the same GET /api/login/timestamp challenge pair used for signing. Deprecated keys `signature`, `account_id` → 400 `LOGIN_PAYLOAD_DEPRECATED`. Ambiguous timestamp/identifier/signature fields → 400 with codes such as `LOGIN_TIMESTAMP_AMBIGUOUS`, `INVALID_LOGIN_TIMESTAMP`, `LOGIN_IDENTIFIER_AMBIGUOUS`, `MISSING_LOGIN_IDENTIFIER`, `MISSING_BASE64URL_SIGNATURE`."}}}},"responses":{"200":{"description":"Login successful. Body includes `jwt_token` and `requestId`; header `New-Token` repeats the JWT.","headers":{"New-Token":{"description":"Same JWT string as `jwt_token` in the response body.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["jwt_token","requestId"],"properties":{"jwt_token":{"type":"string","description":"Bearer JWT for subsequent API calls."},"requestId":{"type":"string","description":"Request correlation id from the auth layer."}},"additionalProperties":true}}}},"400":{"description":"Validation error (payload shape, ambiguous identifiers, deprecated keys, etc.).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Credential verification failed when `SECURITY_OPTIONS.SILENT_LOGIN_FAILURES` is false. Typical `error.code` values include `LOGIN_CHALLENGE_TIMESTAMP_INVALID` (login challenge Unix `timestamp` too far in the future vs server), `LOGIN_BASE64URL_SIGNATURE_INVALID` (base64url Ed25519 over UTF-8 identifier + canonical `timestamp_iso` from GET /api/login/timestamp did not verify), `RODIT_NOT_FOUND`, `RODIT_MISSING_METADATA`, `RODIT_NOT_LIVE`, `RODIT_REVOKED`, `RODIT_FAMILY_MISMATCH`, `SMART_CONTRACT_NOT_TRUSTED`, `SERVER_CONFIG_INCOMPLETE`, `LOGIN_MODE_POLICY_REJECTED_*`, `LOGIN_ERROR`, `INVALID_CREDENTIALS`. Response uses unified `error` object with optional `error.details.failureReason` mirroring `error.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"loginChallengeTimestamp":{"summary":"Login challenge timestamp invalid","value":{"error":{"code":"LOGIN_CHALLENGE_TIMESTAMP_INVALID","message":"Error 102: Login attempt failed: Login challenge timestamp invalid: the Unix `timestamp` from your POST body is too far in the future relative to server time (use the `timestamp` from the same GET /api/login/timestamp response as your login signing payload; check clock skew).","details":{"failureReason":"LOGIN_CHALLENGE_TIMESTAMP_INVALID","failureMessage":"Login challenge timestamp invalid: the Unix `timestamp` from your POST body is too far in the future relative to server time (use the `timestamp` from the same GET /api/login/timestamp response as your login signing payload; check clock skew)."}},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-05-10T12:00:00.000Z"}},"loginBase64urlSignature":{"summary":"Login base64url signature invalid","value":{"error":{"code":"LOGIN_BASE64URL_SIGNATURE_INVALID","message":"Error 102: Login attempt failed: Login base64url signature invalid: Ed25519 verification failed for the base64url_signature over UTF-8 (roditid or accountid) + canonical timestamp_iso from the login challenge (GET /api/login/timestamp). Wrong key, wrong payload, or wrong encoding (must be base64url, not standard base64).","details":{"failureReason":"LOGIN_BASE64URL_SIGNATURE_INVALID","failureMessage":"Login base64url signature invalid: Ed25519 verification failed for the base64url_signature over UTF-8 (roditid or accountid) + canonical timestamp_iso from the login challenge (GET /api/login/timestamp). Wrong key, wrong payload, or wrong encoding (must be base64url, not standard base64)."}},"requestId":"01HX9X0T9CS1EM0WQ7R6F5B2VY","timestamp":"2026-05-10T12:00:00.000Z"}}}}}},"415":{"description":"UnsupportedMediaType - Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error during login.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Authentication service unavailable (e.g. RoditClient not initialized on this instance).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"post_api_login","x-access-level":"public"}},"/api/logout":{"post":{"summary":"RODiT client logout to invalidate the current session","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Logout successful; response body is produced by the RODiT SDK.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"415":{"description":"UnsupportedMediaType - Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Logout failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Authentication service unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"post_api_logout","x-access-level":"authenticated"}},"/api/signclient":{"post":{"summary":"Request minting of a client Passport via SignPortal","description":"Frontend calls this endpoint with tamperproofed values and a locally computed minting fee. The server validates permissions against its own permissioned_routes, recomputes the minting fee, and then delegates to SignPortal to mint a new client IdentyClaw Passport. Optionally, caller can specify a token_id to use for signing; SignPortal will validate the token.\n\n**Errors:** Validation failures use `SIGNCLIENT_*` codes with client-safe `details` where helpful. HTTP 500 responses use generic messages (`SIGNCLIENT_FAILED`); upstream/SignPortal detail is logged with `requestId`, not returned in the body.","parameters":[{"$ref":"#/components/parameters/XRequestId"},{"$ref":"#/components/parameters/XCorrelationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tobesignedValues":{"type":"object","description":"Tamperproof payload describing the client RODiT to mint (includes permissioned_routes, max_requests, maxrq_window, not_after, serviceprovider_signature, etc.)."},"mintingfee":{"type":"string","description":"Total minting fee computed by the client; must match the server's own calculation or the request will be rejected."},"token_id":{"type":"string","pattern":"^[bcdfghjklmnpqrstvwxyz]{12}$","description":"Optional: Caller-provided token ID (12 characters from safe charset). If not provided, SignPortal generates one. Must be valid facial encoding with correct checksum."},"tier":{"type":"string","enum":["personal","enterprise","collectible"],"description":"Optional: Pricing tier for fee calculation. If provided, the server uses tier-specific rate limits and fee structures instead of the formula-based calculation. Tiers: personal (48 requests/60s, variable fee), enterprise (4,999 requests/60s, 1,806 NEAR/year), collectible (496 requests/60s, temporary promotional 9 NEAR one-time through October 16, 2026 then 496 NEAR, immortal)."}},"required":["tobesignedValues","mintingfee"]}}}},"responses":{"201":{"description":"Client RODiT minted successfully (`201`). Response includes SignPortal result (token_id, signatures, metadata) plus fee_data object containing the exact fee values that were signed (token_id, mintingfee, mintingfeeaccount). Frontend uses fee_data to reconstruct and verify the fee_signature_base64url. Optional request `token_id` is ignored for `personal` tier (SignPortal generates it).","content":{"application/json":{"schema":{"type":"object","properties":{"fee_data":{"type":"object","properties":{"token_id":{"type":"string"},"mintingfee":{"type":"string"},"mintingfeeaccount":{"type":"string"}},"required":["token_id","mintingfee","mintingfeeaccount"]}},"required":["fee_data"],"additionalProperties":true}}}},"400":{"description":"Validation error (`SIGNCLIENT_*`), including missing fields, invalid permissioned_routes, fee mismatch (`SIGNCLIENT_FEE_MISMATCH`), fee params (`SIGNCLIENT_FEE_PARAMS_INVALID`), or invalid token_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"feeMismatch":{"$ref":"#/components/examples/ErrorSignclientFeeMismatch"}}}}},"401":{"description":"Unauthorized - SignPortal authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Privileged operations cannot be granted to client tokens (`SIGNCLIENT_PRIVILEGED_OPERATIONS_DENIED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"UnsupportedMediaType - Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server or SignPortal error while attempting to mint the client token (`SIGNCLIENT_FAILED` or `SIGNCLIENT_INVALID_REQUEST` for validation-shaped failures). Response message is generic; use `requestId` in logs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"signclientFailed":{"$ref":"#/components/examples/ErrorSignclientFailed"}}}}},"502":{"description":"Nginx edge: application upstream unavailable (`UPSTREAM_UNAVAILABLE`). See `info.x-edgeGateway`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"upstreamUnavailable":{"$ref":"#/components/examples/ErrorEdgeUpstreamUnavailable"}}}}}},"operationId":"post_api_signclient","x-access-level":"public"}},"/api/mcp/resources":{"get":{"summary":"List available MCP resources (including OpenAPI schema)","description":"Returns all MCP resources. Agents should start with skills documentation (`doc:skills` → public/references/skills.md). Also includes OpenAPI schema, enrollment and troubleshooting guides, canonical `doc:policy:*` policies, NEAR onboarding, JSON-LD contexts, and concierge/discovery references (`doc:reference:a2a-onboarding-concierge`, `doc:reference:concierge-lobby-passport`, `doc:reference:a2a-registry-submission`). Deprecated aliases (`skills:skills`, `policy:terms|privacy|data-retention`) still resolve for one cycle but are omitted from the catalog. Use `/api/mcp/resource/{uri}` to fetch a resource.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of available MCP resources.","content":{"application/json":{"schema":{"type":"object","properties":{"resources":{"type":"array","items":{"type":"object","properties":{"uri":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"}},"required":["uri","name","type"]}},"nextCursor":{"type":"string","nullable":true},"requestId":{"type":"string"}},"required":["resources","requestId"]}}}},"400":{"description":"Invalid parameter - limit must be a numeric value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to list resources (`MCP_LIST_FAILED`). Generic message only; use `requestId` in logs for detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"listFailed":{"$ref":"#/components/examples/ErrorMcpListFailed"}}}}}},"operationId":"get_api_mcp_resources","x-access-level":"public"}},"/api/mcp/resource/{uri}":{"get":{"summary":"Retrieve a specific MCP resource by URI","description":"Fetches a resource by URI. **Start here:** `doc:skills` (canonical public/references/skills.md). Also: `openapi:swagger`, `doc:reference:*` published docs (including `doc:reference:a2a-onboarding-concierge`, `doc:reference:concierge-lobby-passport`, `doc:reference:a2a-registry-submission`), trimmed `config:default`, `readme:main`, `health:status`, `guide:*`, `doc:policy:*` / `policy:service-info`, `onboarding:near`, `jsonld:*`, `token:facial-categories`. Public `did:resolve:{tokenId}` returns `401 MCP_DID_RESOLVE_AUTH_REQUIRED` — use MCP `resolve_passport`, `GET /api/identity/token/{tokenId}/public`, or JWT `/.well-known/did/...`.","parameters":[{"name":"uri","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Requested resource returned.","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string"},"content":{},"requestId":{"type":"string"},"deprecated":{"type":"boolean","description":"Present when the URI is a deprecated alias (e.g. `skills:skills`, `policy:terms`)."},"canonicalUri":{"type":"string","description":"Canonical MCP resource URI to prefer when `deprecated` is true."}},"required":["type","content","requestId"]}}}},"401":{"description":"Authentication required for this resource (`MCP_DID_RESOLVE_AUTH_REQUIRED`). Public MCP `did:resolve:{tokenId}` is disabled; use MCP tool `resolve_passport`, `GET /api/identity/token/{tokenId}/public`, or JWT `/.well-known/did/...`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"didResolveAuthRequired":{"$ref":"#/components/examples/ErrorMcpDidResolveAuthRequired"}}}}},"404":{"description":"Resource not found (`MCP_RESOURCE_NOT_FOUND`). `details.uri` identifies the requested resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"notFound":{"$ref":"#/components/examples/ErrorMcpResourceNotFound"}}}}},"500":{"description":"Failed to get resource (`MCP_RESOURCE_FAILED`). Generic message only; use `requestId` in logs (not a 404).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"OpenAPI schema unavailable when fetching `openapi:swagger` (`MCP_OPENAPI_UNAVAILABLE`). Empty `{ paths: {} }` fallbacks are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"openapiUnavailable":{"$ref":"#/components/examples/ErrorMcpOpenapiUnavailable"}}}}}},"operationId":"get_api_mcp_resource_by_uri","x-access-level":"public"}},"/api/mcp/schema":{"get":{"summary":"Return the OpenAPI schema used by this server","description":"Returns the OpenAPI 3.0 schema directly with required fields: openapi, info, paths, and components.","responses":{"200":{"description":"OpenAPI 3.0 schema document.","content":{"application/json":{"schema":{"type":"object","required":["openapi","info","paths","components"],"properties":{"openapi":{"type":"string"},"info":{"type":"object"},"paths":{"type":"object"},"components":{"type":"object"}}}}}},"503":{"description":"OpenAPI schema unavailable (`MCP_OPENAPI_UNAVAILABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"openapiUnavailable":{"$ref":"#/components/examples/ErrorMcpOpenapiUnavailable"}}}}}},"operationId":"get_api_mcp_schema","x-access-level":"public"}},"/api/metrics":{"get":{"summary":"Get current performance metrics for this IDClawserver instance (admin only)","security":[{"BearerAuth":[],"RoditPermissionedRoutes":["/api/metrics"]}],"responses":{"200":{"description":"Aggregated request and session metrics, including request counts and active sessions.","content":{"application/json":{"schema":{"type":"object","properties":{"requestCount":{"type":"integer"},"errorCount":{"type":"integer"},"requestsPerMinute":{"type":"number"},"currentLoadLevel":{"type":"string"},"requests":{"type":"object","properties":{"total":{"type":"integer"},"errors":{"type":"integer"},"perMinute":{"type":"number"}},"required":["total","errors","perMinute"]},"sessions":{"type":"object","properties":{"active":{"type":"integer"},"active_count":{"type":"integer"},"total":{"type":"integer"}},"required":["active","active_count","total"]},"active":{"type":"integer"},"endpointMetrics":{"type":"object","additionalProperties":true},"metrics":{"type":"object","additionalProperties":true},"timestamp":{"type":"string","format":"date-time"},"requestId":{"type":"string"},"sessions.active":{"type":"integer"},"sessions.active_count":{"type":"integer"},"sessionDebug":{"type":"object","additionalProperties":true}},"required":["requestId","timestamp","requests","sessions","active"]}}}},"401":{"description":"Unauthorized - JWT token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to retrieve metrics.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_metrics","x-access-level":"privileged","description":"**Authorization:** Privileged — JWT plus `permissioned_routes.entities.methods[\"/api/metrics\"]` (OpenAPI scope `/api/metrics`). Enforced by `authenticate` + `authorize`."}},"/api/metrics/system":{"get":{"summary":"Get system resource metrics (CPU, memory, etc.) (admin only)","security":[{"BearerAuth":[],"RoditPermissionedRoutes":["/api/metrics/system"]}],"responses":{"200":{"description":"Current system metrics as reported by the performance service.","content":{"application/json":{"schema":{"type":"object","properties":{"metrics":{"type":"object","additionalProperties":true},"timestamp":{"type":"integer","description":"Unix timestamp in milliseconds."},"requestId":{"type":"string"}},"required":["metrics","timestamp","requestId"]}}}},"401":{"description":"Unauthorized - JWT token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to retrieve system metrics.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_metrics_system","x-access-level":"privileged","description":"**Authorization:** Privileged — JWT plus `permissioned_routes.entities.methods[\"/api/metrics/system\"]` (OpenAPI scope `/api/metrics/system`). Enforced by `authenticate` + `authorize`."}},"/api/metrics/reset":{"post":{"summary":"Reset performance metrics counters (admin only)","description":"Resets in-memory performance counters such as request counts and error counts. Caller must have appropriate admin permissions via RODiT permissioned_routes.","security":[{"BearerAuth":[],"RoditPermissionedRoutes":["/api/metrics/reset"]}],"responses":{"200":{"description":"Metrics reset successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"timestamp":{"type":"integer","description":"Unix timestamp in milliseconds."},"requestId":{"type":"string"}},"required":["message","timestamp","requestId"]}}}},"403":{"description":"Permission denied (admin permission required).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"UnsupportedMediaType - Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to reset metrics.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"post_api_metrics_reset","x-access-level":"privileged"}},"/api/metrics/debug":{"get":{"summary":"Debug endpoint for metrics subsystem (admin only)","description":"Returns a snapshot of performance metrics and client information for debugging.\n\n**Authorization:** Privileged — JWT plus `permissioned_routes.entities.methods[\"/api/metrics/debug\"]` (OpenAPI scope `/api/metrics/debug` on `RoditPermissionedRoutes`). Enforced by `authenticate` + `authorize`.","security":[{"BearerAuth":[],"RoditPermissionedRoutes":["/api/metrics/debug"]}],"responses":{"200":{"description":"Debug information for metrics subsystem.","content":{"application/json":{"schema":{"type":"object","properties":{"debug":{"type":"object","properties":{"hasRoditClient":{"type":"boolean"},"clientType":{"type":"string"},"hasPerformanceService":{"type":"boolean"},"performanceServiceType":{"type":"string"},"metricsSnapshot":{"type":"object","additionalProperties":true},"timestamp":{"type":"integer","description":"Unix timestamp in milliseconds."},"requestProcessingTime":{"type":"integer"}},"required":["hasRoditClient","clientType","hasPerformanceService","performanceServiceType","metricsSnapshot","timestamp","requestProcessingTime"]},"requestId":{"type":"string"}},"required":["debug","requestId"]}}}},"401":{"description":"Unauthorized - JWT token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to retrieve debug information.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_metrics_debug","x-access-level":"privileged"}},"/api/sessions/list_all":{"get":{"summary":"List all active sessions (admin)","description":"Returns a list of active sessions managed by the RODiT SessionManager. Intended for administrative tooling.\n\n**Authorization:** Privileged — JWT plus `permissioned_routes.entities.methods[\"/api/sessions/list_all\"]` (OpenAPI scope `/api/sessions/list_all` on `RoditPermissionedRoutes`). Enforced by `authenticate` + `authorize`.","security":[{"BearerAuth":[],"RoditPermissionedRoutes":["/api/sessions/list_all"]}],"responses":{"200":{"description":"List of active sessions with basic metadata.","content":{"application/json":{"schema":{"type":"object","properties":{"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"roditId":{"type":"string"},"ownerId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"lastAccessedAt":{"type":"string","format":"date-time"},"status":{"type":"string"}},"required":["id","roditId","ownerId","createdAt","expiresAt","lastAccessedAt","status"]}},"count":{"type":"integer"},"timestamp":{"type":"string","format":"date-time"}},"required":["sessions","count","timestamp"]}}}},"401":{"description":"Unauthorized - JWT token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to retrieve sessions (`SESSIONS_RETRIEVE_FAILED`). Generic message only; use `requestId` in logs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"retrieveFailed":{"$ref":"#/components/examples/ErrorSessionsRetrieveFailed"}}}}}},"operationId":"get_api_sessions_list_all","x-access-level":"privileged"}},"/api/sessions/cleanup":{"post":{"summary":"Cleanup expired sessions (admin)","description":"Triggers cleanup of expired sessions and returns cleanup statistics.\n\n**Authorization:** Privileged — JWT plus `permissioned_routes.entities.methods[\"/api/sessions/cleanup\"]` (OpenAPI scope `/api/sessions/cleanup` on `RoditPermissionedRoutes`). Enforced by `authenticate` + `authorize`.","security":[{"BearerAuth":[],"RoditPermissionedRoutes":["/api/sessions/cleanup"]}],"responses":{"200":{"description":"Session cleanup completed with statistics.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"},"stats":{"type":"object","properties":{"removedCount":{"type":"integer"},"activeSessions":{"type":"integer"},"totalSessions":{"type":"integer"},"cleanupResult":{"type":"object","additionalProperties":true}},"required":["removedCount","activeSessions","totalSessions","cleanupResult"]},"requestId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["success","message","stats","requestId","timestamp"]}}}},"415":{"description":"UnsupportedMediaType - Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Session cleanup failed (`SESSION_CLEANUP_FAILED`). Generic message only; use `requestId` in logs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"post_api_sessions_cleanup","x-access-level":"privileged"}},"/api/sessions/revoke":{"post":{"summary":"Revoke a specific session (owner)","description":"Terminates a specific session by ID. Caller must have permission to this endpoint in their RODiT permissioned_routes and must own the target session (session roditId/ownerId matches the caller).","security":[{"BearerAuth":[],"RoditPermissionedRoutes":["/api/sessions/revoke"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string","description":"Identifier of the session to revoke."},"reason":{"type":"string","description":"Optional human-readable reason for revocation."}},"required":["sessionId"]}}}},"responses":{"200":{"description":"Session terminated successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"sessionId":{"type":"string"},"reason":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["message","sessionId","reason","timestamp"]}}}},"400":{"description":"Missing or invalid sessionId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Caller is not the owner of the target session (`SESSION_NOT_OWNED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Session not found or already terminated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"UnsupportedMediaType - Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to terminate session (`SESSION_TERMINATION_FAILED`). Generic message only; `details.sessionId` may be present. Use `requestId` in logs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"post_api_sessions_revoke","x-access-level":"privileged"}},"/mcp":{"get":{"summary":"Streamable MCP transport endpoint","description":"Streamable HTTP endpoint for Model Context Protocol clients. This is the MCP transport path (served from `/mcp`), distinct from the REST discovery/resource endpoints under `/api/mcp/*`.\n\n**Accept Header Requirement**: GET requests MUST include `text/event-stream` in the Accept header (e.g., `Accept: text/event-stream` or `Accept: text/event-stream, application/json`). Requests without this will receive HTTP 406 Not Acceptable.","tags":["MCP"],"parameters":[{"name":"Accept","in":"header","required":true,"schema":{"type":"string","example":"text/event-stream"},"description":"MUST include text/event-stream as a supported content type per MCP Streamable HTTP specification."}],"responses":{"200":{"description":"MCP response payload. Shape depends on the MCP request method and transport exchange."},"400":{"description":"Missing or invalid MCP transport session. **GET and DELETE:** returns application `ErrorResponse` with code `MCP_TRANSPORT_SESSION_REQUIRED` (bare probe guard). POST without a session returns JSON-RPC `-32000` instead. See `GET /.well-known/mcp` for REST discovery paths.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"transportSessionRequired":{"$ref":"#/components/examples/ErrorMcpTransportSessionRequired"}}}}},"406":{"description":"Not Acceptable: Client must include text/event-stream in the Accept header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpJsonRpcErrorResponse"}}}},"415":{"description":"Unsupported Media Type: Invalid Content-Type header."},"426":{"description":"Upgrade Required: Protocol upgrade needed."},"500":{"description":"Server-side MCP transport error."}},"operationId":"get_mcp_streamable_transport","x-access-level":"public"},"post":{"summary":"Streamable MCP transport endpoint","description":"Streamable HTTP endpoint for Model Context Protocol clients. This is the MCP transport path (served from `/mcp`), distinct from the REST discovery/resource endpoints under `/api/mcp/*`.\n\n**Accept Header Requirement**: POST requests MUST include BOTH `application/json` AND `text/event-stream` in the Accept header (e.g., `Accept: application/json, text/event-stream`). Requests without both will receive HTTP 406 Not Acceptable.\n\n**Content-Type Requirement**: POST requests MUST use `Content-Type: application/json`.","tags":["MCP"],"parameters":[{"name":"Accept","in":"header","required":true,"schema":{"type":"string","example":"application/json, text/event-stream"},"description":"MUST include both application/json and text/event-stream as supported content types per MCP Streamable HTTP specification."},{"name":"Content-Type","in":"header","required":true,"schema":{"type":"string","example":"application/json"},"description":"MUST be application/json."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"MCP JSON-RPC request payload"}}}},"responses":{"200":{"description":"MCP response payload. Shape depends on the MCP request method and transport exchange."},"400":{"description":"Invalid MCP request payload or protocol framing. Returns JSON-RPC 2.0 error code `-32000` (not `MCP_TRANSPORT_SESSION_REQUIRED`). Typical cases: non-initialize POST without `Mcp-Session-Id`, malformed JSON-RPC body, or unsupported protocol version. A bare POST probe without a session is expected to receive `-32000`, not the GET curl-guard `ErrorResponse`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpJsonRpcErrorResponse"},"examples":{"missingSession":{"$ref":"#/components/examples/McpPostMissingSession"}}}}},"406":{"description":"Not Acceptable: Client must include both application/json and text/event-stream in the Accept header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpJsonRpcErrorResponse"}}}},"415":{"description":"Unsupported Media Type: Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpJsonRpcErrorResponse"}}}},"426":{"description":"Upgrade Required: Protocol upgrade needed."},"429":{"description":"Too many active MCP Streamable HTTP sessions (`MCP_MAX_SESSIONS`). Returns JSON-RPC 2.0 error on initialize when the in-memory session cap is reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpJsonRpcErrorResponse"}}}},"500":{"description":"Server-side MCP transport error."}},"operationId":"post_mcp_streamable_transport","x-access-level":"public"},"delete":{"summary":"Terminate MCP streamable HTTP session","description":"Closes an established MCP transport session. Requires a valid `Mcp-Session-Id` header. Without a session, returns the same application `ErrorResponse` as bare GET (`MCP_TRANSPORT_SESSION_REQUIRED`), not JSON-RPC `-32000`.","tags":["MCP"],"responses":{"200":{"description":"Session terminated or DELETE handled by the active transport."},"400":{"description":"Missing or invalid MCP transport session (`MCP_TRANSPORT_SESSION_REQUIRED`). Same curl/raw-probe guard as GET `/mcp`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"transportSessionRequired":{"$ref":"#/components/examples/ErrorMcpTransportSessionRequired"}}}}},"500":{"description":"Server-side MCP transport error."}},"operationId":"delete_mcp_streamable_transport","x-access-level":"public"}},"/api/identity/token/{tokenId}/public":{"get":{"summary":"Get redacted public passport profile","description":"Anonymous read of a sanitized passport profile after HOLA verification or for display/kiosk use. Returns display name, creature, avatar, contact URIs, facial encoding, `notBefore`, and `notAfter` — no JWT required. Does not expose full DN, tax, or address fields available on `GET /api/identity/token/{tokenId}/full`.\n\nAlternatively, set `includeProfile: true` on `POST /api/identity/verify` to receive the same profile inline. Verify/drop-take also return `notBefore`/`notAfter` on the handshake body itself (soft mint-age signal; does not affect `verified`).\n\n⚠️ **DISCLAIMER**: Metadata is self-declared by the passport holder.\n\n**Authentication:** Public endpoint (rate-limited at the nginx edge per `info.x-publicRateLimit`).","tags":["Identity"],"parameters":[{"name":"tokenId","in":"path","required":true,"schema":{"$ref":"#/components/schemas/TokenId"},"description":"IdentyClaw Passport ID."}],"responses":{"200":{"description":"Redacted public passport profile.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PublicPassportProfile"},{"type":"object","properties":{"face":{"type":"object","nullable":true,"description":"Facial description decoded from tokenId."},"notAfter":{"type":"string","nullable":true,"description":"Passport expiry from on-chain metadata."},"disclaimer":{"type":"string"},"requestId":{"type":"string"}}}]}}}},"404":{"description":"Passport not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Lookup failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_identity_token_tokenId_public","x-access-level":"public"}},"/api/identity/token/{tokenId}/full":{"get":{"summary":"Get full identity with DN and facial encoding","description":"Returns the combined Distinguished Name (DN), facial description, and peer-routable metadata for a token. Combines data from both /dn and /face endpoints in a single response. Includes `metadata.webhook_url` when set on-chain (OpenClaw gateway base URL for inbound notifications).\n\n⚠️ **DISCLAIMER**: The DN metadata including creature, name, contact URI, address, and other attributes are self-declared by the agent. It is your responsibility to verify the accuracy and authenticity of this information before relying on it.","tags":["Identity"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"tokenId","in":"path","required":true,"schema":{"type":"string"},"description":"IdentyClaw Passport ID."}],"responses":{"200":{"description":"Full identity with DN and facial encoding.","content":{"application/json":{"schema":{"type":"object","properties":{"tokenId":{"type":"string","description":"The requested token ID."},"dn":{"type":"object","nullable":true,"description":"Parsed Distinguished Name information.","properties":{"raw":{"type":"string","description":"Raw DN string from token metadata."},"nameNotSharedWithFamily":{"type":"string","nullable":true},"nameSharedWithFamily":{"type":"string","nullable":true},"displayName":{"type":"string","nullable":true},"contactUri":{"type":"string","nullable":true,"description":"Self-declared routing hint in format scheme:authority:identifier (e.g. email:example.com:user@example.com). Standard schemes: email, twitter, telegram, phone, linkedin, github. Extended schemes (community-documented): a2a, discord, fediverse, matrix, nostr, sms, irc, dns, onion, nfc, caldav. Full tables and registration guidance: MCP doc:reference:token-metadata § ContactURI."},"taxResidence":{"type":"string","nullable":true},"inceptDateTime":{"type":"string","nullable":true},"inceptPlace":{"type":"string","nullable":true},"taxPayerCode":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"creature":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true},"emojiUrl":{"type":"string","nullable":true},"allAttributes":{"type":"object","description":"All DN attributes as key-value pairs"}}},"face":{"type":"object","nullable":true,"description":"Facial description decoded from tokenId.","properties":{"checksumValid":{"type":"boolean"},"categories":{"type":"object","properties":{"skin_tones":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}},"regional_bone_structure":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}},"face_shape":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}},"age_related":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}},"eyes":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}},"eyebrow_style":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}},"overall_structure":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}},"nose":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}},"lips":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}},"skin_conditions":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}},"hair_color":{"type":"object","properties":{"value":{"type":"string"},"index":{"type":"integer"},"letter":{"type":"string"}}}}}}},"metadata":{"type":"object","nullable":true,"description":"Peer-routable token metadata for collaboration.","properties":{"webhook_url":{"type":"string","nullable":true,"description":"Webhook endpoint base URL for inbound notifications (for example OpenClaw gateway host)."}}},"requestId":{"type":"string","description":"Request correlation identifier."},"disclaimer":{"type":"string","description":"Warning that DN metadata including creature, name, contact URI, and other attributes are self-declared and should be verified"}}}}}},"404":{"description":"Token not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to retrieve identity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"get_api_identity_token_by_tokenId_full","x-access-level":"authenticated"}},"/.well-known/terms-of-service":{"get":{"summary":"Terms of Service","description":"Returns the Terms of Service document for the IdentyClaw API. Supports content negotiation: text/markdown (default) or text/html for browser viewing.","tags":["Policies"],"responses":{"200":{"description":"Terms of Service document.","content":{"text/markdown":{"schema":{"type":"string"}},"text/html":{"schema":{"type":"string"}}}}},"operationId":"get_well_known_terms_of_service","x-access-level":"public"}},"/.well-known/privacy-policy":{"get":{"summary":"Privacy Policy","description":"Returns the Privacy Policy document describing how user data is collected, used, and protected.","tags":["Policies"],"responses":{"200":{"description":"Privacy Policy document.","content":{"text/markdown":{"schema":{"type":"string"}},"text/html":{"schema":{"type":"string"}}}}},"operationId":"get_well_known_privacy_policy","x-access-level":"public"}},"/.well-known/data-retention":{"get":{"summary":"Data Retention Policy","description":"Returns the Data Retention Policy describing how long different types of data are stored and deletion procedures.","tags":["Policies"],"responses":{"200":{"description":"Data Retention Policy document.","content":{"text/markdown":{"schema":{"type":"string"}},"text/html":{"schema":{"type":"string"}}}}},"operationId":"get_well_known_data_retention","x-access-level":"public"}},"/.well-known/data-deletion":{"get":{"summary":"IdentyClaw app data deletion","description":"Public page for the IdentyClaw mobile app (operator Discernible IO). Names the app, lists the steps, and states what is deleted and what is kept. This API stores no account, so the page collects nothing. No sign-in. HTML by default; JSON when `Accept` is `application/json` and not `text/html`.","tags":["Policies"],"responses":{"200":{"description":"Deletion instructions for the IdentyClaw app.","content":{"text/html":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object"}}}}},"operationId":"get_well_known_data_deletion","x-access-level":"public"},"post":{"summary":"Deletion submissions are not accepted","description":"This API stores no account. A POST body is refused and is not recorded.","tags":["Policies"],"responses":{"405":{"description":"No deletion submission is accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}},"text/html":{"schema":{"type":"string"}}}}},"operationId":"post_well_known_data_deletion","x-access-level":"public"}},"/.well-known/why-identyclaw":{"get":{"summary":"Why IdentyClaw - Value Proposition","description":"Returns the value proposition document explaining how IdentyClaw enables AI-to-agent interaction, including endpoint benefits, use cases, security advantages, and complete agent communication flows.","tags":["Policies"],"responses":{"200":{"description":"Why IdentyClaw value proposition document.","content":{"text/markdown":{"schema":{"type":"string"}},"text/html":{"schema":{"type":"string"}}}}},"operationId":"get_well_known_why_identyclaw","x-access-level":"public"}},"/.well-known/did-method-rodit":{"get":{"summary":"did:rodit method specification","description":"Returns the canonical `did:rodit` DID method specification (Markdown). Unauthenticated. This is the public URL used for W3C DID Method Registry registration. Distinct from JWT-protected `/.well-known/did/*` resolution endpoints.","tags":["DID"],"responses":{"200":{"description":"did:rodit method specification.","content":{"text/markdown":{"schema":{"type":"string"}},"text/html":{"schema":{"type":"string"}}}}},"operationId":"get_well_known_did_method_rodit","x-access-level":"public"}},"/.well-known/did/rodit/{tokenId}":{"get":{"summary":"Resolve did:rodit DID document","description":"EXPERIMENTAL: Returns the DID document for a specific IdentyClaw Passport ID. This endpoint requires authentication because DID resolution is a paid service. This endpoint is experimental and may change in future versions.","tags":["DID"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"tokenId","in":"path","required":true,"schema":{"type":"string"},"description":"IdentyClaw Passport ID."}],"responses":{"200":{"description":"DID document for the requested token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DidDocument"}}}},"400":{"description":"Invalid DID or token identifier (`INVALID_DID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"invalidDid":{"$ref":"#/components/examples/ErrorInvalidDid"}}}}},"404":{"description":"Token not found (`DID_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"DID resolution failed (`DID_RESOLUTION_FAILED`). Generic message only; use `requestId` in logs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"resolutionFailed":{"$ref":"#/components/examples/ErrorDidResolutionFailed"}}}}}},"operationId":"get_well_known_did_rodit_by_tokenId","x-access-level":"authenticated"}},"/.well-known/did/resolve":{"get":{"summary":"Resolve DID string","description":"EXPERIMENTAL: Resolves either did:rodit or did:web identifiers into DID documents. This endpoint is experimental and may change in future versions.","tags":["DID"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"did","in":"query","required":true,"schema":{"type":"string"},"description":"DID string to resolve (did:rodit or did:web forms)."}],"responses":{"200":{"description":"Resolved DID document.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DidDocument"}}}},"400":{"description":"Missing query parameter `did` (`DID_REQUIRED`) or invalid DID string (`INVALID_DID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"didRequired":{"$ref":"#/components/examples/ErrorDidRequired"},"invalidDid":{"$ref":"#/components/examples/ErrorInvalidDid"}}}}},"404":{"description":"Token not found for the supplied DID (`DID_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"DID resolution failed (`DID_RESOLUTION_FAILED`). Generic message only; use `requestId` in logs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"resolutionFailed":{"$ref":"#/components/examples/ErrorDidResolutionFailed"}}}}}},"operationId":"get_well_known_did_resolve","x-access-level":"authenticated"}},"/.well-known/did/web/token/{tokenId}":{"get":{"summary":"Resolve did:web DID document","description":"Returns the did:web representation for a token hosted by this service, providing interoperability with web DID resolvers.","tags":["DID"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"tokenId","in":"path","required":true,"schema":{"type":"string"},"description":"IdentyClaw Passport ID."}],"responses":{"200":{"description":"did:web DID document for the requested token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DidDocument"}}}},"400":{"description":"Invalid DID or token identifier (`INVALID_DID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"invalidDid":{"$ref":"#/components/examples/ErrorInvalidDid"}}}}},"404":{"description":"Token not found (`DID_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"DID resolution failed (`DID_RESOLUTION_FAILED`). Generic message only; use `requestId` in logs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"resolutionFailed":{"$ref":"#/components/examples/ErrorDidResolutionFailed"}}}}}},"operationId":"get_well_known_did_web_token_by_tokenId","x-access-level":"authenticated"}},"/.well-known/did/web/token/{tokenId}/did.json":{"get":{"summary":"Resolve did:web JSON DID document","description":"EXPERIMENTAL: Returns the JSON DID document commonly served at did.json endpoints for did:web identifiers. This endpoint is experimental and may change in future versions.","tags":["DID"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"tokenId","in":"path","required":true,"schema":{"type":"string"},"description":"IdentyClaw Passport ID."}],"responses":{"200":{"description":"did:web DID document for the requested token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DidDocument"}}}},"400":{"description":"Invalid DID or token identifier (`INVALID_DID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"invalidDid":{"$ref":"#/components/examples/ErrorInvalidDid"}}}}},"404":{"description":"Token not found (`DID_NOT_FOUND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"DID resolution failed (`DID_RESOLUTION_FAILED`). Generic message only; use `requestId` in logs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"resolutionFailed":{"$ref":"#/components/examples/ErrorDidResolutionFailed"}}}}}},"operationId":"get_well_known_did_web_token_by_tokenId_did_json","x-access-level":"authenticated"}},"/api/testhola":{"post":{"summary":"Test HOLA message validation and respond with server HOLA","description":"Self-testing endpoint to validate YOUR HOLA message generation before sending to peers. Use `/api/testhola` for self-testing; use `/api/identity/verify` for peer verification.\n\n**Authentication Scope:** `/api/testhola` is JWT-protected as an API endpoint, but HOLA validation outcomes are based on the submitted HOLA fields and cryptographic checks (not caller-token equality).\n\n**When to Use:**\n- `/api/testhola`: Self-testing to validate YOUR HOLA generation before sending to peers\n- `/api/identity/verify`: Peer verification to verify HOLA received from ANOTHER agent\n\nValidation is fail-closed and covers: format sanity, checksum, timestamp freshness, nonce replay, sender token existence, token expiration (`not_after`), blockchain/public-key availability, and Ed25519 signature verification.\n\nSupports both formats:\n- Standard: HOLA/<recipient>/<tokenId>/<ISO8601-timestamp>/<noncets-hex>/API.IDENTYCLAW.COM/<base32-signature>/<checksum>\n- Subagent: HOLA/<recipient>/<delegateID>/<issuer_tokenId>/<publicKey-base32>/<ISO8601-timestamp>/<noncets-hex>/API.IDENTYCLAW.COM/<base32-signature>/<checksum>\n\nOn rejection, the response includes machine-readable error details under `error.details` with:\n- `stage`: validation stage that failed\n- `reasonCode`: stable failure classifier for automated clients/tests\n- `hint`: remediation hint for common failure modes\n\nEarly shape-lint stages (before full parsing) may set `reasonCode` to: `payload_is_json_object`, `unix_millis_timestamp`, `signature_not_base32`, `placeholder_signature`. Each error includes `details.documentation` (relative reference paths) and `details.example` (a corrected snippet for the offending field) so agents can self-correct without external lookup.\n\n**Webhook Emission Behavior:** The server emits `/api/testhola` webhook events to the authenticated peer's configured webhook URL at `/hooks/wake` and `/hooks/agent` only when `WEBHOOK_TEST_ENABLED=true`.\n\nEmitted events:\n- `testhola_validation_failed`\n- `testhola_response_failed`\n- `testhola_validation_success`","tags":["Testing"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hola":{"type":"string","description":"Request field for the HOLA message to validate. Supports standard and subagent formats. Standard: HOLA/<recipient>/<tokenId>/<ISO8601-timestamp>/<noncets-hex>/API.IDENTYCLAW.COM/<base32-signature>/<checksum>. Subagent: HOLA/<recipient>/<delegateID>/<issuer_tokenId>/<publicKey-base32>/<ISO8601-timestamp>/<noncets-hex>/API.IDENTYCLAW.COM/<base32-signature>/<checksum>. Base32 variant is RFC 4648 Base32 (A-Z2-7, uppercase canonical form, optional '=' padding on input). Checksum is one letter from ABCDEFGHJKMNPQRSTUVWXYZ (mod 23 over canonical prefix + signature + '/').","example":"HOLA/MUNDO/pkcnjdbdefcp/2026-04-14T14:00:00.000Z/4F9A3C7E2D1B9A4C8E7F6A5B4C3D2E1F/API.IDENTYCLAW.COM/MEQW4YLTORUW63THMV2GC3DBNVRWQ/R"},"expectedRecipient":{"type":"string","pattern":"^[A-Za-z]{1,12}$","maxLength":12,"description":"Optional destinatary pin: 1–12 letters, no numbers or special characters. When it matches the HOLA recipient, the RECIPIENT_MISMATCH warning is suppressed."}},"required":["hola"]}}}},"responses":{"200":{"description":"HOLA message is valid. Server responds with its own valid HOLA.","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","description":"Whether the client's HOLA message is valid.","example":true},"peerTokenId":{"type":"string","description":"The token ID from the client's HOLA message."},"destinatary":{"type":"string","description":"Recipient extracted from the client's HOLA message."},"peerVerified":{"type":"boolean","description":"Whether the client's identity was verified.","example":true},"hola":{"type":"string","description":"The server's valid HOLA response signed with roditclient's key pair."},"serverTokenId":{"type":"string","description":"The server's token ID used in the response HOLA."},"serverDestinatary":{"type":"string","description":"Recipient used by the server in its response HOLA."},"serverTimestamp":{"type":"string","format":"date-time","description":"The ISO 8601 timestamp used in the server's HOLA response."},"checks":{"type":"object","properties":{"formatValid":{"type":"boolean"},"checksumValid":{"type":"boolean"},"timestampValid":{"type":"boolean"},"timestampFresh":{"type":"boolean"},"noncetsValid":{"type":"boolean"},"nonceReplaySafe":{"type":"boolean"},"tokenExists":{"type":"boolean"},"tokenActive":{"type":"boolean"},"signatureValid":{"type":"boolean"},"publicKeyAvailable":{"type":"boolean"}}},"requestId":{"type":"string","description":"Request correlation identifier."},"warnings":{"type":"array","description":"Optional warning entries for suspicious but non-fatal conditions such as recipient mismatch.","items":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}}}}}}},"400":{"description":"HOLA message is invalid, stale, or signature verification failed. Returns detailed reason.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"checksumInvalid":{"summary":"Checksum invalid","value":{"error":{"code":"HOLA_VALIDATION_FAILED","message":"Invalid checksum: expected A, got B","details":{"valid":false,"stage":"format_checksum_and_payload_validation","reasonCode":"checksum_invalid","hint":"Recompute checksum: sum UTF-16 code units over canonical uppercase prefix through signature plus trailing '/', mod 23, index into ABCDEFGHJKMNPQRSTUVWXYZ.","checks":{"formatValid":true,"checksumValid":false,"timestampValid":true,"noncetsValid":true}}},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-04-15T07:47:00.000Z"}},"nonceReplay":{"summary":"Nonce replay detected","value":{"error":{"code":"HOLA_VALIDATION_FAILED","message":"Nonce replay detected within allowed timestamp window","details":{"valid":false,"stage":"nonce_replay_validation","reasonCode":"nonce_replay","hint":"Request a new nonce and sign a new hola message.","checks":{"formatValid":true,"checksumValid":true,"timestampValid":true,"noncetsValid":true,"nonceReplaySafe":false}}},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-04-15T07:47:00.000Z"}},"timestampStale":{"summary":"Timestamp stale/future","value":{"error":{"code":"HOLA_TIMESTAMP_INVALID","message":"Timestamp stale or future: age is 601s, max allowed is 300s","details":{"valid":false,"stage":"timestamp_freshness_validation","reasonCode":"timestamp_stale_or_future","hint":"Generate a fresh hola using a recent timestamp within the allowed age window.","ageSeconds":601,"maxAgeSeconds":300}},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-04-15T07:47:00.000Z"}},"signaturePublicKeyUnavailable":{"summary":"Public key unavailable","value":{"error":{"code":"HOLA_SIGNATURE_INVALID","message":"Public key unavailable or invalid length","details":{"valid":false,"stage":"signature_verification","reasonCode":"public_key_unavailable","hint":"Verify signer public key encoding, parent token status, and exact signed payload.","tokenExists":true,"tokenActive":true,"tokenExpired":false,"publicKeyAvailable":false}},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-04-15T07:47:00.000Z"}},"signatureMismatch":{"summary":"Signature mismatch","value":{"error":{"code":"HOLA_SIGNATURE_INVALID","message":"Signature verification failed","details":{"valid":false,"stage":"signature_verification","reasonCode":"signature_mismatch","hint":"Verify signer public key encoding, parent token status, and exact signed payload.","tokenExists":true,"tokenActive":true,"tokenExpired":false,"publicKeyAvailable":true}},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-04-15T07:47:00.000Z"}},"tokenExpired":{"summary":"Sender token expired","value":{"error":{"code":"HOLA_SIGNATURE_INVALID","message":"Token expired (not_after)","details":{"valid":false,"stage":"signature_verification","reasonCode":"token_expired","hint":"Verify signer public key encoding, parent token status, and exact signed payload.","tokenExists":true,"tokenActive":false,"tokenExpired":true,"publicKeyAvailable":false}},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-04-15T07:47:00.000Z"}},"subagentPublicKeyDecodeFailed":{"summary":"Subagent public key decode failed","value":{"error":{"code":"HOLA_SIGNATURE_INVALID","message":"Public key unavailable or invalid length: Invalid base32 input","details":{"valid":false,"stage":"signature_verification","reasonCode":"public_key_decode_failed","hint":"Verify signer public key encoding, parent token status, and exact signed payload.","isSubagentFormat":true,"delegateId":"researcher-sub-001","issuerTokenId":"PKCNJDBDEFCP","tokenExists":true,"tokenActive":true,"tokenExpired":false,"publicKeyAvailable":false}},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-04-15T07:47:00.000Z"}}}}}},"500":{"description":"Server error or unable to generate HOLA response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"NEAR RPC call failed before the token was checked (for example the RPC is out of credits). error.details is omitted; clients should display error.message. This is not token_missing, public_key_error, or public_key_unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"SERVICE_UNAVAILABLE","message":"Chain lookup failed because the NEAR RPC is out of credits. The RODiT token was not checked."},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-10-06T12:00:00.000Z"}}}}},"operationId":"post_api_testhola","x-access-level":"authenticated"}},"/api/hola/drop":{"put":{"summary":"Deposit a HOLA line for a peer Passport (drop box)","description":"Deposit a signed HOLA addressed to another Passport holder into a short-lived server-side drop slot. The recipient later calls `POST /api/hola/drop/take` to atomically take and verify it.\n\n**Storage model (documented choice):** one slot per ordered pair **`(recipient, signer)`**. Concurrent deposits from different signers to the same addressee do **not** overwrite each other. A re-deposit from the same signer to the same recipient overwrites that pair only (latest from that peer).\n\n**TTL:** expires at `min(HOLA timestamp + maxAgeMs, deposit time + maxAgeMs)` with default `maxAgeMs` 300000 (5 minutes). Backend uses Redis-compatible keys `hola:drop:{recipient}:{signer}` + PX TTL and index set `hola:drop:to:{recipient}` (in-memory today).\n\n**Fail-closed checks (no signature/nonce consumption):**\n- Same early format/checksum validation as `/api/testhola` / `/api/identity/verify`\n- `tokenId` (signer) must equal the authenticated JWT subject\n- `recipient` must be a 12-letter Passport ID (**MUNDO rejected**)\n\n**Must NOT** run the full verify pipeline or record nonce replay on deposit — that happens only on take-and-verify.\n\n**Authentication:** Bearer JWT (RODiT login) required.","tags":["Identity"],"security":[{"BearerAuth":[]}],"operationId":"put_api_hola_drop","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["hola"],"properties":{"hola":{"type":"string","maxLength":512,"description":"Full HOLA line addressed to a 12-letter peer Passport ID (not MUNDO)."}}}}}},"responses":{"200":{"description":"Deposit accepted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"expiresAt":{"type":"string","format":"date-time"},"requestId":{"type":"string"}}}}}},"400":{"description":"Validation failed (format, MUNDO recipient, expired at deposit).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid JWT.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Signer tokenId ≠ authenticated caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-access-level":"authenticated"},"post":{"summary":"Deposit a HOLA line for a peer Passport (drop box)","description":"Alias of `PUT /api/hola/drop` for clients that prefer POST. Same body, auth, and semantics.","tags":["Identity"],"security":[{"BearerAuth":[]}],"operationId":"post_api_hola_drop","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["hola"],"properties":{"hola":{"type":"string","maxLength":512}}}}}},"responses":{"200":{"description":"Deposit accepted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"expiresAt":{"type":"string","format":"date-time"},"requestId":{"type":"string"}}}}}},"400":{"description":"Validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid JWT.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Signer mismatch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-access-level":"authenticated"}},"/api/hola/drop/take":{"post":{"summary":"Take and verify the HOLA drop addressed to the caller","description":"Atomically take one HOLA stored for `recipient ==` authenticated caller token ID (slot key is `(recipient, signer)`), then run the **same** verification pipeline as `POST /api/identity/verify`.\n\n**Behavior:**\n1. Load one drop for caller Passport ID (if `expectedSigner`/`peerTokenId` is set, that pair; otherwise the newest deposit among signers)\n2. **Delete immediately** (even if verify will fail) — Redis-compatible GETDEL\n3. If none/expired: **404** `HOLA_DROP_EMPTY`\n4. Re-check HOLA recipient equals caller; otherwise treat as empty (do not verify mis-addressed lines)\n5. Verify (timestamp, nonce replay, token active, Ed25519, optional profile)\n6. Return the same response shape as `/api/identity/verify`, plus optional `hola` echo\n\nNo client-supplied `hola` in the body. Soft/broadcast MUNDO HOLAs continue to use audio or verify-site QR, not this drop.\n\n**Authentication:** Bearer JWT required.","tags":["Identity"],"security":[{"BearerAuth":[]}],"operationId":"post_api_hola_drop_take","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"expectedRecipient":{"type":"string","pattern":"^[A-Za-z]{1,12}$","maxLength":12,"description":"Optional; defaults to the caller Passport ID pin."},"includeProfile":{"type":"boolean","description":"When true, include PublicPassportProfile for peerTokenId (same as identity/verify)."},"constraints":{"type":"object","properties":{"maxAgeMs":{"type":"integer","description":"Maximum HOLA age in milliseconds (default 300000)."}}},"expectedSigner":{"type":"string","pattern":"^[A-Za-z]{12}$","minLength":12,"maxLength":12,"description":"Optional peer Passport ID (signer) whose deposit to take. Alias: peerTokenId. When omitted, the newest deposit addressed to the caller is taken."},"peerTokenId":{"type":"string","pattern":"^[A-Za-z]{12}$","minLength":12,"maxLength":12,"description":"Alias for expectedSigner."}}}}}},"responses":{"200":{"description":"Take-and-verify result (same shape as POST /api/identity/verify, including informational avatarContentHash, plus optional hola echo). avatarContentHash does not affect verified.","content":{"application/json":{"schema":{"type":"object","properties":{"verified":{"type":"boolean","description":"True when all checks pass and the peer hola is accepted."},"peerTokenId":{"type":"string","description":"token_id extracted from the peer hola. For subagent format, this is the issuerTokenId."},"destinatary":{"type":"string","description":"Recipient field extracted from the HOLA message."},"profile":{"allOf":[{"$ref":"#/components/schemas/PublicPassportProfile"}],"description":"Sanitized public passport profile when includeProfile is true."},"notBefore":{"type":"string","nullable":true,"description":"On-chain metadata.not_before from the same token load used for expiry/key checks. Soft mint-age signal for UI; never an input to verified. Prefer this over a follow-up profile/RPC fetch after handshake."},"notAfter":{"type":"string","nullable":true,"description":"On-chain metadata.not_after from the same token load. Soft display of expiry window; hard expiry still sets checks.tokenActive / token_expired. Does not itself flip verified beyond that existing check."},"isSubagentFormat":{"type":"boolean","description":"True when the HOLA message uses the subagent format (11 fields) instead of standard format (8 fields). Only present when true."},"delegateId":{"type":"string","description":"The delegateID from the subagent HOLA message. Only present when isSubagentFormat is true."},"issuerTokenId":{"type":"string","description":"The parent agent's token ID who authorized this subagent. Only present when isSubagentFormat is true."},"checks":{"type":"object","description":"Detailed boolean flags indicating which verification stages passed.","properties":{"tokenExists":{"type":"boolean","description":"True when the peer token exists on NEAR."},"tokenActive":{"type":"boolean","description":"True when the token is considered active for verification. Currently false when token is expired based on metadata.not_after."},"timestampFresh":{"type":"boolean","description":"True when the timestamp is within the accepted age window."},"nonceReplaySafe":{"type":"boolean","description":"True when the nonce has not already been accepted for the same sender token within the active timestamp window."},"signatureValid":{"type":"boolean","description":"True when the Ed25519 signature matches the canonical message."},"checksumValid":{"type":"boolean","description":"True when the checksum matches the canonical hola payload. Always true on HTTP 200; checksum mismatch returns HTTP 400 with reasonCode checksum_invalid before this response."}},"required":["tokenExists","tokenActive","timestampFresh","nonceReplaySafe","signatureValid","checksumValid"]},"failureReasons":{"type":"array","description":"List of machine-readable codes describing why peer verification failed (timestamp, nonce, token, signature). Empty when verified is true. Format and checksum faults are not listed here; they fail closed with HTTP 400.","items":{"type":"string","enum":["timestamp_stale_or_future","nonce_replay","token_missing","token_expired","signature_invalid","public_key_unavailable","public_key_error","subagent_public_key_invalid_length","subagent_public_key_decode_failed"]}},"failureDetails":{"type":"array","description":"Human-readable descriptions and actionable hints for each failureReasons entry.","items":{"type":"object","properties":{"reasonCode":{"type":"string"},"description":{"type":"string"},"hint":{"type":"string","description":"Prescriptive next step for resolving this failure reason."}},"required":["reasonCode","description","hint"]}},"nonceReplayDetails":{"type":"object","description":"Present when failureReasons includes nonce_replay. Describes the HOLA timestamp and freshness window used for replay detection.","properties":{"holaTimestamp":{"type":"string","description":"ISO-8601 timestamp parsed from the HOLA line."},"maxAgeMs":{"type":"integer","description":"Maximum HOLA age in milliseconds used for freshness and replay checks."},"firstSeenAt":{"type":"string","description":"ISO-8601 time when this nonce was first consumed for this signer, if known."}},"required":["holaTimestamp","maxAgeMs"]},"signatureVerificationImplemented":{"type":"boolean","description":"Signals that signature verification is active for this server implementation."},"avatarContentHash":{"$ref":"#/components/schemas/AvatarContentHash"},"warnings":{"type":"array","description":"Optional warning entries for suspicious but non-fatal conditions such as recipient mismatch.","items":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"requestId":{"type":"string","description":"Request correlation identifier."},"hola":{"type":"string","description":"Echo of the taken HOLA line (diagnostics)."}}}}}},"400":{"description":"Invalid constraints or early HOLA validation failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid JWT.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No drop for this recipient (empty, expired, or discarded).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"HOLA_DROP_EMPTY","message":"No HOLA drop is currently stored for this recipient"},"requestId":"01HX7HBE4X7AMP9Q4MP2YE2C41","timestamp":"2026-05-21T07:50:53.322Z"}}}}},"x-access-level":"authenticated"}}}},"requestId":"af41899449c78a9a1dfa181761cad20c"}