{
  "openapi": "3.1.0",
  "info": {
    "title": "Nonresident Tax public web API",
    "version": "1.0.0",
    "summary": "Nonresident Tax (nonresident.tax): marketing site content negotiation, machine-readable discovery documents, the public form API, and the MCP endpoint.",
    "description": "Public HTTP surface of nonresident.tax, operated by Hareword LLC (dba Nonresident Tax). It covers three things an agent can call without an account:\n\n1. **Content.** Every page is prerendered HTML with a markdown sibling. Append `.md` to any path, or send `Accept: text/markdown` to the canonical URL.\n2. **Discovery.** `/.well-known/api-catalog` (RFC 9727), `/openapi.json` (this document), `/llms.txt`, `/sitemap.md`, `/.well-known/agent-skills/index.json`, and the MCP server card.\n3. **Endpoints.** `POST /api/contact`, `POST /api/subscribe`, and `/api/unsubscribe`, served by the `@nt/edge` Worker on this same origin, plus the read-only MCP JSON-RPC endpoint at `/mcp`.\n\nError bodies differ by endpoint, and the path items below are authoritative. An unrouted `/api/*` path, a wrong method, and an upstream failure answer the structured `ApiError` body — a stable `code`, a `hint`, and a `documentation_url`. The form endpoints `POST /api/contact` and `POST /api/subscribe` answer `FormError`, which carries `error` only. `/api/unsubscribe` is opened by a person clicking a link in an email and answers `text/html` throughout, including its errors. Page routes answer a real `404` for a path that does not exist: HTML by default, markdown when `Accept` prefers `text/markdown`, and JSON when `Accept` prefers `application/json`. Ordering and account data are NOT part of this API; they require an authenticated session at https://app.nonresident.tax.\n\nHuman-readable version of this document: https://nonresident.tax/developers/",
    "contact": {
      "name": "Nonresident Tax Engineering",
      "email": "support@nonresident.tax",
      "url": "https://nonresident.tax/developers/"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://nonresident.tax/terms-of-service/"
    }
  },
  "servers": [
    {
      "url": "https://nonresident.tax",
      "description": "Production marketing site"
    },
    {
      "url": "https://test.nonresident.tax",
      "description": "Staging marketing site"
    },
    {
      "url": "https://nt-web-production.pragmaticgrowth.workers.dev",
      "description": "Production worker direct"
    },
    {
      "url": "https://nt-web-staging.pragmaticgrowth.workers.dev",
      "description": "Staging worker direct"
    }
  ],
  "tags": [
    {
      "name": "health",
      "description": "On-demand Worker health (keeps the adapter in server mode)."
    },
    {
      "name": "content",
      "description": "Page content with HTML/Markdown negotiation."
    },
    {
      "name": "feeds",
      "description": "Machine-readable site inventory and feeds."
    },
    {
      "name": "redirects",
      "description": "Legacy URL 301 redirects owned by the Worker."
    },
    {
      "name": "mcp",
      "description": "Model Context Protocol server (separate Worker, same zone)."
    },
    {
      "name": "forms",
      "description": "Public form API (contact, newsletter subscribe/unsubscribe), served by the @nt/edge Worker on this origin."
    }
  ],
  "components": {
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "required": ["ok"],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "AgentSkillsDiscovery": {
        "type": "object",
        "description": "cloudflare agent-skills-discovery v0.2.0 document built from the full content inventory (`buildAgentSkills`).",
        "properties": {
          "version": {
            "type": "string",
            "description": "Discovery format version (e.g. 0.2.0)."
          },
          "skills": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "Discoverable agent skill/content entries for the site."
          }
        },
        "additionalProperties": true
      },
      "LinkTargetArray": {
        "type": "array",
        "description": "RFC 9264 link targets for one relation.",
        "items": {
          "type": "object",
          "required": ["href"],
          "properties": {
            "href": {
              "type": "string",
              "format": "uri",
              "description": "Absolute URL on the requesting origin."
            },
            "type": {
              "type": "string",
              "description": "Media type of the target as actually served."
            }
          },
          "additionalProperties": true
        }
      },
      "JsonRpcRequest": {
        "type": "object",
        "description": "A single JSON-RPC 2.0 request. MCP methods in use: `initialize`, `tools/list`, `tools/call`.",
        "required": ["jsonrpc", "method"],
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "description": "Omitted for notifications.",
            "type": ["string", "number"]
          },
          "method": {
            "type": "string",
            "example": "tools/list"
          },
          "params": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "JsonRpcResponse": {
        "type": "object",
        "description": "A single JSON-RPC 2.0 response envelope.",
        "required": ["jsonrpc"],
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": ["string", "number", "null"]
          },
          "result": {
            "type": "object",
            "additionalProperties": true
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer",
                "description": "-32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32000 transport error."
              },
              "message": {
                "type": "string"
              }
            },
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "McpServerCard": {
        "type": "object",
        "description": "MCP server card. Served identically at `/mcp/server-card` and `/.well-known/mcp/server-card.json`.",
        "required": ["$schema", "name", "version", "description"],
        "properties": {
          "$schema": {
            "type": "string",
            "const": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json"
          },
          "name": {
            "type": "string",
            "description": "Registry-style `namespace/name` identifier."
          },
          "title": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "maxLength": 100
          },
          "websiteUrl": {
            "type": "string",
            "format": "uri"
          },
          "remotes": {
            "type": "array",
            "description": "Transport endpoints. Env-derived, so staging never advertises production.",
            "items": {
              "type": "object",
              "required": ["type", "url"],
              "properties": {
                "type": {
                  "type": "string",
                  "const": "streamable-http"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              },
              "additionalProperties": true
            }
          },
          "serverInfo": {
            "type": "object",
            "description": "The exact `Implementation` this server reports from `initialize`.",
            "properties": {
              "name": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "capabilities": {
            "type": "object",
            "description": "Mirrors the `initialize` result capabilities (read-only tools only).",
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "ApiCatalog": {
        "type": "object",
        "description": "RFC 9727 API catalog expressed as an RFC 9264 linkset. Built per request by `src/worker.ts`, so every href follows the request origin.",
        "required": ["linkset"],
        "properties": {
          "linkset": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["anchor"],
              "properties": {
                "anchor": {
                  "type": "string",
                  "format": "uri",
                  "description": "Link context: the origin this entry describes."
                },
                "service-desc": {
                  "$ref": "#/components/schemas/LinkTargetArray"
                },
                "service-doc": {
                  "$ref": "#/components/schemas/LinkTargetArray"
                },
                "status": {
                  "$ref": "#/components/schemas/LinkTargetArray"
                }
              },
              "additionalProperties": true
            }
          }
        },
        "additionalProperties": true
      },
      "ApiError": {
        "type": "object",
        "description": "Structured error body. Returned for an unrouted `/api/*` path, a method the endpoint does not accept, and an upstream failure, and by page routes when the request negotiates `application/json`. The form endpoints answer `FormError` instead. `error` repeats `message` so clients written against the older string-only shape keep working.",
        "required": [
          "error",
          "code",
          "message",
          "hint",
          "documentation_url",
          "status"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message. Same value as `message`."
          },
          "code": {
            "type": "string",
            "description": "Stable machine code. Branch on this, never on `message`.",
            "enum": ["not_found", "method_not_allowed", "upstream_error"]
          },
          "message": {
            "type": "string",
            "description": "Human-readable message."
          },
          "hint": {
            "type": "string",
            "description": "What to do next. Never echoes request input."
          },
          "documentation_url": {
            "type": "string",
            "format": "uri",
            "description": "Developer documentation for this API."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status, repeated in the body."
          }
        },
        "additionalProperties": false,
        "examples": [
          {
            "error": "No such API endpoint.",
            "code": "not_found",
            "message": "No such API endpoint.",
            "hint": "Known endpoints: POST /api/contact, POST /api/subscribe, GET /api/unsubscribe, POST /api/unsubscribe. Full machine-readable description: https://nonresident.tax/openapi.json",
            "documentation_url": "https://nonresident.tax/developers/",
            "status": 404
          }
        ]
      },
      "ContactRequest": {
        "type": "object",
        "description": "Contact form submission. Opens a support conversation.",
        "required": ["firstName", "lastName", "email", "subject", "message"],
        "properties": {
          "firstName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "lastName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "phone": {
            "type": "string",
            "maxLength": 30,
            "description": "Optional."
          },
          "subject": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "message": {
            "type": "string",
            "minLength": 10,
            "maxLength": 5000
          },
          "pageUri": {
            "type": "string",
            "maxLength": 2000,
            "description": "Optional page the submission came from."
          },
          "pageName": {
            "type": "string",
            "maxLength": 300
          },
          "website": {
            "type": "string",
            "description": "Honeypot. Leave unset. A filled value returns a fake success and creates nothing."
          }
        }
      },
      "ContactSuccess": {
        "type": "object",
        "required": ["success", "message"],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Contact form submitted successfully"
          },
          "conversationId": {
            "type": "integer",
            "description": "Support conversation id. Absent on honeypot fake-success."
          }
        }
      },
      "SubscribeRequest": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "pageUri": {
            "type": "string",
            "maxLength": 2000
          },
          "website": {
            "type": "string",
            "description": "Honeypot. Leave unset."
          }
        }
      },
      "SubscribeSuccess": {
        "type": "object",
        "required": ["success", "message"],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Successfully subscribed to the newsletter"
          }
        }
      },
      "FormError": {
        "type": "object",
        "description": "Form validation/authorization error. `error` is the human-readable message.",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      }
    },
    "headers": {
      "SecurityHeaders": {
        "description": "Worker-stamped security headers (mirror of public/_headers `/*` block). Required because run_worker_first disables _headers application on Worker-served responses.",
        "schema": {
          "type": "object",
          "properties": {
            "X-Frame-Options": {
              "type": "string",
              "const": "DENY"
            },
            "X-Content-Type-Options": {
              "type": "string",
              "const": "nosniff"
            },
            "Referrer-Policy": {
              "type": "string",
              "const": "strict-origin-when-cross-origin"
            },
            "Strict-Transport-Security": {
              "type": "string",
              "const": "max-age=31536000; includeSubDomains; preload"
            },
            "Permissions-Policy": {
              "type": "string"
            },
            "Content-Security-Policy-Report-Only": {
              "type": "string"
            }
          }
        }
      }
    },
    "parameters": {
      "AcceptHeader": {
        "name": "Accept",
        "in": "header",
        "required": false,
        "description": "When the value contains `text/markdown`, extensionless page routes negotiate the sibling `.md` asset instead of HTML. HTML is the default for browsers.",
        "schema": {
          "type": "string",
          "example": "text/markdown"
        }
      },
      "PagePath": {
        "name": "page",
        "in": "path",
        "required": true,
        "description": "Extensionless page path without leading slash (e.g. `company-formation`, `es/company-formation`, `blog/wyoming-llc-formation-guide`). Empty/root is `/`.",
        "schema": {
          "type": "string"
        }
      },
      "SlugPath": {
        "name": "slug",
        "in": "path",
        "required": true,
        "description": "Static path segment(s) for a prebuilt markdown page (enumerated at build time via getStaticPaths).",
        "schema": {
          "type": "string"
        }
      },
      "FormOriginHeader": {
        "name": "Origin",
        "in": "header",
        "required": true,
        "description": "Must be https://nonresident.tax or any https://*.nonresident.tax subdomain. Missing or foreign Origin returns 403.",
        "schema": {
          "type": "string",
          "example": "https://nonresident.tax"
        }
      },
      "UnsubscribeToken": {
        "name": "token",
        "in": "query",
        "required": true,
        "description": "Unguessable per-contact unsubscribe token from the footer of a newsletter email.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "JsonNotFound": {
        "description": "No document exists at the requested path. Returned when the request negotiates `application/json`; the same path answers HTML by default and markdown for `Accept: text/markdown`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "ApiNotFound": {
        "description": "No endpoint at this path.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "ApiMethodNotAllowed": {
        "description": "Method not allowed. The `Allow` header lists the accepted methods.",
        "headers": {
          "Allow": {
            "schema": {
              "type": "string"
            },
            "description": "Accepted methods."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "FormRateLimited": {
        "description": "Per-IP rate limit exceeded.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds until the client may retry."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/FormError"
            }
          }
        }
      },
      "FormForbiddenOrigin": {
        "description": "Missing or disallowed `Origin`. These endpoints are a browser surface; a direct call without an `Origin` on the nonresident.tax zone is rejected.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/FormError"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/internal/health": {
      "get": {
        "tags": ["health"],
        "operationId": "getHealth",
        "summary": "On-demand Worker health",
        "description": "Minimal JSON health route whose sole runtime job is to keep Astro's Cloudflare adapter in server mode so the custom `main` (`src/worker.ts`) is honored. Public. Not prerendered. Source: `src/pages/internal/health.ts`.",
        "security": [],
        "responses": {
          "200": {
            "description": "Worker/adapter healthy.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    },
    "/{page}": {
      "get": {
        "tags": ["content"],
        "operationId": "getPage",
        "summary": "Marketing page (HTML or Markdown)",
        "description": "Extensionless page route. Normal browser requests receive HTML. When `Accept` contains `text/markdown`, the Worker serves the corresponding `.md` sibling (`/` → `/index.md`, `/company-formation/` → `/company-formation.md`, `/es/company-formation/` → `/es/company-formation.md`). Markdown responses use `Cache-Control: no-store` and `Vary: Accept` so they are never edge-cached under the canonical URL. Unknown pages fall through to Astro's prerendered 404. Slashless routes retry with a trailing slash on 404. Paths under `/internal/` and static asset prefixes (`/_astro/*`, `/img/*`, extensioned files) bypass this negotiation. `/metrics` is reserved zone-wide for Google Tag Gateway and must never be added here. A path that does not exist answers a real `404` — never a `200` carrying an app shell. The 404 body follows the same negotiation: HTML by default, the `/404.md` recovery document for `Accept: text/markdown`, and an `ApiError` JSON body for `Accept: application/json`.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PagePath"
          },
          {
            "$ref": "#/components/parameters/AcceptHeader"
          }
        ],
        "responses": {
          "200": {
            "description": "Page content.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                },
                "example": "<!doctype html>..."
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                },
                "example": "# Company Formation\\n..."
              }
            },
            "headers": {
              "X-Frame-Options": {
                "$ref": "#/components/headers/SecurityHeaders"
              }
            }
          },
          "301": {
            "description": "Legacy redirect when the path matches the Worker REDIRECTS table (see paths under the redirects tag and `src/worker.ts` REDIRECTS).",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                },
                "description": "Canonical destination path (query string preserved)."
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/JsonNotFound"
          }
        }
      },
      "head": {
        "tags": ["content"],
        "operationId": "headPage",
        "summary": "Marketing page headers only",
        "description": "Same selection rules as GET without a response body.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PagePath"
          },
          {
            "$ref": "#/components/parameters/AcceptHeader"
          }
        ],
        "responses": {
          "200": {
            "description": "Headers for the negotiated representation; no body, per RFC 9110.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "headers": {
              "X-Frame-Options": {
                "$ref": "#/components/headers/SecurityHeaders"
              }
            }
          },
          "301": {
            "description": "Legacy redirect."
          },
          "404": {
            "$ref": "#/components/responses/JsonNotFound"
          }
        }
      }
    },
    "/{slug}.md": {
      "get": {
        "tags": ["content"],
        "operationId": "getPageMarkdown",
        "summary": "Direct markdown asset",
        "description": "Prebuilt markdown for every page, blog post, blog index, and category enumerated by `getStaticPaths` in `src/pages/[...slug].md.ts`. Unlike Accept negotiation on the HTML URL, direct `.md` fetches remain cacheable static assets.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/SlugPath"
          }
        ],
        "responses": {
          "200": {
            "description": "Markdown document.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "headers": {
              "X-Frame-Options": {
                "$ref": "#/components/headers/SecurityHeaders"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/JsonNotFound"
          }
        }
      }
    },
    "/robots.txt": {
      "get": {
        "tags": ["feeds"],
        "operationId": "getRobotsTxt",
        "summary": "Robots policy",
        "description": "Production allows normal and AI crawlers while blocking selected SEO scrapers. Staging build policy is disallow-all; note that staging custom-domain requests are overridden by Edge's staging `/robots.txt` route.",
        "security": [],
        "responses": {
          "200": {
            "description": "Robots file.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/rss.xml": {
      "get": {
        "tags": ["feeds"],
        "operationId": "getBlogRssFeed",
        "summary": "English blog RSS feed",
        "description": "RSS XML of English blog posts sorted newest first. Item fields derive from post title, description, link, publication date, author, category, and tags.",
        "security": [],
        "responses": {
          "200": {
            "description": "RSS 2.0 feed.",
            "content": {
              "application/rss+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "tags": ["feeds"],
        "operationId": "getLlmsTxt",
        "summary": "LLM-oriented site inventory",
        "description": "Plain-text inventory of the site for LLM consumers.",
        "security": [],
        "responses": {
          "200": {
            "description": "LLM inventory.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/sitemap.md": {
      "get": {
        "tags": ["feeds"],
        "operationId": "getMarkdownSitemap",
        "summary": "Markdown sitemap",
        "description": "Human/agent-readable localized sitemap.",
        "security": [],
        "responses": {
          "200": {
            "description": "Markdown sitemap.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/service-terms.md": {
      "get": {
        "tags": ["feeds"],
        "operationId": "getServiceTermsMarkdown",
        "summary": "Service terms (markdown)",
        "description": "Markdown rendering of the English terms-of-service inventory entry.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service terms markdown.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/agent-skills/index.json": {
      "get": {
        "tags": ["feeds"],
        "operationId": "getAgentSkillsIndex",
        "summary": "Agent skills discovery document",
        "description": "JSON discovery document (cloudflare agent-skills-discovery v0.2.0) generated from the full content inventory.",
        "security": [],
        "responses": {
          "200": {
            "description": "Agent skills discovery JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentSkillsDiscovery"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": ["feeds"],
        "operationId": "getOpenApiDocument",
        "summary": "OpenAPI description of this Worker",
        "description": "This document. Served as a static asset from `public/openapi.json`: `run_worker_first` in wrangler.jsonc excludes `!/*.json`, so the request bypasses the Worker and Cloudflare serves the file with `application/json`. Referenced from `/.well-known/api-catalog` as the `service-desc` relation.",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1.0 description document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/api-catalog": {
      "get": {
        "tags": ["feeds"],
        "operationId": "getApiCatalog",
        "summary": "API catalog (RFC 9727 linkset)",
        "description": "Discovery root for agents. Built by the Worker (`src/worker.ts`) rather than prerendered, because a static asset cannot carry `Content-Type: application/linkset+json` and because every href must follow the request origin (production vs `test.nonresident.tax`). Relations: `service-desc` points at `/openapi.json`, `service-doc` at `/llms.txt`, `status` at `/internal/health`. Worker security headers are stamped on the response.",
        "security": [],
        "responses": {
          "200": {
            "description": "Linkset describing this origin's machine-readable surface.",
            "content": {
              "application/linkset+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCatalog"
                }
              }
            },
            "headers": {
              "X-Frame-Options": {
                "$ref": "#/components/headers/SecurityHeaders"
              }
            }
          }
        }
      },
      "head": {
        "tags": ["feeds"],
        "operationId": "headApiCatalog",
        "summary": "API catalog headers only",
        "description": "Identical headers to GET with no response body. Methods other than GET and HEAD are not served the catalog and fall through to the static-asset 404.",
        "security": [],
        "responses": {
          "200": {
            "description": "Catalog headers; no body, per RFC 9110.",
            "content": {
              "application/linkset+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCatalog"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": ["mcp"],
        "operationId": "postMcpJsonRpc",
        "summary": "Model Context Protocol endpoint (streamable HTTP)",
        "description": "Stateless MCP server, no authentication and no sessions. Served by a SEPARATE Cloudflare Worker (`@nt/mcp`, `apps/mcp/src/index.ts`) that owns the `nonresident.tax/mcp*` and `nonresident.tax/.well-known/mcp/*` zone path routes; path routes take precedence over this site's custom-domain Worker. POST a single JSON-RPC 2.0 message with `Content-Type: application/json` and `Accept: application/json, text/event-stream`. Tools are read-only: `list_services`, `get_service_info`, `search_guides`; each answers from this site's own machine surface (`/.well-known/agent-skills/index.json` plus the `.md` page siblings). `GET` and `DELETE` return 405 because there is no server-initiated stream and no session to terminate.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonRpcRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC result, as a JSON object or an SSE stream.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON (`-32700`) or a body that is not a JSON-RPC message (`-32600`). Answered as a JSON-RPC error envelope with `id: null`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "405": {
            "description": "Method other than POST: there is no server-initiated stream and no session to terminate. Answered as a JSON-RPC error envelope (`-32000`).",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "POST"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          }
        }
      }
    },
    "/mcp/server-card": {
      "get": {
        "tags": ["mcp"],
        "operationId": "getMcpServerCard",
        "summary": "MCP server card",
        "description": "Server card for the MCP endpoint, built by one builder in `@nt/mcp` and served byte-identically at BOTH this path (reserved by the current schema source as `<streamable-http-url>/server-card`) and `/.well-known/mcp/server-card.json` (what agent-readiness scanners probe). Carries the experimental schema's required `$schema`/`name`/`version`/`description`, a `remotes[]` transport entry, and the `serverInfo`/`capabilities` objects a scanner expects. The advertised endpoint is env-derived, so staging advertises `https://test.nonresident.tax/mcp` and production `https://nonresident.tax/mcp`.",
        "security": [],
        "responses": {
          "200": {
            "description": "Server card document.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpServerCard"
                }
              }
            }
          },
          "405": {
            "description": "Method other than GET or HEAD. Answered as a JSON-RPC error envelope (`-32000`).",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "GET, HEAD"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/mcp/server-card.json": {
      "get": {
        "tags": ["mcp"],
        "operationId": "getMcpServerCardWellKnown",
        "summary": "MCP server card (well-known path)",
        "description": "The SAME card as `/mcp/server-card`, byte-identical because one builder in `@nt/mcp` (`src/server-card.ts`) produces both responses. This is the path agent-readiness scanners probe for an MCP server, so it is documented here as a path in its own right rather than only mentioned in the sibling entry's prose. Served by the `@nt/mcp` Worker on the `nonresident.tax/.well-known/mcp/*` zone route, not by this site's Worker. `HEAD` returns identical headers with no body.",
        "security": [],
        "responses": {
          "200": {
            "description": "Server card document, byte-identical to `/mcp/server-card`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpServerCard"
                }
              }
            }
          },
          "405": {
            "description": "Method other than GET or HEAD. Answered as a JSON-RPC error envelope (`-32000`).",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "GET, HEAD"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          }
        }
      }
    },
    "/how-it-works": {
      "get": {
        "tags": ["redirects"],
        "operationId": "redirectHowItWorks",
        "summary": "Legacy redirect → /about-us",
        "description": "Representative legacy 301. Full table is `REDIRECTS` in `src/worker.ts` (kept in sync with `public/_redirects`). Trailing-slash-insensitive; query string preserved.",
        "security": [],
        "responses": {
          "301": {
            "description": "Permanent redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "const": "/about-us"
                }
              }
            }
          }
        }
      }
    },
    "/contact": {
      "get": {
        "tags": ["redirects"],
        "operationId": "redirectContact",
        "summary": "Legacy redirect → /contact-us",
        "security": [],
        "responses": {
          "301": {
            "description": "Permanent redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "const": "/contact-us"
                }
              }
            }
          }
        },
        "description": "Legacy 301 kept from the previous site so old inbound links and bookmarks resolve instead of 404ing. Trailing-slash-insensitive; the query string is preserved. The full table is `REDIRECTS` in `src/worker.ts`, mirrored in `public/_redirects`."
      }
    },
    "/formation": {
      "get": {
        "tags": ["redirects"],
        "operationId": "redirectFormation",
        "summary": "Legacy redirect → /company-formation",
        "security": [],
        "responses": {
          "301": {
            "description": "Permanent redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "const": "/company-formation"
                }
              }
            }
          }
        },
        "description": "Legacy 301 kept from the previous site so old inbound links and bookmarks resolve instead of 404ing. Trailing-slash-insensitive; the query string is preserved. The full table is `REDIRECTS` in `src/worker.ts`, mirrored in `public/_redirects`."
      }
    },
    "/ein-tax-id": {
      "get": {
        "tags": ["redirects"],
        "operationId": "redirectEinTaxId",
        "summary": "Legacy redirect → /federal-tax",
        "security": [],
        "responses": {
          "301": {
            "description": "Permanent redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "const": "/federal-tax"
                }
              }
            }
          }
        },
        "description": "Legacy 301 kept from the previous site so old inbound links and bookmarks resolve instead of 404ing. Trailing-slash-insensitive; the query string is preserved. The full table is `REDIRECTS` in `src/worker.ts`, mirrored in `public/_redirects`."
      }
    },
    "/sitemap-index.xml": {
      "get": {
        "tags": ["feeds"],
        "operationId": "getSitemapIndex",
        "summary": "XML sitemap index",
        "description": "Sitemap index for search engines. Each child sitemap carries a per-URL `lastmod` sourced from content (never build time) and hreflang alternates for the en/es/pt/fr cluster, including `x-default`.",
        "security": [],
        "responses": {
          "200": {
            "description": "Sitemap index document.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/404.md": {
      "get": {
        "tags": ["content"],
        "operationId": "getNotFoundMarkdown",
        "summary": "Markdown 404 recovery document",
        "description": "Short markdown document naming the sitemap, `llms.txt`, the developer documentation, and every machine-readable file. Fetched directly it is a `200` (the document exists). The Worker returns this same body with a real `404` status when a request that prefers `text/markdown` hits a path that does not exist.",
        "security": [],
        "responses": {
          "200": {
            "description": "Recovery document.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/developers": {
      "get": {
        "tags": ["content"],
        "operationId": "getDeveloperPortal",
        "summary": "Developer and agent portal",
        "description": "Human-readable index of everything in this document: the machine-readable files, the public API, the JSON error format, rate limits, content negotiation, and when to use Nonresident Tax. English only — the machine surface is identical in every locale.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/AcceptHeader"
          }
        ],
        "responses": {
          "200": {
            "description": "Portal page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "headers": {
              "X-Frame-Options": {
                "$ref": "#/components/headers/SecurityHeaders"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/JsonNotFound"
          }
        }
      }
    },
    "/developers.md": {
      "get": {
        "tags": ["content"],
        "operationId": "getDeveloperPortalMarkdown",
        "summary": "Developer and agent portal (markdown)",
        "description": "Markdown sibling of `/developers/`, generated from the same source, so the two can never disagree about which files this site publishes.",
        "security": [],
        "responses": {
          "200": {
            "description": "Portal document.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "tags": ["forms"],
        "operationId": "submitContactForm",
        "summary": "Open a support conversation",
        "description": "Submits the contact form and opens a support conversation. Rate limited to 5 requests per hour per IP. Requires an `Origin` on the nonresident.tax zone: this is a browser surface, so a server-to-server call is rejected with 403. Leave `website` unset; it is a honeypot.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/FormOriginHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submission accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactSuccess"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON or validation failure.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormError"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/FormForbiddenOrigin"
          },
          "405": {
            "$ref": "#/components/responses/ApiMethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/FormRateLimited"
          },
          "500": {
            "description": "Unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormError"
                }
              }
            }
          },
          "502": {
            "description": "Support backend rejected the submission.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormError"
                }
              }
            }
          },
          "503": {
            "description": "Endpoint not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormError"
                }
              }
            }
          }
        }
      },
      "options": {
        "tags": ["forms"],
        "operationId": "preflightContactForm",
        "summary": "CORS preflight for the contact endpoint",
        "description": "Answers the browser preflight. Allows POST and OPTIONS with `Content-Type`.",
        "security": [],
        "responses": {
          "204": {
            "description": "Preflight accepted."
          },
          "403": {
            "$ref": "#/components/responses/FormForbiddenOrigin"
          }
        }
      }
    },
    "/api/subscribe": {
      "post": {
        "tags": ["forms"],
        "operationId": "subscribeToNewsletter",
        "summary": "Subscribe an email address to the newsletter",
        "description": "Adds an email address to the newsletter list. Rate limited to 10 requests per hour per IP. Same Origin requirement and honeypot as the contact endpoint.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/FormOriginHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubscribeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscribed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscribeSuccess"
                }
              }
            }
          },
          "400": {
            "description": "Validation failure.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormError"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/FormForbiddenOrigin"
          },
          "405": {
            "$ref": "#/components/responses/ApiMethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/FormRateLimited"
          },
          "500": {
            "description": "Unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormError"
                }
              }
            }
          },
          "503": {
            "description": "Newsletter backend unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormError"
                }
              }
            }
          }
        }
      },
      "options": {
        "tags": ["forms"],
        "operationId": "preflightNewsletterSubscribe",
        "summary": "CORS preflight for the subscribe endpoint",
        "description": "Answers the browser preflight. Allows POST and OPTIONS with `Content-Type`.",
        "security": [],
        "responses": {
          "204": {
            "description": "Preflight accepted."
          },
          "403": {
            "$ref": "#/components/responses/FormForbiddenOrigin"
          }
        }
      }
    },
    "/api/unsubscribe": {
      "get": {
        "tags": ["forms"],
        "operationId": "getUnsubscribeConfirmation",
        "summary": "Unsubscribe confirmation page",
        "description": "Returns an HTML confirmation page carrying a POST form. GET never mutates. Deliberately has no Origin requirement, because email clients send none; the unguessable token plus a 30/hour per-IP limit is the guard. Responses are HTML rather than JSON because this URL is opened by a person clicking a link in an email.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/UnsubscribeToken"
          }
        ],
        "responses": {
          "200": {
            "description": "Confirmation page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Missing token.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/ApiMethodNotAllowed"
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": ["forms"],
        "operationId": "confirmUnsubscribe",
        "summary": "Confirm an unsubscribe",
        "description": "Unsubscribes the contact the token identifies. No request body. No Origin requirement, for the same reason as GET.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/UnsubscribeToken"
          }
        ],
        "responses": {
          "200": {
            "description": "Unsubscribed.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Missing token.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/ApiMethodNotAllowed"
          },
          "410": {
            "description": "Token no longer active.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Contact backend unavailable.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/{unknown}": {
      "parameters": [
        {
          "name": "unknown",
          "in": "path",
          "required": true,
          "description": "Any path segment under `/api/` with no endpoint behind it.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": ["forms"],
        "operationId": "getUnknownApiPath",
        "summary": "Unknown API path",
        "description": "Any request under `/api/` that matches no endpoint answers a structured JSON 404 naming the endpoints that do exist. It is never HTML and never XML.",
        "security": [],
        "responses": {
          "404": {
            "$ref": "#/components/responses/ApiNotFound"
          }
        }
      },
      "post": {
        "tags": ["forms"],
        "operationId": "postUnknownApiPath",
        "summary": "Unknown API path",
        "description": "Same structured JSON 404 as GET.",
        "security": [],
        "responses": {
          "404": {
            "$ref": "#/components/responses/ApiNotFound"
          }
        }
      }
    },
    "/docs": {
      "get": {
        "tags": ["redirects"],
        "operationId": "redirectDocs",
        "summary": "Convenience redirect → /developers/",
        "description": "`/docs`, `/developer`, and `/developer-docs` all 301 to `/developers/`. They exist because an agent looking for documentation guesses those paths before reading any discovery document. `/api` and `/api-docs` are deliberately NOT redirects: `/api/*` on this zone belongs to the @nt/edge Worker, which answers an unrouted path with an `ApiError` JSON body.",
        "security": [],
        "responses": {
          "301": {
            "description": "Permanent redirect.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "const": "/developers/"
                }
              }
            }
          }
        }
      }
    }
  }
}
