{
  "openapi": "3.1.0",
  "info": {
    "title": "Laisky Blog Public Resources API",
    "summary": "Read-only discovery endpoints for Laisky's Blog and Laisky MCP.",
    "description": "This API documents static public resources that AI agents can fetch without executing JavaScript.",
    "version": "2026-07-03",
    "contact": {
      "name": "Laisky",
      "url": "https://blog.laisky.com/about/site/"
    }
  },
  "servers": [
    {
      "url": "https://blog.laisky.com",
      "description": "Production blog domain"
    }
  ],
  "paths": {
    "/api": {
      "get": {
        "operationId": "getPublicResourceIndex",
        "summary": "Get public resource index",
        "description": "Returns a JSON index of public agent and developer resources on blog.laisky.com.",
        "responses": {
          "200": {
            "description": "Public resource index.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicResourceIndex"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getLlmsText",
        "summary": "Get agent guide",
        "description": "Returns the canonical plain-text agent guide.",
        "responses": {
          "200": {
            "description": "Agent guide in plain text.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/llms.txt": {
      "get": {
        "operationId": "getApiLlmsText",
        "summary": "Get API-specific agent guide",
        "description": "Returns a narrow llms.txt guide for API, NLWeb, webhook, and versioning resources.",
        "responses": {
          "200": {
            "description": "API-specific agent guide in plain text.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/mcp/llms.txt": {
      "get": {
        "operationId": "getMcpLlmsText",
        "summary": "Get MCP-specific agent guide",
        "description": "Returns a narrow llms.txt guide for MCP discovery resources.",
        "responses": {
          "200": {
            "description": "MCP-specific agent guide in plain text.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/docs/llms.txt": {
      "get": {
        "operationId": "getDocsLlmsText",
        "summary": "Get documentation-specific agent guide",
        "description": "Returns a narrow llms.txt guide for Markdown documentation resources.",
        "responses": {
          "200": {
            "description": "Documentation-specific agent guide in plain text.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/developers/llms.txt": {
      "get": {
        "operationId": "getDevelopersLlmsText",
        "summary": "Get developer-specific agent guide",
        "description": "Returns a narrow llms.txt guide for developer resources and repository entry points.",
        "responses": {
          "200": {
            "description": "Developer-specific agent guide in plain text.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/index.md": {
      "get": {
        "operationId": "getMarkdownHome",
        "summary": "Get Markdown overview",
        "description": "Returns the Markdown homepage overview for crawlers and agents.",
        "responses": {
          "200": {
            "description": "Markdown overview.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/api-catalog": {
      "get": {
        "operationId": "getApiCatalog",
        "summary": "Get API catalog",
        "description": "Returns the RFC 9727 Linkset API catalog.",
        "responses": {
          "200": {
            "description": "API catalog Linkset.",
            "content": {
              "application/linkset+json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/mcp/server-card.json": {
      "get": {
        "operationId": "getMcpServerCard",
        "summary": "Get MCP server card",
        "description": "Returns the MCP server card for discovering Laisky MCP.",
        "responses": {
          "200": {
            "description": "MCP server card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpServerCard"
                }
              }
            }
          }
        }
      }
    },
    "/v1/resources": {
      "get": {
        "operationId": "listPublicResources",
        "summary": "List public resources",
        "description": "Returns a paginated list of public agent-readable resources.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated public resources.",
            "headers": {
              "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/ResourceList"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/batch/resources": {
      "post": {
        "operationId": "batchGetPublicResources",
        "summary": "Batch get public resources",
        "description": "Accepts resource IDs and returns a batch result. Clients should send Idempotency-Key for retries.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchResourceRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch resource response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchResourceResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/jobs/{job_id}": {
      "get": {
        "operationId": "getPublicResourceJob",
        "summary": "Get async job status",
        "description": "Returns status for an asynchronous public resource job.",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "Job identifier returned by an async operation.",
            "schema": {
              "type": "string",
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AsyncJob"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/jobs": {
      "post": {
        "operationId": "createPublicResourceJob",
        "summary": "Create async public resource job",
        "description": "Creates a static demonstration async job for public resource processing. Clients should send Idempotency-Key when retrying.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateJobRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted.",
            "headers": {
              "Location": {
                "description": "URL to poll for job status.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AsyncJob"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/v1/webhooks": {
      "get": {
        "operationId": "listWebhookEvents",
        "summary": "List webhook event types",
        "description": "Returns documented webhook event types and signature guidance.",
        "responses": {
          "200": {
            "description": "Webhook event catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookCatalog"
                }
              }
            }
          }
        }
      }
    },
    "/ask": {
      "get": {
        "operationId": "askPublicResources",
        "summary": "Ask about public resources",
        "description": "NLWeb-style read endpoint that answers simple questions about public Laisky Blog resources. Set stream=true to receive server-sent events on the same endpoint.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Question about public blog or MCP resources.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "stream",
            "in": "query",
            "required": false,
            "description": "Set true for clients that support streamed answers.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Answer with citations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskResponse"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/ask-stream": {
      "get": {
        "operationId": "streamPublicResourceAnswer",
        "summary": "Stream an NLWeb answer",
        "description": "Streams a simple NLWeb-style answer as server-sent events.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Question about public blog or MCP resources.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Server-sent event stream.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "resource.updated": {
      "post": {
        "operationId": "receiveResourceUpdatedWebhook",
        "summary": "Receive resource update webhook",
        "description": "Webhook receiver contract for public resource update notifications. Verify X-Laisky-Signature with HMAC-SHA256 before processing.",
        "parameters": [
          {
            "name": "X-Laisky-Signature",
            "in": "header",
            "required": true,
            "description": "HMAC-SHA256 signature of the raw webhook payload.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook accepted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum number of resources to return.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Opaque pagination cursor from the previous response.",
        "schema": {
          "type": "string"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Unique key for safely retrying batch requests.",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 128
        }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Maximum requests allowed in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requests remaining in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the current rate-limit window resets.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "schemas": {
      "PublicResource": {
        "type": "object",
        "required": ["id", "url", "mediaType"],
        "properties": {
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "mediaType": {
            "type": "string"
          }
        }
      },
      "ResourceList": {
        "type": "object",
        "required": ["object", "version", "data", "pagination"],
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "version": {
            "type": "string",
            "const": "v1"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicResource"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "Pagination": {
        "type": "object",
        "required": ["limit"],
        "properties": {
          "limit": {
            "type": "integer"
          },
          "next_cursor": {
            "type": ["string", "null"]
          }
        }
      },
      "BatchResourceRequest": {
        "type": "object",
        "required": ["ids"],
        "properties": {
          "ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 100
          }
        }
      },
      "BatchResourceResponse": {
        "type": "object",
        "required": ["object", "data"],
        "properties": {
          "object": {
            "type": "string",
            "const": "batch"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["id", "status"],
              "properties": {
                "id": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": ["ok", "not_found"]
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          }
        }
      },
      "AsyncJob": {
        "type": "object",
        "required": ["id", "object", "status"],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "const": "job"
          },
          "status": {
            "type": "string",
            "enum": ["queued", "running", "succeeded", "failed"]
          },
          "result_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "CreateJobRequest": {
        "type": "object",
        "properties": {
          "resource_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "WebhookCatalog": {
        "type": "object",
        "required": ["events", "signature"],
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": ["resource.updated", "resource.deleted"]
            }
          },
          "signature": {
            "type": "object",
            "required": ["header", "algorithm"],
            "properties": {
              "header": {
                "type": "string",
                "const": "X-Laisky-Signature"
              },
              "algorithm": {
                "type": "string",
                "const": "HMAC-SHA256"
              }
            }
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": ["id", "type", "created_at", "data"],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": ["resource.updated", "resource.deleted"]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "AskResponse": {
        "type": "object",
        "required": ["_meta", "answer", "citations"],
        "properties": {
          "_meta": {
            "$ref": "#/components/schemas/NLWebMeta"
          },
          "answer": {
            "type": "string"
          },
          "citations": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          }
        }
      },
      "NLWebMeta": {
        "type": "object",
        "required": ["protocol", "version", "openapi"],
        "properties": {
          "protocol": {
            "type": "string",
            "const": "nlweb"
          },
          "version": {
            "type": "string"
          },
          "stream": {
            "type": "string",
            "format": "uri"
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "PublicResourceIndex": {
        "type": "object",
        "required": ["name", "version", "resources"],
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "resources": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["name", "url", "mediaType"],
              "properties": {
                "name": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "mediaType": {
                  "type": "string"
                }
              }
            }
          },
          "auth": {
            "type": "object",
            "properties": {
              "required": {
                "type": "boolean"
              },
              "notes": {
                "type": "string"
              }
            }
          }
        }
      },
      "McpServerCard": {
        "type": "object",
        "required": ["name", "description", "url", "transport"],
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "transport": {
            "type": "string"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": ["code", "message", "recovery", "status"],
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "recovery": {
            "type": "string"
          },
          "status": {
            "type": "integer",
            "minimum": 400,
            "maximum": 599
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "NotFound": {
        "description": "The requested static resource was not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "RateLimited": {
        "description": "The request exceeded the public read rate limit.",
        "headers": {
          "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/ErrorResponse"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The signature or credential was invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    }
  }
}
