{
  "service": "Obelisk",
  "tagline": "Full-circle security for the agent era — a unified identity & security plane. Security you can prove, not just promise.",
  "description": "Passkey-first identity, one OIDC provider, tamper-evident receipts, a live security Rating, organizations, and a per-project profile layer. One identity across the whole ecosystem.",
  "version": "obelisk-api-1",
  "baseUrl": "https://obeliskgate.com",
  "forAgents": {
    "start_here": "https://obeliskgate.com/api",
    "guide": "https://obeliskgate.com/agents",
    "integrate": "https://obeliskgate.com/developers",
    "works_with_your_database": "https://obeliskgate.com/developers#database",
    "agent_interface": "https://obeliskgate.com/agent",
    "agent_onboarding": "https://obeliskgate.com/api#agent-identity",
    "agent_proof": "https://obeliskgate.com/agent/<proof_id>",
    "principles": [
      "passkey-first / WebAuthn",
      "no user credentials stored by your app",
      "tamper-evident receipts",
      "sender-bound tokens (DPoP, RFC 9449) opt-in"
    ],
    "auth_vocabulary": {
      "none": "public",
      "session": "obelisk_session cookie OR body.token",
      "bearer": "OIDC access token (Authorization: Bearer)",
      "owner": "session + org owner/admin",
      "ack-secret": "x-obelisk-ecosystem-ack header for Studio ecosystem acknowledgements"
    },
    "token_binding": "DPoP (RFC 9449), opt-in: send a DPoP proof header at /auth/token and the access token binds to your key (cnf.jkt); protected calls then require token + fresh proof.",
    "content_negotiation": "Selected account, organization, API-manifest, and changelog surfaces return JSON with ?format=json or Accept: application/json; static editorial pages remain HTML.",
    "error_shape": "{ ok:false, reason:<code>, message?:<human> } on failures; HTTP status mirrors it."
  },
  "discovery": {
    "openid_configuration": "https://obeliskgate.com/.well-known/openid-configuration",
    "oauth_protected_resource": "https://obeliskgate.com/.well-known/oauth-protected-resource",
    "jwks": "https://obeliskgate.com/.well-known/jwks.json",
    "manifest": "https://obeliskgate.com/api",
    "openapi": "https://obeliskgate.com/openapi.json",
    "agent_interface": "https://obeliskgate.com/.well-known/obelisk-agent",
    "saml_idp_metadata": "https://obeliskgate.com/saml/idp/metadata",
    "health": "https://obeliskgate.com/healthz",
    "transparency": "https://obeliskgate.com/.well-known/obelisk-transparency.json",
    "security_txt": "https://obeliskgate.com/.well-known/security.txt",
    "agents_json": "https://obeliskgate.com/agents.json",
    "llms_txt": "https://obeliskgate.com/.well-known/llms.txt",
    "changelog": "https://obeliskgate.com/changelog?format=json",
    "studio_ecosystem": "https://obeliskgate.com/studio/ecosystem/manifest.json"
  },
  "pages": {
    "developers": "https://obeliskgate.com/developers",
    "integrate_with_your_db": "https://obeliskgate.com/developers#database",
    "pricing": "https://obeliskgate.com/pricing",
    "help": "https://obeliskgate.com/help",
    "faq": "https://obeliskgate.com/help/faq",
    "security": "https://obeliskgate.com/security",
    "about": "https://obeliskgate.com/about",
    "changelog": "https://obeliskgate.com/changelog",
    "mcp": "https://obeliskgate.com/mcp",
    "organizations": "https://obeliskgate.com/organizations"
  },
  "contracts": {
    "project_profile": "https://obeliskgate.com/developers#database"
  },
  "surfaces": {
    "release_history": {
      "title": "Release history",
      "summary": "Public-safe product history for humans and agents, bound to the revision serving it when deploy metadata is available.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/changelog",
          "auth": "none",
          "summary": "Human release history (HTML; versioned JSON with ?format=json or Accept: application/json)."
        }
      ]
    },
    "auth": {
      "title": "Authentication & OIDC",
      "summary": "Passkey-first sign-in + OIDC.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/auth",
          "auth": "none",
          "summary": "The immersive sign-in/sign-up experience (HTML)."
        },
        {
          "method": "GET",
          "path": "/auth/assertion-options",
          "auth": "none",
          "summary": "Begin a (usernameless) passkey sign-in.",
          "query": {
            "user": "optional account name"
          }
        },
        {
          "method": "POST",
          "path": "/auth/assertion-verify",
          "auth": "none",
          "summary": "Complete a passkey sign-in; returns the session + where to go (home).",
          "body": {
            "user": "optional",
            "response": "WebAuthn assertion",
            "deviceId": "optional"
          }
        },
        {
          "method": "POST",
          "path": "/auth/verify-session",
          "auth": "none",
          "summary": "Server-to-server: validate a session token; returns { ok, identityId }.",
          "body": {
            "token": "macaroon"
          }
        },
        {
          "method": "GET",
          "path": "/auth/userinfo",
          "auth": "bearer",
          "summary": "OIDC userinfo for an access token."
        },
        {
          "method": "POST",
          "path": "/auth/magic/request",
          "auth": "none",
          "summary": "Request an email magic-link (where enabled).",
          "body": {
            "email": "string",
            "dryRun": "session-gated: return the link for the session's OWN verified email (founder at sensitive assurance: any email, receipted), don't send"
          }
        },
        {
          "method": "GET",
          "path": "/auth/magic",
          "auth": "none",
          "summary": "W272 — the magic-link landing: consumes the token on the requesting device, then hands off (back to the relying party when one is waiting).",
          "query": {
            "token": "string",
            "return": "validated relying-party return",
            "project": "display name"
          }
        },
        {
          "method": "GET",
          "path": "/auth/authorize",
          "auth": "none",
          "summary": "OIDC authorization endpoint (PKCE S256). W272: response_mode=web_message posts the code to the opener page from a popup; prompt=none|login; a live session fast-paths to one tap.",
          "query": {
            "client_id": "registered client",
            "redirect_uri": "exact registered callback",
            "response_mode": "query | web_message",
            "prompt": "none | login"
          }
        },
        {
          "method": "POST",
          "path": "/auth/par",
          "auth": "none",
          "summary": "S379 — Pushed Authorization Requests (RFC 9126). A registered client authenticates exactly as at /auth/token and pushes its authorization parameters over the back channel; the same client policy as /auth/authorize is applied. Returns a one-time request_uri valid for 60 seconds; then open /auth/authorize?client_id=…&request_uri=… and every other front-channel parameter is ignored. Offered, not required; brute-force throttled.",
          "body": {
            "client_id": "registered client",
            "client_secret": "confidential clients (or HTTP Basic)",
            "response_type": "code",
            "redirect_uri": "exact registered URI",
            "code_challenge": "S256",
            "code_challenge_method": "S256",
            "scope": "openid …",
            "state": "string",
            "resource": "optional (RFC 8707)"
          }
        },
        {
          "method": "POST",
          "path": "/auth/authorize/direct",
          "auth": "none",
          "summary": "W272 — the direct grant: a passkey assertion (or fresh registration) performed ON a registered relying party's page (WebAuthn related origins) becomes a one-time authorization code. CORS-scoped to the origin registered for client_id; PKCE S256; auth-throttled.",
          "body": {
            "client_id": "string",
            "redirect_uri": "string",
            "code_challenge": "S256",
            "code_challenge_method": "S256",
            "state": "string",
            "nonce": "optional",
            "assertion": "{ user?, response }",
            "registration": "{ user, response, signupToken? } — account creation in place"
          }
        }
      ]
    },
    "fedcm": {
      "title": "FedCM (browser-mediated sign-in)",
      "summary": "W272 — Federated Credential Management: the browser shows the Obelisk account chooser inline on a registered relying party's page and hands it a signed ID token. No redirect, no popup, third-party-cookie-proof. Discovery anchor: /.well-known/web-identity.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/fedcm/config.json",
          "auth": "none",
          "summary": "Provider configuration (endpoints, branding, login_url)."
        },
        {
          "method": "GET",
          "path": "/fedcm/accounts",
          "auth": "session",
          "summary": "The signed-in person's account(s); requires Sec-Fetch-Dest: webidentity (sent only by the browser's FedCM machinery)."
        },
        {
          "method": "GET",
          "path": "/fedcm/client-metadata",
          "auth": "none",
          "summary": "Privacy/terms for a registered client; the request Origin must be registered to it.",
          "query": {
            "client_id": "string"
          }
        },
        {
          "method": "POST",
          "path": "/fedcm/assertion",
          "auth": "session",
          "summary": "Mint an ID token (ES256 by default, RS256 for a client registered for it where enabled; aud = client_id, nonce-bound, truthful obelisk.assurance) for ONE registered client whose registered origin equals the request Origin.",
          "body": {
            "account_id": "the session's identity",
            "client_id": "string",
            "nonce": "string"
          }
        },
        {
          "method": "POST",
          "path": "/fedcm/disconnect",
          "auth": "session",
          "summary": "The person asked the browser to forget the link; acknowledged and receipted.",
          "body": {
            "client_id": "string",
            "account_hint": "string"
          }
        }
      ]
    },
    "agent_identity": {
      "title": "Agent identity & proof",
      "summary": "Register, key-bind, verify, publish, suspend, recover, and revoke AI agents, bots, automations, devices, and agent teams without claiming that behavior proves non-humanness.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/account/agents",
          "auth": "session",
          "summary": "Agent Studio (HTML): the owner's whole agent fleet — create, import, pair, and manage."
        },
        {
          "method": "GET",
          "path": "/account/agents/:agentId",
          "auth": "session",
          "summary": "One agent's deep-linkable profile page (HTML): declared runtime facts, proof keys, evidence, delegation, and the full management card. Unknown or foreign ids 404 identically."
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/enroll/begin",
          "auth": "none",
          "summary": "Begin device-style enrollment with an agent-generated public JWK. Returns a short owner code; private key material is refused.",
          "body": {
            "name": "string",
            "subjectKind": "service_bot|automation|ai_agent|hybrid_agent|robot|device|agent_team|ephemeral_subagent",
            "publicJwk": "P-256 public JWK (Ed25519 only where the deployment enables RFC 9421 key control; refused otherwise)",
            "visibility": "private|unlisted|public"
          }
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/enroll/approve",
          "auth": "session",
          "summary": "Passkey-authorized accountable party approves the pending agent; identity remains pending until the agent proves its key.",
          "body": {
            "enrollment_id": "aen_..."
          }
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/enroll/complete",
          "auth": "none",
          "summary": "Agent completes enrollment by proving its key against the enrollment nonce: an ES256 DPoP header for a P-256 key, or, where enabled, an RFC 9421 HTTP message signature for an Ed25519 key. Returns its opaque Proof Link.",
          "body": {
            "enrollment_id": "aen_..."
          },
          "headers": {
            "DPoP": "proof over POST + exact htu + enrollment nonce",
            "Signature-Input": "Ed25519 keys, where enabled: covers @method + @target-uri; keyid = the key's RFC 7638 thumbprint; nonce = enrollment nonce; tag = obelisk-key-control",
            "Signature": "the Ed25519 signature for that Signature-Input label"
          }
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/enroll/status",
          "auth": "none",
          "summary": "Poll bounded enrollment state without learning owner details.",
          "body": {
            "enrollment_id": "aen_..."
          }
        },
        {
          "method": "GET",
          "path": "/agent/:proof_id",
          "auth": "none",
          "summary": "Human-readable opt-in Proof Link. Private profiles 404; a URL is never a credential."
        },
        {
          "method": "GET",
          "path": "/agent/:proof_id.json",
          "auth": "none",
          "summary": "Machine proof vector plus W3C VC and A2A projections. Claims remain independent and freshness-scoped."
        },
        {
          "method": "POST",
          "path": "/api/agent-proof/challenge",
          "auth": "none",
          "summary": "Request a short-lived nonce for a visible active agent.",
          "body": {
            "proof_id": "opaque id"
          }
        },
        {
          "method": "POST",
          "path": "/api/agent-proof/verify",
          "auth": "none",
          "summary": "Verify current agent-key control with a one-use challenge and DPoP; appends an agent-action receipt to the hash chain (actor: the agent, approval method: dpop:<its key thumbprint>) and returns its hash with the /api/proof inclusion URL, plus a short-lived signed proof token naming that receipt when signing is available. If the receipt cannot be appended, nothing is signed.",
          "body": {
            "proof_id": "opaque id",
            "challenge_id": "apc_...",
            "audience": "optional verifier audience"
          },
          "headers": {
            "DPoP": "nonce-bound ES256 proof"
          }
        },
        {
          "method": "POST",
          "path": "/api/agent-proof/run",
          "auth": "none",
          "summary": "Bind a client-built run proof of salted commitments to the active agent key and one-use nonce; appends an agent-action receipt carrying the runId (never a task, input or output value) and returns its hash with the /api/proof inclusion URL, plus a short-lived Obelisk-signed run token naming that receipt. If the receipt cannot be appended, no token is issued. The envelope has no salt field, so salts and the raw input/output stay client-side; the task travels as a hash plus an optional caller-set label. A legacy v1 envelope (unsalted hashes) is accepted and flagged unsalted-v1 in run.warnings.",
          "body": {
            "proof_id": "opaque id",
            "challenge_id": "purpose:run challenge",
            "run_proof": "obelisk-agent-run-proof-v2 (v1 accepted, flagged unsalted-v1)",
            "audience": "optional verifier audience"
          },
          "headers": {
            "DPoP": "nonce-bound ES256 proof"
          }
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/visibility",
          "auth": "session",
          "summary": "Set profile privacy: private, unlisted, or public. Requires passkey-grade step-up."
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/lifecycle",
          "auth": "session",
          "summary": "Suspend, resume, mark compromised, or revoke. Compromise/revoke cascades to agent-bound API keys."
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/key-rotate",
          "auth": "session",
          "summary": "Rotate a public key. Normal rotation requires proof of the current key; compromise recovery rotates first and resumes only from suspended state."
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/transfer/begin",
          "auth": "session",
          "summary": "Source owner suspends an agent and creates a short transfer code for one named target owner."
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/transfer/accept",
          "auth": "session",
          "summary": "Named target owner accepts with a separate passkey and fresh public JWK. The identity stays suspended until target passkey + new agent key co-sign resume."
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/dns-challenge",
          "auth": "session",
          "summary": "Get the stable _obelisk-agent TXT record that proves domain control for this agent. Publish it, then dns-verify.",
          "body": {
            "agent_id": "agt_...",
            "domain": "example.com"
          }
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/dns-verify",
          "auth": "session",
          "summary": "Resolve the TXT record and, on match, append domain-control evidence (channel_control leaves 'declared').",
          "body": {
            "agent_id": "agt_...",
            "domain": "example.com"
          }
        },
        {
          "method": "POST",
          "path": "/auth/agents/proof/a2a-discover",
          "auth": "session",
          "summary": "Fetch an A2A agent card (SSRF-guarded, 128KB cap) and append descriptor-integrity evidence; fills declared a2aCardUrl/capabilities only where empty.",
          "body": {
            "agent_id": "agt_...",
            "url": "https card URL"
          }
        },
        {
          "method": "GET",
          "path": "/api/agent-trust/bundle",
          "auth": "none",
          "summary": "This authority's claim registry as a signed trust bundle (JWS via jwks_uri) for federation peers to pin and import."
        }
      ],
      "claim_model": "Identity record + expiring posture + action-scoped run proof. No scalar ladder; no categorical 'not human', 'not exploited', or 'safe' claim."
    },
    "account": {
      "title": "Account",
      "summary": "The universal Obelisk account — one identity across every app.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/account",
          "auth": "session",
          "summary": "Your account page (HTML; JSON with ?format=json → the full profile)."
        },
        {
          "method": "GET",
          "path": "/account/export",
          "auth": "session",
          "summary": "Download a portable, secret-free copy of your account (JSON)."
        },
        {
          "method": "POST",
          "path": "/auth/profile",
          "auth": "session",
          "summary": "Set your display name.",
          "body": {
            "displayName": "string"
          }
        },
        {
          "method": "POST",
          "path": "/auth/account/rename",
          "auth": "session",
          "summary": "Change your username (old name stays a resolvable alias).",
          "body": {
            "newUsername": "string"
          }
        },
        {
          "method": "POST",
          "path": "/auth/preferences",
          "auth": "session",
          "summary": "Merge your preferences (theme/lang/contrast/motion/text/email).",
          "body": {
            "preferences": "object"
          }
        },
        {
          "method": "POST",
          "path": "/auth/passkeys/list",
          "auth": "session",
          "summary": "List your passkeys (no public keys)."
        },
        {
          "method": "POST",
          "path": "/auth/passkeys/rename",
          "auth": "session",
          "summary": "Rename a passkey.",
          "body": {
            "credId": "string",
            "label": "string"
          }
        },
        {
          "method": "POST",
          "path": "/auth/passkeys/remove",
          "auth": "session",
          "summary": "Remove a passkey (never the last one).",
          "body": {
            "credId": "string"
          }
        },
        {
          "method": "POST",
          "path": "/auth/passkeys/add-options",
          "auth": "session",
          "summary": "Begin adding a passkey to your account."
        },
        {
          "method": "POST",
          "path": "/auth/passkeys/add-verify",
          "auth": "session",
          "summary": "Finish adding a passkey.",
          "body": {
            "response": "WebAuthn attestation"
          }
        },
        {
          "method": "POST",
          "path": "/auth/totp/setup",
          "auth": "session",
          "summary": "Begin TOTP setup (pending) → { secret, uri, qrSvg }."
        },
        {
          "method": "POST",
          "path": "/auth/totp/confirm",
          "auth": "session",
          "summary": "Confirm + activate TOTP with a code.",
          "body": {
            "code": "6 digits"
          }
        },
        {
          "method": "POST",
          "path": "/auth/totp/disable",
          "auth": "session",
          "summary": "Turn off TOTP."
        },
        {
          "method": "POST",
          "path": "/auth/recovery/regenerate",
          "auth": "session",
          "summary": "Generate new backup codes (shown once)."
        },
        {
          "method": "POST",
          "path": "/auth/sessions/revoke",
          "auth": "session",
          "summary": "Revoke a session.",
          "body": {
            "identifier": "string"
          }
        },
        {
          "method": "POST",
          "path": "/auth/devices/revoke",
          "auth": "session",
          "summary": "Sign out a whole device — revokes every session it holds, then drops it from the device ledger.",
          "body": {
            "deviceId": "string"
          }
        },
        {
          "method": "POST",
          "path": "/auth/signout",
          "auth": "session",
          "summary": "Clear the session cookie."
        }
      ]
    },
    "organizations": {
      "title": "Organizations",
      "summary": "Register a business/website for its own Obelisk Rating; manage a team.",
      "endpoints": [
        {
          "method": "POST",
          "path": "/auth/orgs/create",
          "auth": "session",
          "summary": "Create an organization (you become owner).",
          "body": {
            "name": "string",
            "website": "optional url"
          }
        },
        {
          "method": "GET",
          "path": "/org/:slug",
          "auth": "session",
          "summary": "Org dashboard (HTML; JSON with ?format=json), member-gated."
        },
        {
          "method": "POST",
          "path": "/org/:slug/scan",
          "auth": "owner",
          "summary": "Run an external security-posture scan → an Obelisk Rating. Scan a specific site with { siteId } or the primary by default.",
          "body": {
            "website": "optional override",
            "siteId": "optional registered-site id"
          }
        },
        {
          "method": "POST",
          "path": "/org/:slug/websites/add",
          "auth": "owner",
          "summary": "Add a website this org owns (first becomes primary).",
          "body": {
            "url": "https url",
            "label": "optional"
          }
        },
        {
          "method": "POST",
          "path": "/org/:slug/websites/remove",
          "auth": "owner",
          "summary": "Remove a registered site by id.",
          "body": {
            "siteId": "string"
          }
        },
        {
          "method": "POST",
          "path": "/org/:slug/websites/primary",
          "auth": "owner",
          "summary": "Promote a site to primary (the org headline rating tracks it).",
          "body": {
            "siteId": "string"
          }
        },
        {
          "method": "POST",
          "path": "/org/:slug/members/role",
          "auth": "owner",
          "summary": "Change a member's role. Owner-only for owner changes; the last owner can't be demoted.",
          "body": {
            "user": "identityId",
            "role": "member|admin|owner"
          }
        },
        {
          "method": "POST",
          "path": "/org/:slug/members/remove",
          "auth": "owner",
          "summary": "Remove a member. The last owner can never be removed.",
          "body": {
            "user": "identityId"
          }
        },
        {
          "method": "POST",
          "path": "/org/:slug/invite",
          "auth": "owner",
          "summary": "Create a role-scoped invite link.",
          "body": {
            "role": "member|admin",
            "email": "optional"
          }
        },
        {
          "method": "POST",
          "path": "/org/:slug/invite/revoke",
          "auth": "owner",
          "summary": "Revoke an invite.",
          "body": {
            "code": "string"
          }
        },
        {
          "method": "POST",
          "path": "/org/:slug/settings",
          "auth": "owner",
          "summary": "Rename the organization and/or set its description. The @slug handle is permanent; member profiles update automatically.",
          "body": {
            "name": "string ≤80",
            "description": "optional string ≤280"
          }
        },
        {
          "method": "POST",
          "path": "/org/:slug/leave",
          "auth": "session",
          "summary": "Leave the organization (self-service). The last owner must transfer ownership first; your agents scoped to it become personal."
        },
        {
          "method": "POST",
          "path": "/org/:slug/transfer-ownership",
          "auth": "owner",
          "summary": "Hand ownership to an existing member (passkey step-up required). You stay on as admin.",
          "body": {
            "user": "identityId"
          }
        },
        {
          "method": "GET",
          "path": "/join/:code",
          "auth": "none",
          "summary": "Accept an invite (signs you in first if needed)."
        }
      ]
    },
    "app_registration": {
      "title": "App registration",
      "summary": "Self-serve OAuth/OIDC client registration — the developer portal (/account/apps) + its API.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/account/apps",
          "auth": "session",
          "summary": "The hosted app-registration portal (HTML): register apps, list, copy config, revoke."
        },
        {
          "method": "POST",
          "path": "/auth/register",
          "auth": "session",
          "summary": "RFC 7591 dynamic client registration → { client_id, ... }. Optional { org } scopes the app to an organization you administer (else your personal tenant). Requires OBELISK_DCR=open + tenant enforcement.",
          "body": {
            "client_name": "string",
            "redirect_uris": "https[]",
            "obelisk_project_trust_profile": "object",
            "resource_uris": "https[]",
            "org": "optional org slug"
          }
        },
        {
          "method": "POST",
          "path": "/auth/apps/list",
          "auth": "session",
          "summary": "List apps in tenants you control (personal + admin orgs). Never returns a secret."
        },
        {
          "method": "POST",
          "path": "/auth/apps/revoke",
          "auth": "session",
          "summary": "Revoke an app you own → status:revoked.",
          "body": {
            "clientId": "string"
          }
        },
        {
          "method": "POST",
          "path": "/auth/apps/update",
          "auth": "session",
          "summary": "Update an owned app name, exact callbacks, resources or trust profile. Requires verified assurance and the latest list revision; stale edits return 409.",
          "body": {
            "clientId": "string",
            "expectedRevision": "integer",
            "client_name": "optional string",
            "redirect_uris": "optional https[]",
            "resource_uris": "optional https[]",
            "obelisk_project_trust_profile": "optional object"
          }
        },
        {
          "method": "POST",
          "path": "/auth/apps/rotate-secret",
          "auth": "session",
          "summary": "Rotate an owned confidential client secret. Returns it once; the previous secret immediately stops working. Requires verified assurance and current revision.",
          "body": {
            "clientId": "string",
            "expectedRevision": "integer"
          }
        }
      ]
    },
    "email_verification": {
      "title": "Email verification",
      "summary": "Verify an address on the signed-in account without creating, merging or upgrading sessions.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/auth/email",
          "auth": "session",
          "summary": "Hosted email verification and same-browser confirmation page."
        },
        {
          "method": "POST",
          "path": "/auth/email/request",
          "auth": "session",
          "summary": "Email a five-minute, single-use proof bound to the current strong session and browser. Rate limited; never returns the proof.",
          "body": {
            "email": "email address",
            "codeChallenge": "S256 base64url challenge",
            "deviceNonce": "random browser nonce"
          }
        },
        {
          "method": "POST",
          "path": "/auth/email/confirm",
          "auth": "session",
          "summary": "Confirm email possession for the same strong session. Rejects replay, expiry and another account's address; never mints a session.",
          "body": {
            "proofToken": "emailed proof",
            "codeVerifier": "PKCE verifier",
            "deviceNonce": "requesting browser nonce"
          }
        }
      ]
    },
    "project_profile": {
      "title": "Per-project profile",
      "summary": "The on-top layer: each project's own per-user profile slice, scoped to its OIDC client.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/project-profile",
          "auth": "bearer",
          "summary": "Read this project's profile slice for the token's user → { sub, project, profile }."
        },
        {
          "method": "POST",
          "path": "/api/project-profile",
          "auth": "bearer",
          "summary": "Shallow-merge into this project's slice.",
          "body": {
            "profile": "object"
          }
        }
      ]
    },
    "studio_ecosystem": {
      "title": "Studio ecosystem",
      "summary": "Studio-wide Obelisk rollout truth: declared, observed, required, migration cargo, and receipt-backed acknowledgement state.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/studio/ecosystem/manifest.json",
          "auth": "none",
          "summary": "Fleet manifest for Studio Ops: every registered project, required Obelisk versions, observed status, and migration state."
        },
        {
          "method": "GET",
          "path": "/studio/ecosystem/projects/:slug",
          "auth": "none",
          "summary": "One project's ecosystem profile row from the canonical manifest."
        },
        {
          "method": "GET",
          "path": "/studio/ecosystem/projects/:slug/migration",
          "auth": "none",
          "summary": "Content-addressed recipient-owned Ark migration cargo for one project."
        },
        {
          "method": "POST",
          "path": "/studio/ecosystem/projects/:slug/ack",
          "auth": "ack-secret",
          "summary": "Record a recipient-owned migration acknowledgement. Requires x-obelisk-ecosystem-ack; writes an ecosystem-ack receipt.",
          "body": {
            "result": "applied|already-current|rejected|blocked",
            "cargoDigest": "sha256:...",
            "repoCommit": "optional commit",
            "evidence": "optional object"
          }
        }
      ]
    },
    "mcp": {
      "title": "MCP tool server",
      "summary": "Obelisk's public Model Context Protocol tool server — read-only trust tools over JSON-RPC 2.0, plus the plain catalogue.",
      "endpoints": [
        {
          "method": "POST",
          "path": "/mcp",
          "auth": "none",
          "summary": "JSON-RPC 2.0 MCP endpoint (initialize, tools/list, tools/call). Read-only trust tools; stateful per-client rate limit.",
          "body": {
            "jsonrpc": "the literal \"2.0\"",
            "id": "number|string",
            "method": "initialize | tools/list | tools/call",
            "params": "tools/call: { name, arguments }"
          }
        },
        {
          "method": "GET",
          "path": "/mcp/tools",
          "auth": "none",
          "summary": "The plain JSON tool catalogue: every tool's name, description, and input schema."
        }
      ]
    },
    "proof": {
      "title": "Receipt proof plane",
      "summary": "Public receipt-by-hash lookup with sorted-Merkle inclusion proofs against the tamper-evident ledger. By-hash only — no enumeration.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/proof",
          "auth": "none",
          "summary": "Self-describing: how to verify, and where the transparency head to pin lives."
        },
        {
          "method": "GET",
          "path": "/api/proof/:hash",
          "auth": "none",
          "summary": "Receipt lookup by 64-hex currentHash → chain position + a Merkle inclusion proof {leafHash, path, root}. Hash material only; never bodies, actors, or intents."
        }
      ]
    },
    "federation": {
      "title": "SAML identity provider",
      "summary": "Obelisk's SAML 2.0 identity provider: metadata is published and assertions are signed, but no SAML service provider can be connected on the hosted Gate yet. Use OpenID Connect to sign in to other tools today.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/saml/idp/metadata",
          "auth": "none",
          "summary": "IdP metadata: entity ID, SSO URL, and the pinned X.509 signing certificate."
        },
        {
          "method": "GET",
          "path": "/saml/idp/sso",
          "auth": "none",
          "summary": "SAML sign-on endpoint. No service provider is registered on the hosted Gate, so it answers 400 unknown_sp. Once one is, it requires an Obelisk session (any sign-in method; the assertion states PasswordProtectedTransport, not how the person signed in) and auto-POSTs an RSA-SHA256-signed assertion to the SP's ACS.",
          "query": {
            "SAMLRequest": "SP AuthnRequest (redirect binding)"
          }
        }
      ]
    },
    "seal": {
      "title": "The verified seal",
      "summary": "The verified “Secured by Obelisk Gate” seal + per-site verification pages.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/embed/seal.js",
          "auth": "none",
          "summary": "Drop-in loader: one script tag renders the live seal card on a relying party's page."
        },
        {
          "method": "GET",
          "path": "/embed/v1/seal",
          "auth": "none",
          "summary": "The framed seal card itself, served by obeliskgate.com with no ✓. In the browser it reads location.ancestorOrigins: MATCH (every framing page at an address the registration names) shows the ✓ and the address; MISMATCH (another address) says whose seal it is and shows no button; UNKNOWN (not reported, or hidden) shows no ✓."
        },
        {
          "method": "GET",
          "path": "/verify/:rp",
          "auth": "none",
          "summary": "Public verification page for a client_id, resolved in the live client registry. States: LIVE (the registration passes the sign-in policy, so a sign-in can complete; the only state shown with a ✓, listing its registered addresses), LISTED (registered, cannot complete a sign-in), SUSPENDED (its status refuses sign-in); 404 when Obelisk Gate holds no registration for it. Optional ?seen=<origin> (the card appends it): for a LIVE registration the page states whether that exact origin is one of its registered addresses, and says the link carried it, never that the seal was shown there. A LIVE page also lists what Obelisk Gate measured about the registration (callback policy, client authentication, registered callbacks; 30-day passkey and DPoP shares only when the server enables them), each with when it was measured or why it was not, and gives the site no rating.",
          "query": {
            "seen": "exact http(s) origin (optional)"
          }
        },
        {
          "method": "GET",
          "path": "/api/seal",
          "auth": "none",
          "summary": "Off by default: answers 404 unless the server sets OBELISK_SEAL_ATTESTATION=on. When on: how to verify a seal attestation (typ obelisk-seal+jwt, aud obelisk-seal, the JWKS, the state vocabulary)."
        },
        {
          "method": "GET",
          "path": "/api/seal/:rp",
          "auth": "none",
          "summary": "Off by default (404 unless OBELISK_SEAL_ATTESTATION=on). When on: the registration's seal verdict as a JWS signed with the key at /.well-known/jwks.json — header typ obelisk-seal+jwt; claims iss, sub (the client id), aud obelisk-seal, state (LIVE | LISTED | SUSPENDED), origins (the sign-in addresses a LIVE registration names), rev, kind, enforced, jti, iat, exp = iat + 24 h, and reason when not LIVE. 404 with no token for an id Obelisk Gate holds no registration for."
        },
        {
          "method": "GET",
          "path": "/api/seal/log",
          "auth": "none",
          "summary": "Off by default (404 unless OBELISK_SEAL_ATTESTATION=on). When on: the seal-state log, newest first — one row each time what the seal says about a registration changes (first recorded, registered, state, sign-in addresses, details, removed), each naming the receipt whose Merkle inclusion proof /api/proof/:hash returns. Rows come only from the service's start-up registry comparison and owners' signed-in changes.",
          "query": {
            "rp": "one client id (optional)",
            "limit": "1–500, default 100"
          }
        }
      ]
    },
    "scan": {
      "title": "Website scan",
      "summary": "The public website posture scan (the same engine behind the org Rating).",
      "endpoints": [
        {
          "method": "POST",
          "path": "/api/scan",
          "auth": "none",
          "summary": "Scan a public https:// website's security posture → a rating with concrete levers. SSRF-guarded, throttled per client.",
          "body": {
            "url": "public https:// website (domains only)"
          }
        }
      ]
    },
    "contact": {
      "title": "Contact",
      "summary": "Reach the team.",
      "endpoints": [
        {
          "method": "POST",
          "path": "/api/contact",
          "auth": "none",
          "summary": "Send a message to the team (form-encoded: name, email, subject, message). Honeypot-protected, throttled.",
          "body": {
            "name": "string",
            "email": "string",
            "subject": "string?",
            "message": "string"
          }
        }
      ]
    }
  }
}