{
  "openapi": "3.1.0",
  "info": {
    "title": "yonyon.ai public API",
    "version": "1.0.0",
    "description": "Public, unauthenticated endpoints an AI agent can call on yonyon.ai. No API key, OAuth, or client registration. The only constraint is rate: 10 requests per IP per minute. Versioning: this is v1; breaking changes ship under a new path prefix and the previous version stays available for at least 6 months after a successor is announced.\n\nUnmatched /api/* paths return this Error envelope with code=not_found (HTTP 404).",
    "contact": {
      "name": "Yonatan Gross",
      "email": "yonaigross@gmail.com",
      "url": "https://yonyon.ai"
    }
  },
  "servers": [
    {
      "url": "https://yonyon.ai"
    }
  ],
  "paths": {
    "/api/chat": {
      "post": {
        "operationId": "askYonatan",
        "summary": "Ask a question about Yonatan Gross (streaming)",
        "description": "Streams a grounded answer about Yonatan's work, projects, services, and experience. Public and rate-limited (10 requests/IP/minute). No authentication required. Browser-facing: requests must carry an Origin matching the host (CSRF). For server-side agents, prefer POST /ask.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A streamed plain-text answer (concatenate the chunks).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "description": "CSRF rejected (Origin missing or not allowed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/ask": {
      "post": {
        "operationId": "askNlweb",
        "summary": "Natural-language query (NLWeb)",
        "description": "Microsoft NLWeb endpoint. Agent-facing single-shot query over the same RAG + model that powers the site chat. No Origin/CSRF requirement. Returns JSON with `_meta` { response_type, version } by default; returns Server-Sent Events (start / result / complete) when the caller sends `Accept: text/event-stream`, `?streaming=true`, or `{ \"prefer\": { \"streaming\": true } }`. Rate-limited (10 requests/IP/minute).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AskRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON answer (default) or text/event-stream when streaming is requested.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskResponse"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      },
      "get": {
        "operationId": "askDescriptor",
        "summary": "NLWeb endpoint descriptor",
        "description": "Returns a small JSON descriptor of how to call POST /ask.",
        "responses": {
          "200": {
            "description": "Endpoint descriptor.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Requests allowed per window.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests remaining in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "Epoch seconds when the current window resets.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "schemas": {
      "ChatRequest": {
        "type": "object",
        "required": ["messages"],
        "properties": {
          "messages": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "object",
              "required": ["role", "content"],
              "properties": {
                "role": {
                  "type": "string",
                  "enum": ["user", "assistant"]
                },
                "content": {
                  "type": "string",
                  "maxLength": 2000
                }
              }
            }
          }
        }
      },
      "AskRequest": {
        "type": "object",
        "required": ["query"],
        "properties": {
          "query": {
            "type": "string",
            "maxLength": 2000,
            "description": "The natural-language question."
          },
          "prefer": {
            "type": "object",
            "properties": {
              "streaming": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "AskResponse": {
        "type": "object",
        "required": ["_meta", "query", "answer", "results"],
        "properties": {
          "@context": {
            "type": "string",
            "const": "https://schema.org"
          },
          "_meta": {
            "type": "object",
            "required": ["response_type", "version"],
            "properties": {
              "response_type": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "query": {
            "type": "string"
          },
          "answer": {
            "type": "string"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "@type": {
                  "type": "string"
                },
                "text": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "Typed error envelope returned for every non-2xx response.",
        "required": ["error", "code"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error token.",
            "enum": [
              "invalid_request",
              "rate_limited",
              "unavailable",
              "internal_error",
              "forbidden",
              "not_found"
            ]
          },
          "retry_after": {
            "type": "integer",
            "description": "Seconds to wait before retrying (present on rate_limited)."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed request (invalid JSON, validation failure, or blocked content).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded (10 requests/IP/minute).",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unavailable": {
        "description": "The assistant is temporarily unavailable (model backend not configured/reachable).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
