{
  "openapi": "3.0.0",
  "info": {
    "title": "Polyblog API",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.polyblog.io"
    }
  ],
  "components": {
    "schemas": {
      "Entity": {
        "type": "object",
        "description": "A Polyblog resource. Fields vary by endpoint and requested projection.",
        "additionalProperties": true
      },
      "Error": {
        "type": "object",
        "description": "Structured JSON API error.",
        "required": [
          "name",
          "message",
          "status"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "details": {
            "type": "object",
            "additionalProperties": true
          }
        }
      }
    },
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "basic",
        "description": "HTTP Basic auth carrying only the API key secret: Authorization: Basic base64(<key secret>)."
      },
      "accessToken": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "Operator session token issued by the Polyblog dashboard: Authorization: Token <access token>."
      }
    }
  },
  "paths": {
    "/api/articles": {
      "get": {
        "tags": [
          "Articles"
        ],
        "summary": "List articles",
        "description": "Lists the articles of a blog. Requires the `articles:read` scope. Anonymous (unauthenticated) callers only ever see published articles; drafts are visible only to an API key or operator token of the owning organization.\n",
        "security": [
          {
            "apiKey": []
          },
          {
            "accessToken": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "blogId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "locale",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "published",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "in": "query",
            "name": "slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A list of articles",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "_id": {
                        "type": "string"
                      },
                      "blogId": {
                        "type": "string"
                      },
                      "title": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string"
                      },
                      "content": {
                        "type": "string"
                      },
                      "slug": {
                        "type": "string"
                      },
                      "locale": {
                        "type": "string"
                      },
                      "coverUrl": {
                        "type": "string"
                      },
                      "published": {
                        "type": "boolean"
                      },
                      "creationTime": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "lastEditTime": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key is missing the `articles:read` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "API key rate limit exceeded",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the request may be retried.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "getApiArticles"
      },
      "post": {
        "tags": [
          "Articles"
        ],
        "summary": "Create or update an article",
        "description": "Creates an article (or updates one when the body carries an existing `_id`). Requires the `articles:write` scope. Fires the `article.created` webhook.\n",
        "security": [
          {
            "apiKey": []
          },
          {
            "accessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "blogId": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "content": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  },
                  "locale": {
                    "type": "string"
                  },
                  "published": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created or updated article",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key is missing the `articles:write` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "API key rate limit exceeded",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the request may be retried.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "postApiArticles"
      }
    },
    "/api/articles/{idOrSlug}": {
      "get": {
        "tags": [
          "Articles"
        ],
        "summary": "Get an article",
        "description": "Returns a single article by id or slug. Requires the `articles:read` scope. An article belonging to another organization responds 404 so ids cannot be probed. Anonymous callers can only read a published article; a draft responds 404 unless the caller is an API key or operator token of the owning organization.\n",
        "security": [
          {
            "apiKey": []
          },
          {
            "accessToken": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "idOrSlug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The article",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          },
          "404": {
            "description": "Not found (or owned by another organization / unpublished)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "getApiArticlesIdOrSlug"
      }
    },
    "/api/articles/generate": {
      "post": {
        "tags": [
          "Articles"
        ],
        "summary": "Generate an article with AI",
        "description": "Generates (and by default publishes) an SEO article for a blog. Requires the `articles:write` scope. Fires the `article.created` webhook when the generated article is persisted.\n",
        "security": [
          {
            "apiKey": []
          },
          {
            "accessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "The generated article and metadata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          },
          "403": {
            "description": "API key is missing the `articles:write` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "postApiArticlesGenerate"
      }
    },
    "/api/articles/{articleId}": {
      "delete": {
        "tags": [
          "Articles"
        ],
        "summary": "Delete an article",
        "description": "Deletes an article. Requires the `articles:write` scope. An article owned by another organization responds 404. Fires the `article.deleted` webhook.\n",
        "security": [
          {
            "apiKey": []
          },
          {
            "accessToken": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "articleId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The deleted article id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          },
          "404": {
            "description": "Not found (or owned by another organization)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "deleteApiArticlesArticleId"
      }
    },
    "/api/blogs": {
      "get": {
        "tags": [
          "Blogs"
        ],
        "summary": "List blogs",
        "description": "Lists the blogs of your organization. Requires the `blogs:read` scope.\n",
        "security": [
          {
            "apiKey": []
          },
          {
            "accessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "A list of blogs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Entity"
                  }
                }
              }
            }
          },
          "403": {
            "description": "API key is missing the `blogs:read` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "getApiBlogs"
      },
      "post": {
        "tags": [
          "Blogs"
        ],
        "summary": "Create a blog",
        "description": "Creates a blog for your organization. Requires the `blogs:write` scope. Fires the `blog.created` webhook.\n",
        "security": [
          {
            "apiKey": []
          },
          {
            "accessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "The created blog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          },
          "403": {
            "description": "API key is missing the `blogs:write` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "postApiBlogs"
      }
    },
    "/blogs": {
      "get": {
        "summary": "get blog details",
        "description": "returns a blog details",
        "parameters": [
          {
            "in": "path",
            "name": "blogId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "blog details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "_id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "coverUrl": {
                      "type": "string"
                    },
                    "creationTime": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "lastEditTime": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "locales": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "getBlogs"
      }
    },
    "/api/blogs/{blogId}": {
      "put": {
        "tags": [
          "Blogs"
        ],
        "summary": "Update a blog",
        "description": "Updates a blog owned by your organization. Requires the `blogs:write` scope.\n",
        "security": [
          {
            "apiKey": []
          },
          {
            "accessToken": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "blogId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The updated blog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          },
          "403": {
            "description": "Not your organization's blog, or missing the `blogs:write` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "putApiBlogsBlogId"
      }
    },
    "/api/topics": {
      "get": {
        "tags": [
          "Topics"
        ],
        "summary": "List topic ideas",
        "description": "Lists the topic ideas of a blog you own. Requires the `topics:read` scope.\n",
        "security": [
          {
            "apiKey": []
          },
          {
            "accessToken": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "blogId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The topic ideas",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          },
          "403": {
            "description": "API key is missing the `topics:read` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "getApiTopics"
      }
    },
    "/api/webhooksubscriptions": {
      "get": {
        "tags": [
          "Webhook subscriptions"
        ],
        "summary": "List webhook subscriptions",
        "description": "Webhook subscriptions deliver `article.created`, `article.updated`, `article.deleted`, `blog.created` and `contact.created` events to your server as signed POST requests (`X-Polyblog-Signature: t=<timestamp>,v1=<hex HMAC-SHA256 of \"timestamp.body\">`). An endpoint failing 20 times in a row is disabled automatically. Subscriptions are managed with an operator access token; the `secret` is only returned once, on create.\n",
        "security": [
          {
            "accessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Array of webhook subscriptions (without secrets)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Entity"
                  }
                }
              }
            }
          }
        },
        "operationId": "getApiWebhooksubscriptions"
      },
      "post": {
        "tags": [
          "Webhook subscriptions"
        ],
        "summary": "Create a webhook subscription",
        "description": "Pass a `blogId` to receive only that blog's events, or omit it for an organization-wide subscription. The response includes the signing `secret` exactly once — store it; it cannot be retrieved again.\n",
        "security": [
          {
            "accessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "blogId": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string",
                    "example": "https://example.com/polyblog-webhook"
                  },
                  "events": {
                    "type": "array",
                    "description": "Empty array subscribes to all events",
                    "items": {
                      "type": "string",
                      "enum": [
                        "article.created",
                        "article.updated",
                        "article.deleted",
                        "blog.created",
                        "contact.created"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created subscription, including its secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          }
        },
        "operationId": "postApiWebhooksubscriptions"
      }
    },
    "/api/webhooksubscriptions/{webhookSubscriptionId}": {
      "put": {
        "tags": [
          "Webhook subscriptions"
        ],
        "summary": "Update a webhook subscription",
        "description": "`url`, `events` and `active` are editable; the secret, organization and blog are immutable. Re-enabling an auto-disabled endpoint is done by setting `active` back to true.\n",
        "security": [
          {
            "accessToken": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "webhookSubscriptionId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated subscription (without secret)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          }
        },
        "operationId": "putApiWebhooksubscriptionsWebhookSubscriptionId"
      },
      "delete": {
        "tags": [
          "Webhook subscriptions"
        ],
        "summary": "Delete a webhook subscription",
        "security": [
          {
            "accessToken": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "webhookSubscriptionId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          }
        },
        "operationId": "deleteApiWebhooksubscriptionsWebhookSubscriptionId",
        "description": "Delete a webhook subscription"
      }
    }
  },
  "tags": []
}
