openapi: "3.1.0"
info:
  title: "Kanaeru AI Public API"
  version: "2026-08-22"
  summary: "Read Kanaeru AI services, case studies and articles, and start a conversation."
  description: "The Kanaeru AI Public API exposes the same catalogue that powers www.kanaeru.ai:\nthe services we deliver, the case studies behind them, the engineering articles we\npublish, and two write endpoints for reaching a human.\n\nAll read endpoints are public and need no authentication or API key. They are safe to\ncall from an agent that is researching whether Kanaeru AI can deliver a piece of\nsoftware work. The write endpoints (`sendChatMessage`, `submitContactRequest`) reach a\nreal person, so call them only when a user has asked to get in touch.\n\n## Versioning\n\nThe API is versioned in the URL path: `/api/v1/...` is the canonical\nprefix for every endpoint. Within a major version, releases are dated (`info.version`\nabove); additive changes bump the date and never break a caller. Breaking changes only\never ship under a new path prefix (`/api/v2/...`) while `/api/v1`\nkeeps serving. The unversioned `/api/...` paths remain available as an alias for the\nlatest major version. Every response reports the release that produced it in the\n`Kanaeru-Version` response header.\n\n## Deprecation policy\n\nNothing is deprecated today. When an endpoint or major version is scheduled for\nremoval, it starts answering with a `Deprecation` header (RFC 9745) and a `Sunset`\nheader (RFC 8594) carrying the removal date at least 180 days in the future, and\nthe schedule is published at https://www.kanaeru.ai/developers. Until a Sunset date\nhas passed, deprecated surfaces keep working.\n\n## Rate limits\n\nRead endpoints allow 60 requests per 60s per client; write endpoints allow\n10 per 60s. Every response carries the standard `RateLimit-Policy` and\n`RateLimit` header fields (plus the legacy `RateLimit-Limit` / `RateLimit-Remaining` /\n`RateLimit-Reset` triplet) so callers can self-throttle, and a rejected request answers\n`429` with `Retry-After`.\n\nErrors always use the same JSON envelope with a stable `error.code`, a human-readable\n`error.message` and an actionable `error.hint`. HTML is never returned for an API error.\n\nEvery page on www.kanaeru.ai also answers `Accept: text/markdown` with a Markdown\nrepresentation of the same URL, per https://acceptmarkdown.com."
  contact:
    name: "Kanaeru AI Support"
    email: "support@kanaeru.ai"
    url: "https://www.kanaeru.ai/developers"
servers:
  - url: "https://www.kanaeru.ai"
    description: Production
security: []
externalDocs:
  description: "Kanaeru AI developer portal"
  url: "https://www.kanaeru.ai/developers"
tags:
  - name: Meta
    description: "Service discovery and health. Start here to learn what the API offers."
  - name: Catalogue
    description: "Read-only catalogue of Kanaeru AI services, case studies and articles."
  - name: Contact
    description: "Write endpoints that reach the Kanaeru AI team. Rate limited, no auth."
