{ "components": { "parameters": { "Slug": { "description": "Post slug. The API normalizes path slugs to lowercase kebab-case before lookup.", "in": "path", "name": "slug", "required": true, "schema": { "example": "shipping-agents-with-receipts", "maxLength": 120, "minLength": 1, "type": "string" } } }, "responses": { "AuthNotConfigured": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "`BLOG_API_KEY` is not configured on the server." }, "BadRequest": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "The request body, slug, or query parameters are invalid." }, "NotFound": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "The requested post was not found." }, "Unauthorized": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "The blog API key is missing or invalid." } }, "schemas": { "BlogPost": { "properties": { "author": { "example": "a2a cloud", "type": "string" }, "content": { "example": "# Shipping agents with receipts\n\nAgents need durable runtime boundaries.", "type": "string" }, "cover_image_url": { "example": "https://blog.a2acloud.io/brand/og-image.png", "format": "uri", "maxLength": 500, "type": [ "string", "null" ] }, "created_at": { "format": "date-time", "type": "string" }, "excerpt": { "example": "Why agent infrastructure needs identity, authority, and proof.", "maxLength": 420, "type": "string" }, "id": { "example": 42, "format": "int64", "type": "integer" }, "published_at": { "format": "date-time", "type": [ "string", "null" ] }, "slug": { "example": "shipping-agents-with-receipts", "type": "string" }, "status": { "$ref": "#/components/schemas/BlogStatus" }, "tags": { "example": [ "platform", "agents" ], "items": { "type": "string" }, "maxItems": 12, "type": "array" }, "title": { "example": "Shipping agents with receipts", "maxLength": 180, "type": "string" }, "updated_at": { "format": "date-time", "type": "string" } }, "required": [ "id", "slug", "title", "excerpt", "content", "author", "status", "tags", "cover_image_url", "created_at", "updated_at", "published_at" ], "type": "object" }, "BlogPostInput": { "properties": { "author": { "default": "a2a cloud", "maxLength": 120, "type": "string" }, "content": { "example": "# Shipping agents with receipts\n\nAgents need durable runtime boundaries.", "maxLength": 120000, "minLength": 1, "type": "string" }, "cover_image_url": { "format": "uri", "maxLength": 500, "type": "string" }, "excerpt": { "default": "", "example": "Why agent infrastructure needs identity, authority, and proof.", "maxLength": 420, "type": "string" }, "published_at": { "description": "Defaults to the current time for published posts and null for drafts.", "format": "date-time", "type": "string" }, "slug": { "description": "Optional on create. The path slug is used for upserts. Slugs are normalized to lowercase kebab-case.", "example": "shipping-agents-with-receipts", "maxLength": 120, "type": "string" }, "status": { "$ref": "#/components/schemas/BlogStatus", "default": "published" }, "tags": { "default": [], "items": { "type": "string" }, "maxItems": 12, "type": "array" }, "title": { "example": "Shipping agents with receipts", "maxLength": 180, "minLength": 1, "type": "string" } }, "required": [ "title", "content" ], "type": "object" }, "BlogStatus": { "enum": [ "draft", "published" ], "type": "string" }, "DeleteResponse": { "properties": { "ok": { "const": true, "type": "boolean" } }, "required": [ "ok" ], "type": "object" }, "ErrorResponse": { "properties": { "error": { "type": "string" } }, "required": [ "error" ], "type": "object" }, "HealthError": { "properties": { "error": { "type": "string" }, "ok": { "const": false, "type": "boolean" } }, "required": [ "ok", "error" ], "type": "object" }, "HealthOk": { "properties": { "ok": { "const": true, "type": "boolean" } }, "required": [ "ok" ], "type": "object" }, "PostListResponse": { "properties": { "posts": { "items": { "$ref": "#/components/schemas/BlogPost" }, "type": "array" } }, "required": [ "posts" ], "type": "object" }, "PostResponse": { "properties": { "post": { "$ref": "#/components/schemas/BlogPost" } }, "required": [ "post" ], "type": "object" } }, "securitySchemes": { "apiKeyAuth": { "description": "Use the configured `BLOG_API_KEY` value.", "in": "header", "name": "x-api-key", "type": "apiKey" }, "bearerAuth": { "description": "Use `Authorization: Bearer $BLOG_API_KEY`.", "scheme": "bearer", "type": "http" } } }, "info": { "description": "Publishing and read API for the a2a cloud blog. Published content is public; draft, preview, create, update, and delete operations require the blog API key.", "title": "a2a cloud blog API", "version": "0.1.0" }, "openapi": "3.1.0", "paths": { "/api/healthz": { "get": { "operationId": "getBlogHealth", "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HealthOk" } } }, "description": "The service can initialize its schema and query Postgres." }, "503": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HealthError" } } }, "description": "The service or database is unavailable." } }, "summary": "Check blog service health", "tags": [ "Health" ] } }, "/api/posts": { "get": { "description": "Returns published posts by default. `status=all` and `status=draft` include draft content and require either bearer auth or the `x-api-key` header.", "operationId": "listBlogPosts", "parameters": [ { "description": "Use `published` or omit the parameter for public published posts. `all` and `draft` require authentication.", "in": "query", "name": "status", "required": false, "schema": { "default": "published", "enum": [ "published", "draft", "all" ], "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostListResponse" } } }, "description": "Posts returned in newest-first order." }, "401": { "$ref": "#/components/responses/Unauthorized" }, "503": { "$ref": "#/components/responses/AuthNotConfigured" } }, "security": [ {}, { "bearerAuth": [] }, { "apiKeyAuth": [] } ], "summary": "List posts", "tags": [ "Posts" ] }, "post": { "operationId": "createBlogPost", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BlogPostInput" } } }, "required": true }, "responses": { "201": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } }, "description": "The post was created." }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "409": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "A post with this slug already exists." }, "503": { "$ref": "#/components/responses/AuthNotConfigured" } }, "security": [ { "bearerAuth": [] }, { "apiKeyAuth": [] } ], "summary": "Create a blog post", "tags": [ "Posts" ] } }, "/api/posts/{slug}": { "delete": { "operationId": "deleteBlogPost", "parameters": [ { "$ref": "#/components/parameters/Slug" } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteResponse" } } }, "description": "The post was deleted." }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "503": { "$ref": "#/components/responses/AuthNotConfigured" } }, "security": [ { "bearerAuth": [] }, { "apiKeyAuth": [] } ], "summary": "Delete a post by slug", "tags": [ "Posts" ] }, "get": { "description": "Returns a published post. Add `preview=1` to fetch drafts or future-dated posts; preview requests require either bearer auth or the `x-api-key` header.", "operationId": "getBlogPost", "parameters": [ { "$ref": "#/components/parameters/Slug" }, { "description": "Set to `1` to bypass the public published-post filter. Requires authentication.", "in": "query", "name": "preview", "required": false, "schema": { "enum": [ "1" ], "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } }, "description": "The matching post." }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "503": { "$ref": "#/components/responses/AuthNotConfigured" } }, "security": [ {}, { "bearerAuth": [] }, { "apiKeyAuth": [] } ], "summary": "Get a post by slug", "tags": [ "Posts" ] }, "put": { "operationId": "upsertBlogPost", "parameters": [ { "$ref": "#/components/parameters/Slug" } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BlogPostInput" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } }, "description": "The post was created or updated." }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "503": { "$ref": "#/components/responses/AuthNotConfigured" } }, "security": [ { "bearerAuth": [] }, { "apiKeyAuth": [] } ], "summary": "Upsert a post by slug", "tags": [ "Posts" ] } } }, "servers": [ { "description": "Production", "url": "https://blog.a2acloud.io" }, { "description": "Local development", "url": "http://localhost:3000" } ], "tags": [ { "description": "Blog post listing, publishing, and lookup.", "name": "Posts" }, { "description": "Service health checks.", "name": "Health" } ] }