{ "openapi": "3.0.1", "info": { "title": "[Postal]: Post Management Service APIs", "description": "API for managing blog posts with full CRUD operations, publishing, and archiving.", "version": "1.0.0" }, "servers": [ { "url": "https://postal.nesohq.org", "description": "Production" }, { "url": "http://localhost:8081", "description": "Local Development" } ], "paths": { "/api/v1/health": { "get": { "summary": "Health check", "description": "Check if the service is running.", "tags": [ "Health" ], "responses": { "200": { "description": "Service is healthy", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" }, "service": { "type": "string", "example": "postal" }, "version": { "type": "string", "example": "1.0.0" } } } } } } } } }, "/api/v1/posts": { "get": { "summary": "List posts", "description": "Retrieves a list of posts with optional filtering and pagination. Returns lightweight post objects optimized for list views (excludes full content, only returns content_length for read time calculation).", "tags": [ "Posts" ], "parameters": [ { "name": "limit", "in": "query", "description": "Number of posts to return", "schema": { "type": "integer", "default": 20, "minimum": 1, "maximum": 100 } }, { "name": "offset", "in": "query", "description": "Number of posts to skip (for pagination)", "schema": { "type": "integer", "default": 0, "minimum": 0 } }, { "name": "status", "in": "query", "description": "Filter by post status", "schema": { "type": "string", "enum": [ "draft", "published", "archived", "deleted" ] } }, { "name": "category_id", "in": "query", "description": "Filter by category ID", "schema": { "type": "integer" } }, { "name": "sub_category_id", "in": "query", "description": "Filter by subcategory ID", "schema": { "type": "integer" } }, { "name": "search", "in": "query", "description": "Search in title, content, and summary", "schema": { "type": "string" } }, { "name": "sort_by", "in": "query", "description": "Field to sort by", "schema": { "type": "string", "enum": [ "created_at", "updated_at", "view_count", "title" ], "default": "created_at" } }, { "name": "sort_order", "in": "query", "description": "Sort order", "schema": { "type": "string", "enum": [ "ASC", "DESC" ], "default": "DESC" } } ], "responses": { "200": { "description": "Posts retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostListResponse" } } } } } }, "post": { "summary": "Create post", "description": "Creates a new blog post.", "tags": [ "Posts" ], "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePostRequest" } } } }, "responses": { "201": { "description": "Post created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/posts/{id}/view": { "post": { "summary": "Record post view", "description": "Increments the view count for a specific post. This should be called by the client when a post is actually viewed by a user.", "tags": [ "Posts" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "View count recorded successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } } }, "400": { "description": "Invalid post ID", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Failed to increment view count", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/posts/{id}": { "get": { "summary": "Get post by ID", "description": "Retrieves a specific post by its ID.", "tags": [ "Posts" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Post retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } } }, "404": { "description": "Post not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } }, "put": { "summary": "Update post", "description": "Updates an existing post.", "tags": [ "Posts" ], "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdatePostRequest" } } } }, "responses": { "200": { "description": "Post updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Post not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } }, "delete": { "summary": "Delete post", "description": "Deletes a post.", "tags": [ "Posts" ], "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Post deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Post not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/posts/slug/{slug}": { "get": { "summary": "Get post by slug", "description": "Retrieves a specific post by its slug.", "tags": [ "Posts" ], "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Post retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } } }, "404": { "description": "Post not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/posts/uuid/{uuid}": { "get": { "summary": "Get post by UUID", "description": "Retrieves a specific post by its UUID.", "tags": [ "Posts" ], "parameters": [ { "name": "uuid", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Post retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } } }, "404": { "description": "Post not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/posts/{id}/publish": { "post": { "summary": "Publish post", "description": "Publishes a draft post.", "tags": [ "Posts" ], "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Post published successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Post not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/posts/{id}/unpublish": { "post": { "summary": "Unpublish post", "description": "Unpublishes a published post.", "tags": [ "Posts" ], "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Post unpublished successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Post not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/posts/{id}/archive": { "post": { "summary": "Archive post", "description": "Archives a post.", "tags": [ "Posts" ], "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Post archived successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostResponse" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Post not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/posts/batch": { "delete": { "tags": [ "Posts" ], "summary": "Batch delete posts", "description": "Deletes multiple posts by their UUIDs in a single request.", "operationId": "batchDeletePosts", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "uuids" ], "properties": { "uuids": { "type": "array", "minItems": 1, "items": { "type": "string", "format": "uuid" }, "example": [ "d290f1ee-6c54-4b01-90e6-d701748f0851", "a1b2c3d4-e5f6-7890-abcd-ef1234567890" ] } } } } } }, "responses": { "200": { "description": "Posts deleted successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "boolean", "example": true }, "message": { "type": "string", "example": "Posts deleted successfully" }, "data": { "type": "object", "properties": { "deleted_count": { "type": "integer", "example": 2 }, "uuids": { "type": "array", "items": { "type": "string", "format": "uuid" } } } } } } } } }, "400": { "description": "Bad request (invalid body or no valid UUIDs)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "No posts found or some UUIDs not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } }, "post": { "tags": [ "Posts" ], "summary": "Batch upload posts", "description": "Upload a CSV file to create posts in bulk", "operationId": "batchUploadPosts", "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "file": { "type": "string", "format": "binary", "description": "CSV file" } }, "required": [ "file" ] } } } }, "responses": { "201": { "description": "Posts created successfully" }, "400": { "description": "Invalid CSV or validation error" }, "401": { "description": "Unauthorized" }, "500": { "description": "Internal server error" } }, "security": [ { "BearerAuth": [] } ] } }, "/api/v1/posts/seed-read-time": { "post": { "summary": "Seed post read times", "description": "Recalculates the read time for all existing posts in the background. Flushes the Redis cache first.", "tags": [ "Posts" ], "security": [ { "BearerAuth": [] } ], "responses": { "200": { "description": "Seeding process started", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } } }, "components": { "securitySchemes": { "BearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" } }, "schemas": { "Post": { "type": "object", "properties": { "id": { "type": "integer", "example": 1 }, "uuid": { "type": "string", "format": "uuid" }, "title": { "type": "string", "example": "Getting Started with Go" }, "slug": { "type": "string", "example": "getting-started-with-go" }, "content": { "type": "string", "example": "# Introduction\n\nThis is a blog post..." }, "summary": { "type": "string", "example": "Learn the basics of Go programming" }, "thumbnail": { "type": "string", "example": "https://example.com/image.jpg" }, "category_id": { "type": "integer", "example": 1 }, "sub_category_id": { "type": "integer", "example": 2 }, "meta_title": { "type": "string" }, "meta_description": { "type": "string" }, "keywords": { "type": "string", "example": "go,programming,tutorial" }, "og_image": { "type": "string" }, "status": { "type": "string", "enum": [ "draft", "published", "archived" ], "example": "published" }, "is_public": { "type": "boolean", "example": true }, "is_featured": { "type": "boolean", "example": false }, "is_pinned": { "type": "boolean", "example": false }, "published_at": { "type": "string", "format": "date-time" }, "archived_at": { "type": "string", "format": "date-time" }, "created_by": { "type": "integer", "example": 1 }, "updated_by": { "type": "integer" }, "view_count": { "type": "integer", "example": 100 }, "version": { "type": "integer", "example": 1 }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "deleted_at": { "type": "string", "format": "date-time" } } }, "CreatePostRequest": { "type": "object", "required": [ "title", "slug", "content", "category_id" ], "properties": { "title": { "type": "string", "example": "Getting Started with Go" }, "slug": { "type": "string", "example": "getting-started-with-go" }, "content": { "type": "string", "example": "# Introduction\n\nThis is a blog post..." }, "summary": { "type": "string", "example": "Learn the basics of Go programming" }, "thumbnail": { "type": "string" }, "category_id": { "type": "integer", "example": 1 }, "sub_category_id": { "type": "integer", "example": 2 }, "meta_title": { "type": "string" }, "meta_description": { "type": "string" }, "keywords": { "type": "string", "example": "go,programming,tutorial" }, "og_image": { "type": "string" }, "status": { "type": "string", "enum": [ "draft", "published" ], "default": "draft" }, "is_public": { "type": "boolean", "default": true }, "is_featured": { "type": "boolean", "default": false }, "is_pinned": { "type": "boolean", "default": false } } }, "UpdatePostRequest": { "type": "object", "properties": { "title": { "type": "string" }, "slug": { "type": "string" }, "content": { "type": "string" }, "summary": { "type": "string" }, "thumbnail": { "type": "string" }, "category_id": { "type": "integer" }, "sub_category_id": { "type": "integer" }, "meta_title": { "type": "string" }, "meta_description": { "type": "string" }, "keywords": { "type": "string" }, "og_image": { "type": "string" }, "status": { "type": "string", "enum": [ "draft", "published", "archived" ] }, "is_public": { "type": "boolean" }, "is_featured": { "type": "boolean" }, "is_pinned": { "type": "boolean" } } }, "PostResponse": { "type": "object", "properties": { "status": { "type": "boolean", "example": true }, "message": { "type": "string", "example": "Post retrieved successfully" }, "data": { "$ref": "#/components/schemas/Post" } } }, "PostListResponse": { "type": "object", "properties": { "status": { "type": "boolean", "example": true }, "message": { "type": "string", "example": "Posts retrieved successfully" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/PostListItem" } }, "meta": { "type": "object", "properties": { "total": { "type": "integer", "example": 100 }, "limit": { "type": "integer", "example": 10 }, "offset": { "type": "integer", "example": 0 } } } } }, "PostListItem": { "type": "object", "description": "Lightweight post object for list endpoints (optimized for performance)", "properties": { "id": { "type": "integer", "example": 1 }, "slug": { "type": "string", "example": "getting-started-with-go" }, "title": { "type": "string", "example": "Getting Started with Go" }, "summary": { "type": "string", "example": "Learn the basics of Go programming" }, "meta_description": { "type": "string", "example": "A comprehensive guide to getting started with Go" }, "keywords": { "type": "string", "example": "go,programming,tutorial" }, "category_id": { "type": "integer", "example": 1 }, "sub_category_id": { "type": "integer", "example": 2, "nullable": true }, "is_featured": { "type": "boolean", "example": false }, "is_pinned": { "type": "boolean", "example": false }, "created_by": { "type": "integer", "example": 1 }, "view_count": { "type": "integer", "example": 100 }, "content_length": { "type": "integer", "example": 1250, "description": "Character count of the content (for read time calculation)" }, "created_at": { "type": "string", "format": "date-time" } } }, "SuccessResponse": { "type": "object", "properties": { "status": { "type": "boolean", "example": true }, "message": { "type": "string", "example": "Operation successful" }, "data": { "type": "object", "nullable": true } } }, "ErrorResponse": { "type": "object", "properties": { "status": { "type": "boolean", "example": false }, "message": { "type": "string", "example": "Error occurred" }, "error": { "type": "string" } } } } }, "tags": [ { "name": "Health", "description": "Health check endpoints" }, { "name": "Posts", "description": "Blog post management operations" } ] }