{"openapi":"3.1.0","info":{"title":"dataacrobat.com — Ask Jay API","version":"1.1.0","description":"Public access to Jay Ozer's portfolio assistant. No API key or account is required. Ask Jay answers from published portfolio content and streams Server-Sent Events. Requests are rate limited by IP; a 429 response means the caller should wait before retrying. Use /api/v1 for version 1. Compatible additions stay in v1; breaking changes use a new major URL. The existing /api/ask URL remains a supported compatibility alias. See /developers for the versioning policy.","contact":{"name":"Jay Ozer","url":"https://www.dataacrobat.com/#contact"}},"servers":[{"url":"https://www.dataacrobat.com","description":"Public portfolio"}],"externalDocs":{"description":"Developer documentation and MCP setup","url":"https://www.dataacrobat.com/developers"},"security":[],"tags":[{"name":"Portfolio","description":"Read public portfolio information and ask questions about Jay."}],"paths":{"/api/v1":{"get":{"operationId":"getPublicApiIndex","tags":["Portfolio"],"summary":"Discover the public API","description":"Get documentation links, the OpenAPI URL, the Ask Jay endpoint, and MCP discovery links.","responses":{"200":{"description":"Public API discovery information.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiIndex"},"example":{"name":"dataacrobat.com public API","version":"1.1.0","apiVersion":"v1","baseUrl":"https://www.dataacrobat.com/api/v1","versioningPolicy":"https://www.dataacrobat.com/developers","compatibilityAliases":["https://www.dataacrobat.com/api/ask"],"description":"Ask questions about Jay Ozer's published work and background.","documentation":"https://www.dataacrobat.com/developers","openapi":"https://www.dataacrobat.com/openapi.json","authentication":{"type":"none"},"endpoints":[{"method":"POST","url":"https://www.dataacrobat.com/api/v1/ask","description":"Ask Jay; returns Server-Sent Events."},{"method":"GET","url":"https://www.dataacrobat.com/api/v1/ask","description":"Read the current chat quota without consuming it."}],"mcp":{"url":"https://www.dataacrobat.com/mcp","serverCard":"https://www.dataacrobat.com/.well-known/mcp/server-card.json"}}}}}}}},"/api/v1/ask":{"get":{"operationId":"getAskJayQuota","tags":["Portfolio"],"summary":"Read the caller's Ask Jay quota","description":"Inspect the current IP-based chat quota without invoking the AI or consuming a request.","responses":{"200":{"description":"Current chat quota, also exposed in the response headers. This response is not cached.","headers":{"API-Version":{"description":"The supported API major version.","schema":{"type":"string","const":"1"}},"RateLimit-Policy":{"description":"The enforced quota and window in seconds, for example: \"chat\";q=30;w=60.","schema":{"type":"string"}},"RateLimit":{"description":"Remaining requests (r) and seconds until the next quota release (t).","schema":{"type":"string"}},"RateLimit-Limit":{"description":"Maximum requests in this policy's window.","schema":{"type":"integer","minimum":0}},"RateLimit-Remaining":{"description":"Requests remaining for the caller's IP.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until the next quota release.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskQuota"}}}}}},"post":{"operationId":"askJay","tags":["Portfolio"],"summary":"Ask Jay about his work and background","description":"Send a question or a conversation of 1–50 messages. When both are present, messages takes precedence. The response is a text/event-stream, not a JSON document. Each SSE data field contains a JSON status, content, done, or error event; [DONE] marks the end. Errors after streaming starts arrive as error events within the HTTP 200 stream. No credentials are required.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","anyOf":[{"$ref":"#/components/schemas/QuestionRequest"},{"$ref":"#/components/schemas/ConversationRequest"}]},"examples":{"question":{"summary":"Ask a single question","value":{"question":"What has Jay built?"}},"conversation":{"summary":"Send conversation history","value":{"messages":[{"role":"user","content":"What has Jay built?"}]}}}}}},"responses":{"200":{"description":"SSE stream with status, content, done, and error events, followed by [DONE].","headers":{"API-Version":{"description":"The supported API major version.","schema":{"type":"string","const":"1"}},"RateLimit-Policy":{"description":"The enforced quota and window in seconds, for example: \"chat\";q=30;w=60.","schema":{"type":"string"}},"RateLimit":{"description":"Remaining requests (r) and seconds until the next quota release (t).","schema":{"type":"string"}},"RateLimit-Limit":{"description":"Maximum requests in this policy's window.","schema":{"type":"integer","minimum":0}},"RateLimit-Remaining":{"description":"Requests remaining for the caller's IP.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until the next quota release.","schema":{"type":"integer","minimum":1}}},"content":{"text/event-stream":{"schema":{"type":"string","description":"SSE frames separated by blank lines. Parse the data field of each frame."},"example":"data: {\"type\":\"status\",\"status\":\"thinking\"}\n\ndata: {\"type\":\"content\",\"content\":\"I build production AI systems.\"}\n\ndata: [DONE]\n\n"}}},"422":{"description":"Invalid JSON or request validation failure, such as an empty or oversized message.","headers":{"API-Version":{"description":"The supported API major version.","schema":{"type":"string","const":"1"}},"RateLimit-Policy":{"description":"The enforced quota and window in seconds, for example: \"chat\";q=30;w=60.","schema":{"type":"string"}},"RateLimit":{"description":"Remaining requests (r) and seconds until the next quota release (t).","schema":{"type":"string"}},"RateLimit-Limit":{"description":"Maximum requests in this policy's window.","schema":{"type":"integer","minimum":0}},"RateLimit-Remaining":{"description":"Requests remaining for the caller's IP.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until the next quota release.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"429":{"description":"IP rate limit reached. Honor Retry-After before retrying; job analysis has an additional hourly limit.","headers":{"API-Version":{"description":"The supported API major version.","schema":{"type":"string","const":"1"}},"RateLimit-Policy":{"description":"The enforced quota and window in seconds, for example: \"chat\";q=30;w=60.","schema":{"type":"string"}},"RateLimit":{"description":"Remaining requests (r) and seconds until the next quota release (t).","schema":{"type":"string"}},"RateLimit-Limit":{"description":"Maximum requests in this policy's window.","schema":{"type":"integer","minimum":0}},"RateLimit-Remaining":{"description":"Requests remaining for the caller's IP.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until the next quota release.","schema":{"type":"integer","minimum":1}},"Retry-After":{"description":"Seconds to wait before retrying the request.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttpError"},"example":{"detail":"Rate limit exceeded. Please wait before trying again."}}}},"500":{"description":"Unexpected failure before streaming starts. Internal details are not exposed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttpError"}}}}}}},"/api":{"get":{"operationId":"getPublicApiIndexLegacy","tags":["Portfolio"],"summary":"Discover the public API through the compatibility index","description":"Get documentation links, the OpenAPI URL, the Ask Jay endpoint, and MCP discovery links.","responses":{"200":{"description":"Public API discovery information.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiIndex"},"example":{"name":"dataacrobat.com public API","version":"1.1.0","apiVersion":"v1","baseUrl":"https://www.dataacrobat.com/api/v1","versioningPolicy":"https://www.dataacrobat.com/developers","compatibilityAliases":["https://www.dataacrobat.com/api/ask"],"description":"Ask questions about Jay Ozer's published work and background.","documentation":"https://www.dataacrobat.com/developers","openapi":"https://www.dataacrobat.com/openapi.json","authentication":{"type":"none"},"endpoints":[{"method":"POST","url":"https://www.dataacrobat.com/api/v1/ask","description":"Ask Jay; returns Server-Sent Events."},{"method":"GET","url":"https://www.dataacrobat.com/api/v1/ask","description":"Read the current chat quota without consuming it."}],"mcp":{"url":"https://www.dataacrobat.com/mcp","serverCard":"https://www.dataacrobat.com/.well-known/mcp/server-card.json"}}}}}}}},"/api/ask":{"get":{"operationId":"getAskJayQuotaLegacy","tags":["Portfolio"],"summary":"Read chat quota through the supported compatibility alias","description":"Inspect the current IP-based chat quota without invoking the AI or consuming a request.","responses":{"200":{"description":"Current chat quota, also exposed in the response headers. This response is not cached.","headers":{"API-Version":{"description":"The supported API major version.","schema":{"type":"string","const":"1"}},"RateLimit-Policy":{"description":"The enforced quota and window in seconds, for example: \"chat\";q=30;w=60.","schema":{"type":"string"}},"RateLimit":{"description":"Remaining requests (r) and seconds until the next quota release (t).","schema":{"type":"string"}},"RateLimit-Limit":{"description":"Maximum requests in this policy's window.","schema":{"type":"integer","minimum":0}},"RateLimit-Remaining":{"description":"Requests remaining for the caller's IP.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until the next quota release.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskQuota"}}}}}},"post":{"operationId":"askJayLegacy","tags":["Portfolio"],"summary":"Ask Jay through the supported compatibility alias","description":"Send a question or a conversation of 1–50 messages. When both are present, messages takes precedence. The response is a text/event-stream, not a JSON document. Each SSE data field contains a JSON status, content, done, or error event; [DONE] marks the end. Errors after streaming starts arrive as error events within the HTTP 200 stream. No credentials are required.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","anyOf":[{"$ref":"#/components/schemas/QuestionRequest"},{"$ref":"#/components/schemas/ConversationRequest"}]},"examples":{"question":{"summary":"Ask a single question","value":{"question":"What has Jay built?"}},"conversation":{"summary":"Send conversation history","value":{"messages":[{"role":"user","content":"What has Jay built?"}]}}}}}},"responses":{"200":{"description":"SSE stream with status, content, done, and error events, followed by [DONE].","headers":{"API-Version":{"description":"The supported API major version.","schema":{"type":"string","const":"1"}},"RateLimit-Policy":{"description":"The enforced quota and window in seconds, for example: \"chat\";q=30;w=60.","schema":{"type":"string"}},"RateLimit":{"description":"Remaining requests (r) and seconds until the next quota release (t).","schema":{"type":"string"}},"RateLimit-Limit":{"description":"Maximum requests in this policy's window.","schema":{"type":"integer","minimum":0}},"RateLimit-Remaining":{"description":"Requests remaining for the caller's IP.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until the next quota release.","schema":{"type":"integer","minimum":1}}},"content":{"text/event-stream":{"schema":{"type":"string","description":"SSE frames separated by blank lines. Parse the data field of each frame."},"example":"data: {\"type\":\"status\",\"status\":\"thinking\"}\n\ndata: {\"type\":\"content\",\"content\":\"I build production AI systems.\"}\n\ndata: [DONE]\n\n"}}},"422":{"description":"Invalid JSON or request validation failure, such as an empty or oversized message.","headers":{"API-Version":{"description":"The supported API major version.","schema":{"type":"string","const":"1"}},"RateLimit-Policy":{"description":"The enforced quota and window in seconds, for example: \"chat\";q=30;w=60.","schema":{"type":"string"}},"RateLimit":{"description":"Remaining requests (r) and seconds until the next quota release (t).","schema":{"type":"string"}},"RateLimit-Limit":{"description":"Maximum requests in this policy's window.","schema":{"type":"integer","minimum":0}},"RateLimit-Remaining":{"description":"Requests remaining for the caller's IP.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until the next quota release.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"429":{"description":"IP rate limit reached. Honor Retry-After before retrying; job analysis has an additional hourly limit.","headers":{"API-Version":{"description":"The supported API major version.","schema":{"type":"string","const":"1"}},"RateLimit-Policy":{"description":"The enforced quota and window in seconds, for example: \"chat\";q=30;w=60.","schema":{"type":"string"}},"RateLimit":{"description":"Remaining requests (r) and seconds until the next quota release (t).","schema":{"type":"string"}},"RateLimit-Limit":{"description":"Maximum requests in this policy's window.","schema":{"type":"integer","minimum":0}},"RateLimit-Remaining":{"description":"Requests remaining for the caller's IP.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until the next quota release.","schema":{"type":"integer","minimum":1}},"Retry-After":{"description":"Seconds to wait before retrying the request.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttpError"},"example":{"detail":"Rate limit exceeded. Please wait before trying again."}}}},"500":{"description":"Unexpected failure before streaming starts. Internal details are not exposed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttpError"}}}}}}}},"components":{"schemas":{"ApiIndex":{"type":"object","required":["name","version","description","documentation","openapi","authentication","endpoints","mcp"],"properties":{"name":{"type":"string"},"version":{"type":"string"},"description":{"type":"string"},"documentation":{"type":"string","format":"uri"},"openapi":{"type":"string","format":"uri"},"authentication":{"type":"object","required":["type"],"properties":{"type":{"type":"string","const":"none"}}},"endpoints":{"type":"array","items":{"type":"object","required":["method","url","description"],"properties":{"method":{"type":"string","enum":["POST","GET"]},"url":{"type":"string","format":"uri"},"description":{"type":"string"}}}},"mcp":{"type":"object","required":["url","serverCard"],"properties":{"url":{"type":"string","format":"uri"},"serverCard":{"type":"string","format":"uri"}}}}},"QuestionRequest":{"type":"object","required":["question"],"not":{"required":["messages"]},"properties":{"question":{"type":"string","minLength":1,"maxLength":10000,"pattern":"\\S","description":"A nonblank message, up to 10,000 characters."},"sessionId":{"type":["string","null"],"description":"Optional identifier for the conversation."},"responseId":{"type":["string","null"],"description":"Optional compatibility field. The server generates its own response identifier."}}},"AskQuota":{"type":"object","required":["scope","limit","remaining","resetAfterSeconds"],"properties":{"scope":{"type":"string","enum":["POST /api/v1/ask","POST /api/ask"]},"limit":{"type":"integer","minimum":0},"remaining":{"type":"integer","minimum":0},"resetAfterSeconds":{"type":"integer","minimum":1}}},"ConversationRequest":{"type":"object","required":["messages"],"properties":{"messages":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/ChatMessage"}},"sessionId":{"type":["string","null"],"description":"Optional identifier for the conversation."},"responseId":{"type":["string","null"],"description":"Optional compatibility field. The server generates its own response identifier."}}},"ChatMessage":{"type":"object","required":["role","content"],"properties":{"role":{"type":"string","enum":["user","assistant"],"description":"The message author."},"content":{"type":"string","minLength":1,"maxLength":10000,"pattern":"\\S","description":"A nonblank message, up to 10,000 characters."}}},"HttpError":{"type":"object","required":["detail"],"properties":{"detail":{"type":"string","description":"The reason the request was rejected."},"error":{"$ref":"#/components/schemas/ErrorMetadata"},"documentation":{"type":"string"}}},"ErrorMetadata":{"type":"object","required":["code","message","resolution"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"resolution":{"type":"string"}}},"ValidationError":{"type":"object","required":["detail"],"properties":{"error":{"$ref":"#/components/schemas/ErrorMetadata"},"documentation":{"type":"string"},"detail":{"type":"array","items":{"type":"object","required":["loc","msg","type"],"properties":{"loc":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"integer"}]}},"msg":{"type":"string"},"type":{"type":"string"}}}}}}}},"x-api-versioning":{"strategy":"url-path","currentVersion":"v1","policy":"https://www.dataacrobat.com/developers"}}