{ "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": { "A2AAuthUnavailable": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginErrorResponse" } } }, "description": "a2a Cloud authentication is unavailable." }, "A2AUnauthorized": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginErrorResponse" } } }, "description": "An a2a Cloud browser session is required." }, "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": { "BlogComment": { "properties": { "author_name": { "example": "Dev User", "type": "string" }, "body": { "example": "This cleared up the deployment flow.", "maxLength": 4000, "minLength": 2, "type": "string" }, "created_at": { "format": "date-time", "type": "string" }, "id": { "example": 12, "format": "int64", "type": "integer" }, "post_id": { "example": 42, "format": "int64", "type": "integer" }, "updated_at": { "format": "date-time", "type": "string" } }, "required": [ "id", "post_id", "author_name", "body", "created_at", "updated_at" ], "type": "object" }, "BlogCommentInput": { "properties": { "body": { "example": "This cleared up the deployment flow.", "maxLength": 4000, "minLength": 2, "type": "string" } }, "required": [ "body" ], "type": "object" }, "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" }, "BlogReaction": { "enum": [ "like", "dislike" ], "type": "string" }, "BlogReactionInput": { "properties": { "reaction": { "description": "Set to null to clear the viewer's reaction.", "oneOf": [ { "$ref": "#/components/schemas/BlogReaction" }, { "type": "null" } ] } }, "required": [ "reaction" ], "type": "object" }, "BlogReactionSummary": { "properties": { "dislikes": { "example": 1, "minimum": 0, "type": "integer" }, "likes": { "example": 10, "minimum": 0, "type": "integer" }, "viewer_reaction": { "oneOf": [ { "$ref": "#/components/schemas/BlogReaction" }, { "type": "null" } ] } }, "required": [ "likes", "dislikes", "viewer_reaction" ], "type": "object" }, "BlogStatus": { "enum": [ "draft", "published" ], "type": "string" }, "CommentListResponse": { "properties": { "comments": { "items": { "$ref": "#/components/schemas/BlogComment" }, "type": "array" } }, "required": [ "comments" ], "type": "object" }, "CommentResponse": { "properties": { "comment": { "$ref": "#/components/schemas/BlogComment" } }, "required": [ "comment" ], "type": "object" }, "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" }, "LoginErrorResponse": { "properties": { "error": { "type": "string" }, "login_url": { "format": "uri", "type": "string" } }, "required": [ "error" ], "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" }, "ReactionResponse": { "properties": { "reactions": { "$ref": "#/components/schemas/BlogReactionSummary" } }, "required": [ "reactions" ], "type": "object" } }, "securitySchemes": { "a2aSession": { "description": "Shared a2a Cloud browser session cookie.", "in": "cookie", "name": "a2a_session", "type": "apiKey" }, "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, read, comment, and reaction API for the a2a cloud blog. Published content and comments are public; publishing requires the blog API key; comment and reaction writes require an a2a Cloud browser session.", "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" ] } }, "/api/posts/{slug}/comments": { "get": { "operationId": "listBlogPostComments", "parameters": [ { "$ref": "#/components/parameters/Slug" } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CommentListResponse" } } }, "description": "Comments returned oldest-first." }, "400": { "$ref": "#/components/responses/BadRequest" }, "404": { "$ref": "#/components/responses/NotFound" } }, "summary": "List comments for a post", "tags": [ "Comments" ] }, "post": { "description": "Requires the shared a2a Cloud browser session cookie from the web login flow.", "operationId": "createBlogPostComment", "parameters": [ { "$ref": "#/components/parameters/Slug" } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BlogCommentInput" } } }, "required": true }, "responses": { "201": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CommentResponse" } } }, "description": "The comment was created." }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/A2AUnauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "503": { "$ref": "#/components/responses/A2AAuthUnavailable" } }, "security": [ { "a2aSession": [] } ], "summary": "Create a comment", "tags": [ "Comments" ] } }, "/api/posts/{slug}/reaction": { "get": { "description": "Returns public like/dislike counts. If an a2a Cloud session cookie is present, `viewer_reaction` is populated.", "operationId": "getBlogPostReactions", "parameters": [ { "$ref": "#/components/parameters/Slug" } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReactionResponse" } } }, "description": "Reaction counts for the post." }, "400": { "$ref": "#/components/responses/BadRequest" }, "404": { "$ref": "#/components/responses/NotFound" } }, "security": [ {}, { "a2aSession": [] } ], "summary": "Get post reaction counts", "tags": [ "Reactions" ] }, "put": { "description": "Creates, updates, or clears the authenticated viewer's like/dislike reaction.", "operationId": "setBlogPostReaction", "parameters": [ { "$ref": "#/components/parameters/Slug" } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BlogReactionInput" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReactionResponse" } } }, "description": "Updated reaction counts for the post." }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/A2AUnauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "503": { "$ref": "#/components/responses/A2AAuthUnavailable" } }, "security": [ { "a2aSession": [] } ], "summary": "Set the viewer reaction", "tags": [ "Reactions" ] } } }, "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": "Public comments on published posts.", "name": "Comments" }, { "description": "Like and dislike reactions on published posts.", "name": "Reactions" }, { "description": "Service health checks.", "name": "Health" } ] }