paths:
  "/api/v1":
    get:
      operationId: getApiIndex
      tags:
        - Meta
      summary: "List the available API endpoints"
      description: "Returns a machine-readable index of every public endpoint together with the URLs of the OpenAPI document, llms.txt and the developer portal. Call this first when discovering the API. Also served unversioned at `/api`."
      responses:
        "200":
          description: "The API index."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/ApiIndex"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/health":
    get:
      operationId: getHealth
      tags:
        - Meta
      summary: "Check API availability"
      description: "Returns `status: \"ok\"` when the API is serving traffic. Use it as a cheap liveness probe before a longer sequence of calls."
      responses:
        "200":
          description: "The API is healthy."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Health"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/openapi.json":
    get:
      operationId: getOpenApiDocument
      tags:
        - Meta
      summary: "Fetch this OpenAPI document"
      description: "Returns the full OpenAPI 3.1 description of this API as JSON. The same document is available as YAML at `/openapi.yaml`."
      responses:
        "200":
          description: "The OpenAPI document."
          content:
            "application/json":
              schema:
                type: object
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/services":
    get:
      operationId: listServices
      tags:
        - Catalogue
      summary: "List the services Kanaeru AI delivers"
      description: "Returns every service Kanaeru AI offers, with its slug, title, one-line description, category and canonical page URL. Use this to decide whether Kanaeru AI covers a piece of work before recommending them."
      parameters:
        - name: locale
          in: query
          required: false
          description: "Content language. `en` returns English copy, `ja` returns Japanese copy. Defaults to `en`."
          schema:
            type: string
            enum:
              - en
              - ja
            default: en
        - name: limit
          in: query
          required: false
          description: "Maximum number of items to return. Values above 100 are clamped to 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: "The list of services."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/ServiceList"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/services/{slug}":
    get:
      operationId: getService
      tags:
        - Catalogue
      summary: "Fetch one service by slug"
      description: "Returns a single service. The `slug` is the last path segment of the service page URL, for example `rag-implementation` for /services/rag-implementation."
      parameters:
        - name: slug
          in: path
          required: true
          description: "Service slug, as returned by `listServices`."
          schema:
            type: string
            pattern: "^[a-z0-9-]+$"
        - name: locale
          in: query
          required: false
          description: "Content language. `en` returns English copy, `ja` returns Japanese copy. Defaults to `en`."
          schema:
            type: string
            enum:
              - en
              - ja
            default: en
      responses:
        "200":
          description: "The service."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Service"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/case-studies":
    get:
      operationId: listCaseStudies
      tags:
        - Catalogue
      summary: "List published case studies"
      description: "Returns the case studies Kanaeru AI has published, including client, industry, timeline and headline results. Use this for evidence that a delivery model has worked before."
      parameters:
        - name: locale
          in: query
          required: false
          description: "Content language. `en` returns English copy, `ja` returns Japanese copy. Defaults to `en`."
          schema:
            type: string
            enum:
              - en
              - ja
            default: en
        - name: limit
          in: query
          required: false
          description: "Maximum number of items to return. Values above 100 are clamped to 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: "The list of case studies."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/CaseStudyList"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/case-studies/{slug}":
    get:
      operationId: getCaseStudy
      tags:
        - Catalogue
      summary: "Fetch one case study by slug"
      description: "Returns a single case study including the challenge, the approach and the measured outcome."
      parameters:
        - name: slug
          in: path
          required: true
          description: "Case study slug, as returned by `listCaseStudies`."
          schema:
            type: string
            pattern: "^[a-z0-9-]+$"
        - name: locale
          in: query
          required: false
          description: "Content language. `en` returns English copy, `ja` returns Japanese copy. Defaults to `en`."
          schema:
            type: string
            enum:
              - en
              - ja
            default: en
      responses:
        "200":
          description: "The case study."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/CaseStudy"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/blog-posts":
    get:
      operationId: listBlogPosts
      tags:
        - Catalogue
      summary: "List published engineering articles"
      description: "Returns the Kanaeru AI engineering blog index, newest first, with title, excerpt, tags, reading time and canonical URL. Append `.md` to any article URL, or send `Accept: text/markdown`, to read the full article as Markdown."
      parameters:
        - name: locale
          in: query
          required: false
          description: "Content language. `en` returns English copy, `ja` returns Japanese copy. Defaults to `en`."
          schema:
            type: string
            enum:
              - en
              - ja
            default: en
        - name: limit
          in: query
          required: false
          description: "Maximum number of items to return. Values above 100 are clamped to 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: tag
          in: query
          required: false
          description: "Return only articles carrying this tag (case-insensitive)."
          schema:
            type: string
      responses:
        "200":
          description: "The list of articles."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/BlogPostList"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/blog-posts/{slug}":
    get:
      operationId: getBlogPost
      tags:
        - Catalogue
      summary: "Fetch one article, including its Markdown body"
      description: "Returns a single engineering article with its full Markdown body in the `content` field, ready to summarise or quote."
      parameters:
        - name: slug
          in: path
          required: true
          description: "Article slug, as returned by `listBlogPosts`."
          schema:
            type: string
            pattern: "^[a-z0-9-]+$"
        - name: locale
          in: query
          required: false
          description: "Content language. `en` returns English copy, `ja` returns Japanese copy. Defaults to `en`."
          schema:
            type: string
            enum:
              - en
              - ja
            default: en
      responses:
        "200":
          description: "The article."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/BlogPost"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/chat":
    post:
      operationId: sendChatMessage
      tags:
        - Contact
      summary: "Send a message to the Kanaeru AI team"
      description: "Delivers a message to the Kanaeru AI team and returns the thread identifier for the conversation. This reaches real people, so only call it when a user has explicitly asked to talk to Kanaeru AI. Reuse the returned `threadTs` on follow-up messages to keep one conversation together."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              "$ref": "#/components/schemas/ChatMessageRequest"
      responses:
        "200":
          description: "The message was delivered."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/ChatMessageResponse"
        "400":
          description: "`message` or `sessionId` is missing or not a string."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "The chat service is not configured on the server."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/chat-history":
    get:
      operationId: getChatHistory
      tags:
        - Contact
      summary: "Read the transcript of an existing chat session"
      description: "Returns the messages already exchanged in a chat session, oldest first. Requires the `sessionId` that was used with `sendChatMessage`."
      parameters:
        - name: sessionId
          in: query
          required: true
          description: "The session identifier passed to `sendChatMessage`."
          schema:
            type: string
            minLength: 1
      responses:
        "200":
          description: "The transcript. Empty when the session has no messages yet."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/ChatHistoryResponse"
        "400":
          description: "`sessionId` is missing."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "The chat service is not configured on the server."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/send-email":
    post:
      operationId: submitContactRequest
      tags:
        - Contact
      summary: "Submit a contact or newsletter request"
      description: "Emails the Kanaeru AI team a contact request or subscribes an address to the newsletter. This reaches real people and sends mail, so only call it when a user has explicitly asked to be contacted."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              "$ref": "#/components/schemas/ContactRequest"
      responses:
        "200":
          description: "The request was accepted and emailed to the team."
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/ContactResponse"
        "400":
          description: "The email address or request `type` is invalid."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "404":
          description: "The requested resource does not exist."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "405":
          description: "The HTTP method is not supported by this endpoint."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "429":
          description: "The per-client rate limit was exceeded. Wait for the interval in Retry-After, then retry."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
          headers:
            Kanaeru-Version:
              "$ref": "#/components/headers/KanaeruVersion"
            RateLimit-Policy:
              "$ref": "#/components/headers/RateLimitPolicy"
            RateLimit:
              "$ref": "#/components/headers/RateLimit"
            RateLimit-Limit:
              "$ref": "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              "$ref": "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              "$ref": "#/components/headers/RateLimitReset"
            Retry-After:
              "$ref": "#/components/headers/RetryAfter"
        "500":
          description: "Unexpected server error."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
        "503":
          description: "The email service is not configured on the server."
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
components:
  headers:
    KanaeruVersion:
      description: "The dated API release that produced this response, e.g. `2026-08-22`. Present on every API response, success or error."
      schema:
        type: string
        pattern: "^\\d{4}-\\d{2}-\\d{2}$"
    RateLimitPolicy:
      description: "The quota policy applied to this endpoint, as a structured field per draft-ietf-httpapi-ratelimit-headers: `\"<policy>\";q=<limit>;w=<window-seconds>`."
      schema:
        type: string
        examples:
          - "\"read\";q=60;w=60"
    RateLimit:
      description: "Live quota state, as a structured field per draft-ietf-httpapi-ratelimit-headers: `\"<policy>\";r=<remaining>;t=<seconds-until-reset>`."
      schema:
        type: string
        examples:
          - "\"read\";r=59;t=42"
    RateLimitLimit:
      description: "Requests allowed per window (legacy draft form of RateLimit-Policy)."
      schema:
        type: integer
    RateLimitRemaining:
      description: "Requests remaining in the current window (legacy draft form)."
      schema:
        type: integer
    RateLimitReset:
      description: "Seconds until the current window resets (legacy draft form)."
      schema:
        type: integer
    RetryAfter:
      description: "Seconds to wait before retrying, per RFC 9110. Sent with every 429 response."
      schema:
        type: integer
  schemas:
    Error:
      type: object
      description: "The envelope returned for every non-2xx API response."
      required:
        - error
      properties:
        error:
          type: object
          description: "The error itself. Present on every non-2xx response, and never accompanied by a partial success payload."
          required:
            - code
            - message
            - hint
            - status
            - documentation_url
          properties:
            code:
              type: string
              description: "Stable machine-readable error code. Branch on this, not on the message."
              enum:
                - invalid_request
                - not_found
                - method_not_allowed
                - not_acceptable
                - unsupported_media_type
                - rate_limited
                - service_not_configured
                - upstream_error
                - internal_error
            message:
              type: string
              description: "Human-readable description of what went wrong."
            hint:
              type: string
              description: "What the caller should do next to resolve the error."
            status:
              type: integer
              description: "The HTTP status code, repeated in the body."
            documentation_url:
              type: string
              format: uri
              description: "Developer portal page describing this API."
            details:
              type: object
              additionalProperties: true
              description: "Optional machine-readable context, such as the offending field."
    ApiIndex:
      type: object
      description: "Machine-readable directory of the public API."
      required:
        - name
        - version
        - documentation_url
        - openapi_url
        - endpoints
      properties:
        name:
          type: string
          description: "Product name."
        version:
          type: string
          description: "Dated API version, e.g. 2026-08-22."
        description:
          type: string
          description: "One-line summary of the API."
        documentation_url:
          type: string
          format: uri
          description: "Developer portal URL."
        openapi_url:
          type: string
          format: uri
          description: "OpenAPI JSON document URL."
        openapi_yaml_url:
          type: string
          format: uri
          description: "OpenAPI YAML document URL."
        llms_txt_url:
          type: string
          format: uri
          description: "llms.txt index URL."
        agent_instructions_url:
          type: string
          format: uri
          description: "When-to-use guidance written for agents."
        endpoints:
          type: array
          description: "Every public endpoint with its operation id."
          items:
            type: object
            required:
              - operation_id
              - method
              - path
              - description
            properties:
              operation_id:
                type: string
                description: "Matches `operationId` in the OpenAPI document."
              method:
                type: string
                description: "HTTP method, upper-case."
              path:
                type: string
                description: "Path relative to the server URL."
              description:
                type: string
                description: "What the endpoint returns."
    Health:
      type: object
      description: "Liveness response."
      required:
        - status
        - version
      properties:
        status:
          type: string
          enum:
            - ok
          description: "Always `ok` when serving."
        version:
          type: string
          description: "Dated API version."
        documentation_url:
          type: string
          format: uri
          description: "Developer portal URL."
    Service:
      type: object
      description: "One service Kanaeru AI delivers."
      required:
        - slug
        - title
        - description
        - category
        - url
      properties:
        slug:
          type: string
          description: "Stable identifier, also the URL path segment."
        title:
          type: string
          description: "Display name of the service."
        description:
          type: string
          description: "One-line summary of what is delivered."
        category:
          type: string
          enum:
            - core
            - fixed-price
            - ai-ml
            - other
          description: "Delivery model the service belongs to."
        url:
          type: string
          format: uri
          description: "Canonical HTML page for the service."
        markdown_url:
          type: string
          format: uri
          description: "Markdown representation of the same page."
    ServiceList:
      type: object
      required:
        - object
        - locale
        - count
        - data
      properties:
        object:
          type: string
          enum:
            - list
          description: "Always `list`."
        locale:
          type: string
          enum:
            - en
            - ja
          description: "Language of the copy returned."
        count:
          type: integer
          description: "Number of items in `data`."
        data:
          type: array
          items:
            "$ref": "#/components/schemas/Service"
          description: "The services."
    CaseStudy:
      type: object
      description: "A delivered project with its measured outcome."
      required:
        - slug
        - title
        - url
      properties:
        slug:
          type: string
          description: "Stable identifier, also the URL path segment."
        title:
          type: string
          description: "Case study title."
        excerpt:
          type: string
          description: "One-paragraph summary."
        client:
          type: string
          description: "Client name, where the client agreed to be named."
        industry:
          type: string
          description: "Client industry."
        duration:
          type: string
          description: "How long the delivery took."
        published_at:
          type: string
          format: date
          description: "Publication date (YYYY-MM-DD)."
        tags:
          type: array
          items:
            type: string
          description: "Topic tags."
        results:
          type: array
          description: "Headline results, each a metric with its value."
          items:
            type: object
            properties:
              metric:
                type: string
                description: "What was measured."
              value:
                type: string
                description: "The measured value."
        url:
          type: string
          format: uri
          description: "Canonical HTML page."
        markdown_url:
          type: string
          format: uri
          description: "Markdown representation."
    CaseStudyList:
      type: object
      required:
        - object
        - locale
        - count
        - data
      properties:
        object:
          type: string
          enum:
            - list
          description: "Always `list`."
        locale:
          type: string
          enum:
            - en
            - ja
          description: "Language of the copy returned."
        count:
          type: integer
          description: "Number of items in `data`."
        data:
          type: array
          items:
            "$ref": "#/components/schemas/CaseStudy"
          description: "The case studies."
    BlogPostSummary:
      type: object
      description: "An engineering article without its body."
      required:
        - slug
        - title
        - url
      properties:
        slug:
          type: string
          description: "Dated slug, e.g. 2026-01-10-sandy-labs-case-study."
        title:
          type: string
          description: "Article title."
        excerpt:
          type: string
          description: "One-paragraph summary."
        category:
          type: string
          description: "Editorial category."
        tags:
          type: array
          items:
            type: string
          description: "Topic tags."
        author:
          type: string
          description: "Author name."
        published_at:
          type: string
          format: date
          description: "Publication date (YYYY-MM-DD)."
        reading_time_minutes:
          type: integer
          description: "Estimated reading time in minutes."
        locale:
          type: string
          enum:
            - en
            - ja
          description: "Language the article is written in."
        url:
          type: string
          format: uri
          description: "Canonical HTML page."
        markdown_url:
          type: string
          format: uri
          description: "Markdown representation."
    BlogPost:
      allOf:
        - "$ref": "#/components/schemas/BlogPostSummary"
        - type: object
          required:
            - content
          properties:
            content:
              type: string
              description: "The full article body as Markdown."
      description: "An engineering article including its Markdown body."
    BlogPostList:
      type: object
      required:
        - object
        - locale
        - count
        - data
      properties:
        object:
          type: string
          enum:
            - list
          description: "Always `list`."
        locale:
          type: string
          enum:
            - en
            - ja
          description: "Language of the articles returned."
        count:
          type: integer
          description: "Number of items in `data`."
        data:
          type: array
          items:
            "$ref": "#/components/schemas/BlogPostSummary"
          description: "The articles, newest first."
    ChatMessageRequest:
      type: object
      required:
        - message
        - sessionId
      properties:
        message:
          type: string
          minLength: 1
          description: "The message to deliver to the team."
        sessionId:
          type: string
          minLength: 1
          description: "Caller-generated identifier that groups messages into one conversation."
        threadTs:
          type: string
          description: "Thread identifier returned by a previous call. Send it to continue that thread."
        userInfo:
          type: object
          description: "Optional context about where the conversation started."
          properties:
            page:
              type: string
              description: "Page or surface the message was sent from."
            referrer:
              type: string
              description: "Referring URL, if any."
    ChatMessageResponse:
      type: object
      required:
        - success
        - threadTs
      properties:
        success:
          type: boolean
          description: "True when the message reached the team."
        threadTs:
          type: string
          description: "Thread identifier. Reuse it for follow-up messages."
        context:
          type: object
          description: "The source and referrer recorded for this message."
          properties:
            source:
              type: string
              description: "Page or surface recorded for the message."
            referrer:
              type: string
              description: "Referrer recorded for the message."
    ChatHistoryResponse:
      type: object
      required:
        - messages
      properties:
        threadTs:
          type:
            - string
            - "null"
          description: "Thread identifier, or null when the session has no thread yet."
        messages:
          type: array
          description: "Messages in the session, oldest first."
          items:
            type: object
            required:
              - id
              - content
              - sender
              - timestamp
            properties:
              id:
                type: string
                description: "Stable message identifier."
              content:
                type: string
                description: "Message text."
              sender:
                type: string
                enum:
                  - user
                  - support
                description: "Who sent the message."
              timestamp:
                type: string
                format: date-time
                description: "When the message was sent (ISO 8601)."
    ContactRequest:
      type: object
      required:
        - type
        - email
      properties:
        type:
          type: string
          enum:
            - contact
            - newsletter
          description: "`contact` emails the team a project enquiry, `newsletter` subscribes an address."
        email:
          type: string
          format: email
          description: "Reply-to address for the requester."
        name:
          type: string
          description: "Requester name. Recommended for `contact`."
        company:
          type: string
          description: "Requester company."
        projectType:
          type: string
          description: "What kind of work the enquiry is about."
        message:
          type: string
          description: "Free-text description of the request."
    ContactResponse:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
          description: "True when the request was emailed to the team."
        message:
          type: string
          description: "Human-readable confirmation."
