{"openapi":"3.1.0","info":{"title":"a2a cloud blog API","version":"0.1.0","description":"Publishing and read API for the a2a cloud blog. Published content, comments, and reaction totals are public; publishing requires the blog API key. Discussion writes are disabled until origin-bound browser sessions are available."},"servers":[{"url":"https://blog.a2acloud.io","description":"Production"},{"url":"http://localhost:3000","description":"Local development"}],"tags":[{"name":"Posts","description":"Blog post listing, publishing, and lookup."},{"name":"Comments","description":"Public comments on published posts."},{"name":"Reactions","description":"Like and dislike reactions on published posts."},{"name":"Health","description":"Service health checks."}],"paths":{"/api/healthz":{"get":{"tags":["Health"],"summary":"Check blog service health","operationId":"getBlogHealth","responses":{"200":{"description":"The service can initialize its schema and query Postgres.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthOk"}}}},"503":{"description":"The service or database is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthError"}}}}}}},"/api/posts":{"get":{"tags":["Posts"],"summary":"List posts","operationId":"listBlogPosts","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.","security":[{},{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"status","in":"query","required":false,"description":"Use `published` or omit the parameter for public published posts. `all` and `draft` require authentication.","schema":{"type":"string","enum":["published","draft","all"],"default":"published"}}],"responses":{"200":{"description":"Posts returned in newest-first order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"$ref":"#/components/responses/AuthNotConfigured"}}},"post":{"tags":["Posts"],"summary":"Create a blog post","operationId":"createBlogPost","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlogPostInput"}}}},"responses":{"201":{"description":"The post was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"A post with this slug already exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"$ref":"#/components/responses/AuthNotConfigured"}}}},"/api/posts/{slug}":{"get":{"tags":["Posts"],"summary":"Get a post by slug","operationId":"getBlogPost","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.","security":[{},{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Slug"},{"name":"preview","in":"query","required":false,"description":"Set to `1` to bypass the public published-post filter. Requires authentication.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"The matching post.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"503":{"$ref":"#/components/responses/AuthNotConfigured"}}},"put":{"tags":["Posts"],"summary":"Upsert a post by slug","operationId":"upsertBlogPost","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlogPostInput"}}}},"responses":{"200":{"description":"The post was created or updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"$ref":"#/components/responses/AuthNotConfigured"}}},"delete":{"tags":["Posts"],"summary":"Delete a post by slug","operationId":"deleteBlogPost","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Slug"}],"responses":{"200":{"description":"The post was deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"503":{"$ref":"#/components/responses/AuthNotConfigured"}}}},"/api/posts/{slug}/comments":{"get":{"tags":["Comments"],"summary":"List comments for a post","operationId":"listBlogPostComments","parameters":[{"$ref":"#/components/parameters/Slug"}],"responses":{"200":{"description":"Comments returned oldest-first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Comments"],"summary":"Comment writes are temporarily unavailable","operationId":"createBlogPostComment","description":"This endpoint fails closed with HTTP 503 while origin-bound browser sessions are disabled.","parameters":[{"$ref":"#/components/parameters/Slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlogCommentInput"}}}},"responses":{"503":{"$ref":"#/components/responses/DiscussionWritesDisabled"}}}},"/api/posts/{slug}/reaction":{"get":{"tags":["Reactions"],"summary":"Get post reaction counts","operationId":"getBlogPostReactions","description":"Returns public like/dislike counts. `viewer_reaction` is null while origin-bound sessions are disabled.","parameters":[{"$ref":"#/components/parameters/Slug"}],"responses":{"200":{"description":"Reaction counts for the post.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReactionResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Reactions"],"summary":"Reaction writes are temporarily unavailable","operationId":"setBlogPostReaction","description":"This endpoint fails closed with HTTP 503 while origin-bound browser sessions are disabled.","parameters":[{"$ref":"#/components/parameters/Slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlogReactionInput"}}}},"responses":{"503":{"$ref":"#/components/responses/DiscussionWritesDisabled"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Use `Authorization: Bearer $BLOG_API_KEY`."},"apiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"Use the configured `BLOG_API_KEY` value."}},"parameters":{"Slug":{"name":"slug","in":"path","required":true,"description":"Post slug. The API normalizes path slugs to lowercase kebab-case before lookup.","schema":{"type":"string","minLength":1,"maxLength":120,"example":"shipping-agents-with-receipts"}}},"responses":{"BadRequest":{"description":"The request body, slug, or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"The blog API key is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"DiscussionWritesDisabled":{"description":"Discussion writes are disabled until origin-bound sessions are available.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"The requested post was not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"AuthNotConfigured":{"description":"`BLOG_API_KEY` is not configured on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"schemas":{"BlogStatus":{"type":"string","enum":["draft","published"]},"BlogPost":{"type":"object","required":["id","slug","title","excerpt","content","author","status","tags","cover_image_url","created_at","updated_at","published_at"],"properties":{"id":{"type":"integer","format":"int64","example":42},"slug":{"type":"string","example":"shipping-agents-with-receipts"},"title":{"type":"string","maxLength":180,"example":"Shipping agents with receipts"},"excerpt":{"type":"string","maxLength":420,"example":"Why agent infrastructure needs identity, authority, and proof."},"content":{"type":"string","example":"# Shipping agents with receipts\n\nAgents need durable runtime boundaries."},"author":{"type":"string","example":"a2a cloud"},"status":{"$ref":"#/components/schemas/BlogStatus"},"tags":{"type":"array","maxItems":12,"items":{"type":"string"},"example":["platform","agents"]},"cover_image_url":{"type":["string","null"],"format":"uri","maxLength":500,"example":"https://blog.a2acloud.io/brand/og-image.png"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"published_at":{"type":["string","null"],"format":"date-time"}}},"BlogPostInput":{"type":"object","required":["title","content"],"properties":{"slug":{"type":"string","maxLength":120,"description":"Optional on create. The path slug is used for upserts. Slugs are normalized to lowercase kebab-case.","example":"shipping-agents-with-receipts"},"title":{"type":"string","minLength":1,"maxLength":180,"example":"Shipping agents with receipts"},"excerpt":{"type":"string","maxLength":420,"default":"","example":"Why agent infrastructure needs identity, authority, and proof."},"content":{"type":"string","minLength":1,"maxLength":120000,"example":"# Shipping agents with receipts\n\nAgents need durable runtime boundaries."},"author":{"type":"string","maxLength":120,"default":"a2a cloud"},"status":{"$ref":"#/components/schemas/BlogStatus","default":"published"},"tags":{"type":"array","maxItems":12,"items":{"type":"string"},"default":[]},"cover_image_url":{"type":"string","format":"uri","maxLength":500},"published_at":{"type":"string","format":"date-time","description":"Defaults to the current time for published posts and null for drafts."}}},"BlogComment":{"type":"object","required":["id","post_id","author_name","body","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64","example":12},"post_id":{"type":"integer","format":"int64","example":42},"author_name":{"type":"string","example":"Dev User"},"body":{"type":"string","minLength":2,"maxLength":4000,"example":"This cleared up the deployment flow."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"BlogCommentInput":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":2,"maxLength":4000,"example":"This cleared up the deployment flow."}}},"BlogReaction":{"type":"string","enum":["like","dislike"]},"BlogReactionInput":{"type":"object","required":["reaction"],"properties":{"reaction":{"oneOf":[{"$ref":"#/components/schemas/BlogReaction"},{"type":"null"}],"description":"Set to null to clear the viewer's reaction."}}},"BlogReactionSummary":{"type":"object","required":["likes","dislikes","viewer_reaction"],"properties":{"likes":{"type":"integer","minimum":0,"example":10},"dislikes":{"type":"integer","minimum":0,"example":1},"viewer_reaction":{"oneOf":[{"$ref":"#/components/schemas/BlogReaction"},{"type":"null"}]}}},"PostListResponse":{"type":"object","required":["posts"],"properties":{"posts":{"type":"array","items":{"$ref":"#/components/schemas/BlogPost"}}}},"PostResponse":{"type":"object","required":["post"],"properties":{"post":{"$ref":"#/components/schemas/BlogPost"}}},"CommentListResponse":{"type":"object","required":["comments"],"properties":{"comments":{"type":"array","items":{"$ref":"#/components/schemas/BlogComment"}}}},"CommentResponse":{"type":"object","required":["comment"],"properties":{"comment":{"$ref":"#/components/schemas/BlogComment"}}},"ReactionResponse":{"type":"object","required":["reactions"],"properties":{"reactions":{"$ref":"#/components/schemas/BlogReactionSummary"}}},"DeleteResponse":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","const":true}}},"HealthOk":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","const":true}}},"HealthError":{"type":"object","required":["ok","error"],"properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string"}}},"ErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"LoginErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"login_url":{"type":"string","format":"uri"}}}}}}