{"type":"text/markdown","content":"# API Login Authentication\n\nComplete guide to authenticating with the IdentyClaw API server to obtain JWT tokens.\n\n## Table of Contents\n\n- [Quick Start: Login Pattern](#quick-start-login-pattern)\n- [Overview](#overview)\n- [Vocabulary (API login)](#vocabulary-api-login)\n- [When to Use](#when-to-use)\n- [Understanding ID vs Account](#understanding-id-vs-account)\n- [Key Rotation](#key-rotation)\n- [Authentication Flow](#authentication-flow)\n- [Verify the API server (MITM protection)](#verify-the-api-server-mitm-protection)\n- [Step 1: Get Consistent Timestamp](#step-1-get-consistent-timestamp)\n- [Step 2: Sign the Message](#step-2-sign-the-message)\n- [Step 3: POST /api/login](#step-3-post-apilogin)\n- [Complete Pattern Example](#complete-pattern-example)\n- [Step 4: Use JWT Token](#step-4-use-jwt-token)\n- [Agent harnesses (OpenClaw, Cursor, shell agents)](#agent-harnesses-openclaw-cursor-shell-agents)\n- [Verify Your Login Works](#verify-your-login-works)\n- [Machine-readable login errors](#machine-readable-login-errors)\n- [Common Pitfalls](#common-pitfalls)\n- [Next Steps](#next-steps)\n\n## Quick Start: Login Pattern\n\n**Reference (IdentyClaw)** — API `https://api.identyclaw.com`. Proven wire format (also exercised by the deployment test suite in **clienttest-idc** via direct `fetch`/`curl`): challenge → sign locally → `POST /api/login` → Bearer JWT on protected routes. **No client-side NEAR RPC is required** for this path—you only sign with your NEAR/Passport key and call HTTP.\n\n### Complete Flow Pattern (curl)\n\n```bash\n# 1. One-time challenge (use both fields from the same response)\ncurl -sS https://api.identyclaw.com/api/login/timestamp\n\n# 2. Sign UTF-8 bytes of: <accountid or roditid> + <timestamp_iso> (no separator)\n#    → base64url_signature (see Steps 1–2 below)\n\n# 3. Exchange signature for JWT (send exactly one timestamp field)\ncurl -sS -X POST https://api.identyclaw.com/api/login \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"accountid\":\"<64-char-hex>\",\"timestamp\":1776622758,\"base64url_signature\":\"<sig>\"}'\n\n# 4. Protected call\nJWT=\"<jwt_token from step 3>\"\ncurl -sS https://api.identyclaw.com/api/me/identity \\\n  -H \"Authorization: Bearer ${JWT}\"\n```\n\n**⚠️ CRITICAL:** Fetch a fresh `GET /api/login/timestamp` pair immediately before each login attempt; use it once, then discard.\n\n**Optional — `@rodit/rodit-auth-be`:** `RoditClient.login_server()` also validates the **API server's** on-chain Passport before returning a JWT. That adds **NEAR RPC** load and dependency on RPC bandwidth/latency on the agent host. Use it when MITM protection outweighs that cost; otherwise curl login above is sufficient for most agents. See [Verify the API server (MITM protection)](#verify-the-api-server-mitm-protection).\n\n**Key Points**:\n- ✓ Fetch **fresh timestamp** from API for each login attempt\n- ✓ Sign with **timestamp_iso** (ISO string from the API response)\n- ✓ Send exactly one timestamp field in POST payload: **timestamp** (Unix seconds) or **timestamp_iso**\n- ✓ Concatenate **accountid + timestamp_iso** with no separator for signing\n- ✓ Encode signature as **base64url** (not standard base64)\n- ✓ Response field is **jwt_token** (not `token`)\n- ✓ JWT expires in 1 hour - request new token when needed\n- ✓ Treat each timestamp pair as **single-use** for one login attempt only\n\n**Note on Login Methods**: The API supports two login identifiers:\n- **accountid**: 64-character lowercase hex NEAR implicit account ID (for enrollment)\n- **roditid**: 12-letter on-chain token_id (for direct IdentyClaw Passport login)\nChoose the appropriate identifier based on your use case.\n\n## Timestamp Handling Checklist\n\n**Do**:\n- Fetch `GET /api/login/timestamp` immediately before signing\n- Use `timestamp_iso` for **signing** the message\n- Send exactly one timestamp field in the POST payload: `timestamp` (Unix seconds) or `timestamp_iso`\n- On any failed `POST /api/login`, fetch a **new** timestamp pair before retry\n\n**Implementation checklist**:\n- Fetch a new timestamp pair immediately before signing each attempt\n- Keep timestamp pairs in request-local scope and discard them after the attempt\n- Use `timestamp` and `timestamp_iso` from the same response object\n- Send `timestamp` in the POST payload (or omit it and use server default)\n\n## Overview\n\nAPI Login is the authentication mechanism for accessing protected IdentyClaw API endpoints. It provides a JWT token that authorizes your requests to the server.\n\n## Vocabulary (API login)\n\nThis guide uses the following terms consistently. **HTTP request and response fields are unchanged** — see [API Reference](api-reference.md) and OpenAPI (for example `accountid`, `timestamp`, `base64url_signature`, `jwt_token` on `POST /api/login`).\n\n| Term | Meaning |\n| --- | --- |\n| **Login challenge pair** | `timestamp` and `timestamp_iso` from `GET /api/login/timestamp`, used together once per login attempt |\n| **Login signing payload** | Concatenation `accountid` + `timestamp_iso` (no separator), UTF-8 bytes you sign for login |\n| **base64url login signature** | Ed25519 signature over the login signing payload, encoded for `base64url_signature` |\n| **JWT** / **API bearer token** | Session credential returned by `POST /api/login` (`jwt_token`) |\n| **API error code (login lane)** | Machine-readable classifier for JWT login failures — see [Machine-readable login errors](#machine-readable-login-errors) (`error.code`; legacy shape may use top-level string `error`) |\n\n## When to Use\n\nUse API Login when you need to:\n\n- Access protected API endpoints\n- Get your own identity (`/api/me/identity`)\n- Request nonces for HOLA protocol (`GET /api/holanonce16ts` → JSON `noncetsHex`, `timestamp` — see [holanonce-api.md](holanonce-api.md); not `timestamp_iso`)\n- Self-test your outbound HOLA (`POST /api/testhola` — JWT required)\n\nPeer HOLA verification (`POST /api/identity/verify`) is **public** — no JWT required. See [skills.md](skills.md).\n\nFor most protected workflows, API login is your **first step** when you need a JWT.\n\nFor inter-agent HOLA lines and tooling, see [HOLA Protocol](hola-agent-authentication.md).\n\n## Understanding ID vs Account\n\nThink of your **IdentyClaw Passport as your passport** and your **NEAR account as your wallet**:\n\n- **Your Passport (Passport ID)**: Your unique 12-letter identity (e.g., `bkbvehbdcrgm`) that proves who you are. This is what you show to others. IdentyClaw Passports are implemented as on-chain credentials on the NEAR blockchain.\n- **Your Wallet (NEAR Account)**: The account that owns and controls your passport (e.g., `alice.near`). This is what holds your private keys and signs on your behalf.\n\nWhen you authenticate, you're using your wallet's private key to prove you own your passport.\n\n## Key Rotation\n\nKey rotation is seamless and requires **no coordination between clients and servers**. IdentyClaw does not maintain a separate key registry — ownership is determined on-chain. **You choose the rotation frequency**; there is no platform-mandated schedule.\n\n**To rotate signing keys:**\n\n1. Create a new NEAR wallet.\n2. Fund the new wallet on-chain.\n3. Transfer your IdentyClaw Passport with **`near-cli-rs`**, calling the contract method **`rodit_transfer`** (this is not a standard NEAR `nft_transfer`).\n4. Point login and HOLA tooling at the new wallet credentials.\n\nYour Passport ID (12-letter token id) is unchanged. After the transfer, the API and peers accept the new key automatically because they verify current on-chain ownership.\n\nSee [key-rotation.md](key-rotation.md) for the full security guidance, `near-cli-rs` command, and contract interface.\n\n## Authentication Flow\n\n```\n1. GET /api/login/timestamp → {timestamp, timestamp_iso}\n2. Sign message: accountid + timestamp_iso\n3. POST /api/login → {jwt_token: \"eyJhbGc...\"}\n4. Use JWT in Authorization header for protected endpoints\n```\n\n## Verify the API Server (MITM Protection)\n\n### The threat\n\nTLS protects the channel, but it does **not** by itself prove that responses came from the **real** IdentyClaw API. An attacker could:\n\n- **Man-in-the-middle** your connection and return forged JWTs or API payloads\n- Run a **look-alike server** at a similar hostname that accepts valid login signatures but serves untrustworthy results (for example on `POST /api/identity/verify`)\n\nIf you log in with raw `fetch` or `curl` and accept any `jwt_token` from the response, you have proved **your** Passport to whatever endpoint answered—you have **not** proved that endpoint **is** IdentyClaw.\n\n### Default for agents: curl / HTTP login\n\nMost agents should use the [Quick Start](#quick-start-login-pattern) curl flow:\n\n- You authenticate **to** the server (login signature).\n- You do **not** need a working NEAR RPC on the agent machine for login.\n- Mitigations without RPC: pin the API hostname (`https://api.identyclaw.com` or your deployment URL), use TLS, and cross-check high-value results (canonical `tokenId` on official channels—see [finding-agents.md](finding-agents.md)).\n\nThis matches how **clienttest-idc** exercises public and many authenticated endpoints with direct HTTP (`fetch`/`curl`); the SDK is used there mainly where the test constitution requires it, not for every agent integration.\n\n### Optional: client-side server Passport validation (`@rodit/rodit-auth-be`)\n\nLogin can be **mutual**: after `POST /api/login`, a client may validate the returned JWT against the **server's** on-chain Passport before trusting it.\n\n| Direction | Who is verified | curl login | SDK `login_server()` |\n| --- | --- | --- | --- |\n| **Server → client** | Your Passport | ✓ (server validates your signature) | ✓ |\n| **Client → server** | The API server's Passport | ✗ (you trust TLS + hostname) | ✓ (loads server Passport via **NEAR RPC**) |\n\n**Tradeoff:** `RoditClient.login_server()` (and `client.request()`) call NEAR RPC to load and validate the server Passport. Agents on constrained networks, rate-limited RPC, or minimal shells may find that **slower or less reliable than curl alone**. Use the SDK when you explicitly need client→server Passport validation and can provision RPC with enough bandwidth.\n\n```javascript\nconst { RoditClient } = require(\"@rodit/rodit-auth-be\");\n\nconst client = await RoditClient.create(\"client\");\nconst { jwt_token } = await client.login_server();\n// Throws if server JWT / on-chain Passport validation fails\n```\n\n`RoditClient.login_server()` POSTs your signed credentials, then **before returning a session**:\n\n1. Decodes the JWT to read the server's on-chain Passport id (`rodit_id` claim)\n2. Loads that server Passport from NEAR (**RPC required**)\n3. Validates the JWT cryptographically against that Passport (signature, expiry, session state, family, and trust rules)\n\nFull policy reference (family, partner vs peer, controlling address, HOLA vs login lanes): [`identity-verification-policy.md`](identity-verification-policy.md).\n\nIf the endpoint is an impostor or misconfigured proxy, validation fails with explicit **`[SERVER REJECTED]`** errors, including:\n\n| Error code | Meaning |\n| --- | --- |\n| `SERVER_RODIT_FAMILY_MISMATCH` | Server Passport is not in the expected IdentyClaw family |\n| `SERVER_RODIT_NOT_LIVE` | Server Passport is expired or not yet active |\n| `SERVER_RODIT_REVOKED` | Server Passport has been revoked |\n| `SERVER_SMART_CONTRACT_NOT_TRUSTED` | Server Passport was not issued by a trusted contract |\n| `SERVER_TOKEN_IDENTITY_MISMATCH` | JWT server identity does not match the on-chain record |\n\nFor outbound webhooks from IdentyClaw to your agent, webhook signature verification (SDK `verifyWebhookSignature()` / `getWebhookHandler()`) is separate from login—only needed if you accept server-pushed events.\n\nInstall (optional): `npm install @rodit/rodit-auth-be@9.10.6`. Wire-format login in this guide does **not** require the package.\n\n## Step 1: Get Consistent Timestamp\n\n**Endpoint**: `GET /api/login/timestamp` (public, no auth required)\n\n```bash\ncurl https://api.identyclaw.com/api/login/timestamp\n```\n\n**Response Pattern** (values shown are illustrative only):\n```json\n{\n  \"timestamp\": 1776622758,\n  \"timestamp_iso\": \"2026-04-19T18:19:18.000Z\",\n  \"requestId\": \"01HQXYZ...\"\n}\n```\n\n**⚠️ CRITICAL - Timestamp Consistency**:\n- `timestamp` and `timestamp_iso` are generated from the **same moment**\n- Use **timestamp_iso** when signing your message (Step 2)\n- Send exactly one timestamp field in the login payload (Step 3): `timestamp` or `timestamp_iso`\n- Fetch the timestamp pair from this endpoint and sign exactly what the endpoint returns\n- Timestamps are valid for a short window (~5 minutes)\n- Treat each pair as a one-time challenge for one login attempt\n\n## Step 2: Sign the Message\n\n**Message to sign** (UTF-8 bytes):\n```\naccountid + timestamp_iso\n```\n\n**Reference (IdentyClaw) — wire format** (use your fresh values from Step 1):\n- Account ID: `43d3c5b5e77a46b52933bc7a8b79b06f16dd4ca3cfbacd0e6fede0e7e01782ac`\n- timestamp_iso: `2026-04-19T18:19:18.000Z` (from Step 1 response)\n- **Message**: `43d3c5b5e77a46b52933bc7a8b79b06f16dd4ca3cfbacd0e6fede0e7e01782ac2026-04-19T18:19:18.000Z` (no separators, literal concatenation)\n\n⚠️ Use the **fresh timestamp_iso** from Step 1, not values from this example.\n\n**Signing Steps**:\n\n1. Extract your NEAR private key from credentials file\n2. Decode from base58 to get 64-byte keypair\n3. Extract first 32 bytes (secret key)\n4. Sign message with Ed25519\n5. Encode signature as base64url (URL-safe base64: `-` and `_` instead of `+` and `/`, no padding `=`)\n\n**Reference (IdentyClaw) — JavaScript wire format** (use your fresh values):\n\n```javascript\nconst nacl = require('tweetnacl');\nconst bs58 = require('bs58');\nconst fs = require('fs');\n// 1. Load credentials\nconst creds = JSON.parse(fs.readFileSync('~/.near-credentials/mainnet/your-account.json'));\nconst privateKeyBase58 = creds.private_key.replace('ed25519:', '');\n// 2. Decode keypair\nconst keypair = bs58.decode(privateKeyBase58);\nconst secretKey = keypair.slice(0, 32);\n// 3. Sign message (use YOUR fresh timestamp_iso from Step 1)\nconst message = '43d3c5b5e77a46b52933bc7a8b79b06f16dd4ca3cfbacd0e6fede0e7e01782ac2026-04-19T18:19:18.000Z';\nconst messageBytes = Buffer.from(message, 'utf8');\nconst signature = nacl.sign.detached(messageBytes, secretKey);\n// 4. Encode as base64url (URL-safe: - and _ instead of + and /)\nconst base64url = Buffer.from(signature).toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n// Example result: \"xK7vR3mN9pQ2wL8hF5jT6cY1sD4eU0iO...\" (yours will differ)\nconsole.log(base64url);\n```\n\n**Reference (IdentyClaw) — Python wire format** (use your fresh values):\n\n```python\nfrom nacl.signing import SigningKey\nimport base58\nimport json\nimport base64\n# 1. Load credentials\ncreds = json.load(f)\nprivate_key_base58 = creds['private_key'].replace('ed25519:', '')\n# 2. Decode keypair\nkeypair = base58.b58decode(private_key_base58)\nsecret_key = keypair[:32]\n# 3. Sign message (use YOUR fresh timestamp_iso from Step 1)\nsigning_key = SigningKey(secret_key)\nmessage = b'43d3c5b5e77a46b52933bc7a8b79b06f16dd4ca3cfbacd0e6fede0e7e01782ac2026-04-19T18:19:18.000Z'\nsignature = signing_key.sign(message).signature\n# 4. Encode as base64url (URL-safe: - and _ instead of + and /)\nbase64url = base64.b64encode(signature).decode('utf-8').replace('+', '-').replace('/', '_').replace('=', '')\n# Example result: \"xK7vR3mN9pQ2wL8hF5jT6cY1sD4eU0iO...\" (yours will differ)\nprint(base64url)\n```\n\n## Step 3: POST /api/login\n\n**Endpoint**: `POST /api/login` (public, no auth required)\n\n**Request Pattern** (use your fresh values):\n```bash\ncurl -sS -X POST https://api.identyclaw.com/api/login \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"accountid\":\"43d3c5b5e77a46b52933bc7a8b79b06f16dd4ca3cfbacd0e6fede0e7e01782ac\",\"timestamp\":1776622758,\"base64url_signature\":\"YOUR_BASE64URL_SIGNATURE\"}'\n```\n\nFor **roditid** login (12-letter Passport ID), use `roditid` and `roditid_base64url_signature` instead—same signing rule: `roditid + timestamp_iso` (see **clienttest-idc** `security.js` and `authentication-comprehensive.js` for `fetch` examples).\n\n**Note**: `POST /api/login` requires timestamp information from the same `GET /api/login/timestamp` challenge pair used for signing. Send exactly one of `timestamp` (Unix seconds) or `timestamp_iso` (canonical ISO string). Do not send both fields.\n\n**Success Response Pattern**:\n```json\n{\n  \"jwt_token\": \"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...\",\n  \"requestId\": \"01HQXYZ...\"\n}\n```\n\n**Important**:\n- `accountid`: Your 64-character lowercase hex NEAR implicit account ID (or use `roditid` for 12-letter token_id)\n- `timestamp`: Unix seconds from Step 1 (or provide `timestamp_iso` instead, but not both)\n- `base64url_signature`: URL-safe base64 from Step 2\n- Response field is `jwt_token` (not `token`)\n- JWT expires in 3600 seconds (1 hour)\n- If login fails, discard this timestamp pair and start again from Step 1\n\n### Retry Pattern (Correct)\n\n**Reference (IdentyClaw)** — fetch a new timestamp pair on each failed attempt; do not reuse a consumed challenge.\n\n```javascript\nasync function loginWithFreshTimestampRetry(accountId, signAndLogin, maxAttempts = 2) {\n  let lastError;\n  for (let attempt = 0; attempt < maxAttempts; attempt++) {\n    const tsResp = await fetch(\"https://api.identyclaw.com/api/login/timestamp\");\n    const { timestamp, timestamp_iso } = await tsResp.json();\n    const base64url_signature = await signAndLogin(accountId, timestamp_iso);\n    const loginResp = await fetch(\"https://api.identyclaw.com/api/login\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ accountid: accountId, timestamp, base64url_signature }),\n    });\n    if (loginResp.ok) {\n      return loginResp.json();\n    }\n    lastError = await loginResp.text();\n  }\n  throw new Error(`Login failed after retries: ${lastError}`);\n}\n```\n\n## Complete Pattern Example\n\n**Reference (IdentyClaw)** — end-to-end curl (no client-side NEAR RPC):\n\n```bash\nTS_JSON=\"$(curl -sS https://api.identyclaw.com/api/login/timestamp)\"\nTIMESTAMP=\"$(printf '%s' \"$TS_JSON\" | jq -r .timestamp)\"\nTIMESTAMP_ISO=\"$(printf '%s' \"$TS_JSON\" | jq -r .timestamp_iso)\"\n# SIG=\"$(sign_accountid_timestamp_iso \"$ACCOUNTID\" \"$TIMESTAMP_ISO\")\"  # your local signer\n\nLOGIN_JSON=\"$(curl -sS -X POST https://api.identyclaw.com/api/login \\\n  -H \"Content-Type: application/json\" \\\n  -d \"{\\\"accountid\\\":\\\"${ACCOUNTID}\\\",\\\"timestamp\\\":${TIMESTAMP},\\\"base64url_signature\\\":\\\"${SIG}\\\"}\")\"\nJWT=\"$(printf '%s' \"$LOGIN_JSON\" | jq -r .jwt_token)\"\n\ncurl -sS https://api.identyclaw.com/api/me/identity \\\n  -H \"Authorization: Bearer ${JWT}\"\n```\n\nSigning (Step 2) is local Ed25519 over `accountid + timestamp_iso`; see [Step 2](#step-2-sign-the-message). Optional SDK path: [Optional: client-side server Passport validation](#optional-client-side-server-passport-validation-roditrodit-auth-be).\n\n**Key Points**:\n- ✓ Fetches **fresh timestamp** from API (not generated locally)\n- ✓ Uses **timestamp_iso** from the response for signing\n- ✓ Signs with **timestamp_iso** (ISO string)\n- ✓ Sends exactly one timestamp field in payload (`timestamp` or `timestamp_iso`)\n- ✓ Returns JWT ready to use\n- ✓ All values are **generated fresh** each time\n\n## Step 4: Use JWT Token\n\nInclude JWT in `Authorization` header for protected endpoints:\n\n```bash\n# Use YOUR freshly obtained JWT token\ncurl https://api.identyclaw.com/api/me/identity \\\n  -H \"Authorization: Bearer eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...\"\n```\n\n**Common Protected Endpoints**:\n- `GET /api/me/identity` - Get your passport information\n- `GET /api/holanonce16ts` - HOLA nonce; response JSON: `noncetsHex`, `timestamp`, `length`, `algorithm`, `requestId` ([holanonce-api.md](holanonce-api.md))\n- `POST /api/identity/verify` - Verify HOLA messages from peers\n- `POST /api/testhola` - Test your HOLA generation\n\n## Agent harnesses (OpenClaw, Cursor, shell agents)\n\nAI agent runtimes often **redact** `jwt_token` in tool output (for example `eyJhbG…ISDw`). Using that fragment in a later `Authorization: Bearer …` header produces `401 INVALIDATED_TOKEN` with `details.reason: invalid_jwt_format` even when `POST /api/login` returned **200**.\n\nBefore debugging server session state, verify the bearer token on the client: length ~1500–2000 characters, three dot-separated segments, field name **`jwt_token`**.\n\nStore and reuse the full JWT on the **agent machine** (SDK, local plugin, or file)—not inline in chat or curl the model can see. Complete client-side patterns: [`mcp-auth-tools.md`](mcp-auth-tools.md) (`doc:reference:mcp-auth-tools`). Machine-readable diagnosis: `guide:troubleshooting`.\n\n## Verify Your Login Works\n\n**Test Endpoint**: `GET /api/me/identity` (requires JWT)\n\nUse this endpoint to validate your login was successful.\n\n```bash\n# Use YOUR freshly obtained JWT token\ncurl https://api.identyclaw.com/api/me/identity \\\n  -H \"Authorization: Bearer YOUR_JWT_TOKEN\"\n```\n\n**Success Response** (200 OK):\n```json\n{\n  \"token_id\": \"bkbvehbdcrgm\",\n  \"owner_id\": \"your-account.near\",\n  \"metadata\": {\n    \"title\": \"Your Passport Title\",\n    \"issued_at\": \"1776622758000000000\",\n    \"not_after\": \"1808158758000000000\"\n  }\n}\n```\n\n✓ **If you see your passport information with `token_id` matching your ID, your login is working correctly!**\n\n**Important**:\n- Must use `Authorization: Bearer <token>` header format\n- JWT expires in 1 hour - request new token when expired\n- Token is valid for all protected endpoints\n\n## Machine-readable login errors\n\nThese **API error codes** apply to the **JWT login lane only** (challenge from `GET /api/login/timestamp`, **base64url** signature on the **login signing payload**, `POST /api/login`). They are **not** used for HOLA line verification (`HOLA_*` codes and `failureReasons` on verify).\n\n### Response shape\n\n- **Unified** (typical): `{ \"error\": { \"code\", \"message\", \"details\"? }, \"requestId\", \"timestamp\" }`.\n- **Legacy login validation** (some 400 paths): top-level string `error` equal to the code — use `body.error?.code ?? body.error` when parsing.\n\n### `GET /api/login/timestamp`\n\n| HTTP | API error code | Typical cause |\n| --- | --- | --- |\n| 429 | `RATE_LIMIT_EXCEEDED` | Too many requests |\n| 500 | `AGENT_AUTH_PARAMS_FAILED` | Server could not generate challenge |\n\n### `POST /api/login` — request validation (HTTP 400)\n\n| API error code | Typical cause |\n| --- | --- |\n| `LOGIN_TIMESTAMP_AMBIGUOUS` | Both `timestamp` and `timestamp_iso` sent |\n| `INVALID_LOGIN_TIMESTAMP` | Missing or malformed timestamp for login |\n| `LOGIN_PAYLOAD_DEPRECATED` | Deprecated keys or two non-empty signature fields |\n| `LOGIN_IDENTIFIER_AMBIGUOUS` | Both `roditid` and `accountid` non-empty |\n| `MISSING_LOGIN_IDENTIFIER` | Neither identifier |\n| `MISSING_BASE64URL_SIGNATURE` | No `base64url_signature` / `roditid_base64url_signature` |\n\n### `POST /api/login` — credential verification (HTTP 401)\n\nOn failure, `error.code` is usually the same string as `error.details.failureReason` (when details are present).\n\n| API error code | Meaning |\n| --- | --- |\n| `LOGIN_CHALLENGE_TIMESTAMP_INVALID` | Login challenge Unix `timestamp` rejected (e.g. too far in the future vs server); must align with **login challenge pair** from `GET /api/login/timestamp`. |\n| `LOGIN_BASE64URL_SIGNATURE_INVALID` | **base64url login signature** did not verify over UTF-8 **login signing payload** (`roditid` or `accountid` + canonical `timestamp_iso` from that challenge). |\n| `RODIT_NOT_FOUND` | No on-chain RODiT for the identifier |\n| `RODIT_MISSING_METADATA` | Token missing required metadata |\n| `RODIT_NOT_LIVE` | Passport outside `not_before` / `not_after` |\n| `RODIT_REVOKED` | Passport revoked |\n| `RODIT_FAMILY_MISMATCH` | Passport family does not match server configuration |\n| `SMART_CONTRACT_NOT_TRUSTED` | Issuing contract not trusted |\n| `SERVER_CONFIG_INCOMPLETE` | Server RODiT configuration incomplete |\n| `LOGIN_MODE_POLICY_REJECTED_*` | Dynamic codes when `LOGIN_MODE` rejects a path |\n| `LOGIN_ERROR` | Unexpected error during login |\n| `INVALID_CREDENTIALS` | Generic fallback |\n\n**Note:** Outbound **webhook** signature failures use `WEBHOOK_SIGNATURE_INVALID` (not returned from `POST /api/login`).\n\n### Protected endpoints — bearer JWT validation (HTTP 401)\n\nThese apply **after** login, on routes that require `Authorization: Bearer <jwt_token>`.\n\n| HTTP | API error code | `details.reason` (typical) | Meaning |\n| --- | --- | --- | --- |\n| 401 | `INVALIDATED_TOKEN` | `invalid_jwt_format` | Bearer value is not a parseable JWT (often **truncated/redacted**, e.g. copied from agent tool output) |\n| 401 | `INVALIDATED_TOKEN` | `error_checking_session` | JWT parseable but session invalid/expired/revoked |\n| 401 | `MISSING_TOKEN` | — | No Authorization header |\n\n**Agent-client note:** When `invalid_jwt_format` occurs immediately after a successful `POST /api/login`, the client almost certainly sent a **truncated** token. Check token length (~1500–2000 chars) before investigating server session state.\n\n### Other\n\n| HTTP | API error code | When |\n| --- | --- | --- |\n| 503 | `AUTH_SERVICE_UNAVAILABLE` | Auth client not initialized on this instance |\n\n**HOLA lane:** Inter-agent HOLA verification uses different classifiers (`HOLA_*` on HTTP errors from `/api/testhola` and early `/api/identity/verify` checks; `failureReasons` on verify). Do not map those strings to login-only codes above.\n\n## Common Pitfalls\n\n❌ **Using different timestamps for signing vs payload**\n- **Problem**: Calling `/api/login/timestamp` at time T1, then using a different ISO timestamp in payload/signature\n- **Solution**: Use the same fresh `timestamp_iso` from one `/api/login/timestamp` response for both\n\n❌ **Reusing a timestamp after a failed login**\n- **Problem**: Retrying `POST /api/login` with the same old `timestamp_iso`\n- **Solution**: Fetch a brand-new timestamp from `/api/login/timestamp` before every retry\n\n❌ **Signing with wrong timestamp format**\n- **Problem**: Signing with Unix timestamp instead of ISO string\n- **Solution**: Sign with `timestamp_iso` (e.g., `2026-04-19T18:19:18.000Z`)\n\n❌ **Non-canonical ISO timestamp**\n- **Problem**: Sending a non-canonical ISO format (offset timezone, missing milliseconds, etc.)\n- **Solution**: Use `timestamp_iso` exactly as returned by `/api/login/timestamp`\n\n❌ **Wrong signature encoding**\n- **Problem**: Using standard base64 instead of base64url\n- **Solution**: Replace `+` with `-`, `/` with `_`, remove `=` padding\n\n❌ **Wrong Content-Type**\n- **Problem**: Missing or incorrect Content-Type header\n- **Solution**: Always use `Content-Type: application/json`\n\n❌ **Passport expired**\n- **Problem**: Passport's `not_after` date has passed\n- **Solution**: Check Passport metadata, mint new Passport if expired\n\n❌ **Wrong network**\n- **Problem**: Using testnet credentials on mainnet (or vice versa)\n- **Solution**: Verify network configuration matches your Passport's network\n\n❌ **Copying a redacted JWT or misreading post-login INVALIDATED_TOKEN**\n- **Problem**: Tool output shows `jwt_token` as `eyJhbG…ISDw`; a later request uses that fragment, or the agent blames server session storage\n- **Solution**: Verify bearer token length (~1500–2000) on the client first; store the full JWT on the agent machine per [mcp-auth-tools.md](mcp-auth-tools.md)\n\n## Next Steps\n\n- [HOLA Protocol for inter-agent authentication](hola-agent-authentication.md)\n- [Understand Passport metadata](token-metadata.md)\n- [Explore API endpoints](api-reference.md)\n- [View JSON-LD integration](jsonld-metadata.md)\n","requestId":"fdb41077d005c13fd906ec58759d4df4"}