{
  "openapi": "3.1.0",
  "info": {
    "title": "updates.page API",
    "version": "1.0.0",
    "summary": "Publish and manage changelog posts programmatically.",
    "description": "The content API behind `@updatespage/cli` \u2014 what a terminal, a CI job or an agent uses to publish a changelog post.\n\n**Authentication.** Every endpoint takes a bearer token in the `Authorization` header. Get one interactively with `updates login` (OAuth 2.0 with PKCE, or the device grant on a machine with no browser), or by hand in the dashboard under Account \u2192 API Keys.\n\n**Entitlement.** API access is included for the first 14 days of a new account and continues on the Pro plan. A request made without it is refused with 403 at authentication time, whenever the key was created.\n\n**Scopes.** There are none. Every token this API issues carries the full access of the user who approved it \u2014 treat one as you would that person's password. A key marked `machine` is restricted to this document's endpoints (posts, categories, uploads, imports) and cannot reach account settings, billing, team or other keys; that is a blast radius, not authorization.\n\n**Naming.** Posts are `posts` throughout the API. The tables and some older payload keys still say `announcement`; both param keys are accepted.",
    "contact": {
      "name": "updates.page support",
      "email": "hey@updates.page",
      "url": "https://docs.updates.page"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://app.updates.page",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Guides and the CLI reference",
    "url": "https://docs.updates.page"
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Posts",
      "description": "Create, publish, schedule and delete changelog posts."
    },
    {
      "name": "Categories",
      "description": "The headings posts are filed under. Free accounts are capped at three."
    },
    {
      "name": "Uploads",
      "description": "Images for use in post content and cover images."
    },
    {
      "name": "Imports",
      "description": "Bring an existing changelog in from an RSS feed."
    },
    {
      "name": "OAuth",
      "description": "Getting, checking and revoking a token."
    },
    {
      "name": "Service",
      "description": "Liveness."
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "tags": [
          "Service"
        ],
        "summary": "Liveness check",
        "operationId": "health",
        "security": [],
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts": {
      "get": {
        "tags": [
          "Posts"
        ],
        "summary": "List posts",
        "operationId": "listPosts",
        "description": "Scheduled posts first (soonest last), then published (newest first), with drafts ordered by when they were last touched. Without `limit`, the whole list as a bare array — the shape this endpoint has always had. With `limit`, a paginated envelope: `{ posts, has_more, next_cursor }`, where `next_cursor` is an opaque cursor to pass back as `after` for the next page.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by state. Omit for everything.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "published",
                "scheduled"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, capped at 50. Providing it switches the response to the paginated envelope; a non-numeric value falls back to 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "The `next_cursor` from the previous page. Opaque; an unreadable value serves the first page. Only meaningful together with `limit`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The account's posts: a bare array without `limit`, the paginated envelope with it.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PostSummary"
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "posts",
                        "has_more"
                      ],
                      "properties": {
                        "posts": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/PostSummary"
                          }
                        },
                        "has_more": {
                          "type": "boolean",
                          "description": "Whether another page exists after this one."
                        },
                        "next_cursor": {
                          "type": "string",
                          "nullable": true,
                          "description": "Pass back as `after` to fetch the next page. Null on the last page."
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Posts"
        ],
        "summary": "Create a post",
        "operationId": "createPost",
        "description": "Creates a post. Omit `published_at` for a draft, set it in the past to publish immediately, or in the future to schedule.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "post"
                ],
                "properties": {
                  "post": {
                    "type": "object",
                    "required": [
                      "title"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "description": "The post's headline."
                      },
                      "content": {
                        "type": "string",
                        "description": "The body, as HTML. Plain text is accepted and rendered as-is."
                      },
                      "summary": {
                        "type": "string",
                        "description": "Short summary shown in feeds and embeds. Derived from the content when blank."
                      },
                      "category_id": {
                        "type": "integer",
                        "description": "Must belong to this account. Defaults to the account's first category."
                      },
                      "author_id": {
                        "type": "integer",
                        "description": "Must be an active member of this account. Defaults to the token's own user."
                      },
                      "is_public": {
                        "type": "boolean",
                        "description": "false keeps the post off the public changelog."
                      },
                      "override_url": {
                        "type": "string",
                        "description": "Link the post to an external page instead of its own."
                      },
                      "published_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "Null for a draft, a past time to publish, a future time to schedule. Sending null on an update returns a published or scheduled post to draft."
                      },
                      "images_blob_ids": {
                        "type": "array",
                        "items": {
                          "type": "integer"
                        },
                        "description": "Ids returned by POST /api/v1/uploads, for images used in the content."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created post. Note this is 200, not 201 \u2014 `create` renders the post through the default template.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The post's id, as returned by the API. Ids are hashids, not integers.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Posts"
        ],
        "summary": "Get a post",
        "operationId": "getPost",
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The post.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "404": {
            "description": "No such post on this account."
          }
        }
      },
      "put": {
        "tags": [
          "Posts"
        ],
        "summary": "Update a post",
        "operationId": "updatePost",
        "description": "Only the fields you send are changed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "post"
                ],
                "properties": {
                  "post": {
                    "type": "object",
                    "properties": {
                      "title": {
                        "type": "string",
                        "description": "The post's headline."
                      },
                      "content": {
                        "type": "string",
                        "description": "The body, as HTML. Plain text is accepted and rendered as-is."
                      },
                      "summary": {
                        "type": "string",
                        "description": "Short summary shown in feeds and embeds. Derived from the content when blank."
                      },
                      "category_id": {
                        "type": "integer",
                        "description": "Must belong to this account. Defaults to the account's first category."
                      },
                      "author_id": {
                        "type": "integer",
                        "description": "Must be an active member of this account. Defaults to the token's own user."
                      },
                      "is_public": {
                        "type": "boolean",
                        "description": "false keeps the post off the public changelog."
                      },
                      "override_url": {
                        "type": "string",
                        "description": "Link the post to an external page instead of its own."
                      },
                      "published_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "Null for a draft, a past time to publish, a future time to schedule. Sending null on an update returns a published or scheduled post to draft."
                      },
                      "images_blob_ids": {
                        "type": "array",
                        "items": {
                          "type": "integer"
                        },
                        "description": "Ids returned by POST /api/v1/uploads, for images used in the content."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The updated post.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "404": {
            "description": "No such post on this account."
          }
        }
      },
      "delete": {
        "tags": [
          "Posts"
        ],
        "summary": "Delete a post",
        "operationId": "deletePost",
        "description": "Permanent.",
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The deleted post.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "404": {
            "description": "No such post on this account."
          }
        }
      }
    },
    "/api/v1/posts/{id}/publish": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The post's id, as returned by the API. Ids are hashids, not integers.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Posts"
        ],
        "summary": "Publish or schedule a post",
        "operationId": "publishPost",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "published_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "A future time schedules the post. Defaults to now."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The published post.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/{id}/unpublish": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The post's id, as returned by the API. Ids are hashids, not integers.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Posts"
        ],
        "summary": "Return a post to draft",
        "operationId": "unpublishPost",
        "description": "Clears `published_at`, which also cancels a schedule.",
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The post, now a draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/{id}/update_cover_image": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The post's id, as returned by the API. Ids are hashids, not integers.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Posts"
        ],
        "summary": "Set the cover image",
        "operationId": "setPostCoverImage",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "post[cover_image]"
                ],
                "properties": {
                  "post[cover_image]": {
                    "type": "string",
                    "format": "binary",
                    "description": "png, jpg, gif or webp. The bracketed name is literal."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The post, with its new cover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          }
        },
        "description": "The field is named `post[cover_image]` \u2014 Rails reads it through `params.require(:post)`, so the bracket is part of the wire format, not a nested object. A client that sends a part named `post` containing JSON gets a 500."
      }
    },
    "/api/v1/posts/{id}/remove_cover_image": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The post's id, as returned by the API. Ids are hashids, not integers.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Posts"
        ],
        "summary": "Remove the cover image",
        "operationId": "removePostCoverImage",
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The post, without a cover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/categories": {
      "get": {
        "tags": [
          "Categories"
        ],
        "summary": "List categories",
        "operationId": "listCategories",
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The account's categories.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Category"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Categories"
        ],
        "summary": "Create a category",
        "operationId": "createCategory",
        "description": "Free accounts are capped at three, and a new account is seeded with exactly three \u2014 so this is refused until one is deleted.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "category"
                ],
                "properties": {
                  "category": {
                    "type": "object",
                    "required": [
                      "name"
                    ],
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "color": {
                        "type": "string",
                        "description": "Hex, e.g. #3B82F6."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The created category.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Category"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/categories/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "put": {
        "tags": [
          "Categories"
        ],
        "summary": "Rename or recolour a category",
        "operationId": "updateCategory",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "category"
                ],
                "properties": {
                  "category": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "color": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The updated category.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Category"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Categories"
        ],
        "summary": "Delete a category",
        "operationId": "deleteCategory",
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "Deleted."
          }
        }
      }
    },
    "/api/v1/uploads": {
      "post": {
        "tags": [
          "Uploads"
        ],
        "summary": "Upload an image",
        "operationId": "createUpload",
        "description": "Returns a public URL to use in post content, and the blob id to pass as `images_blob_ids`.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "png, jpg, gif or webp."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "201": {
            "description": "The stored image.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The image is too large.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/imports/rss": {
      "post": {
        "tags": [
          "Imports"
        ],
        "summary": "Import posts from an RSS feed",
        "operationId": "createRssImport",
        "description": "Starts a background import and returns immediately. Poll the import for its outcome. One import runs per account at a time. Owner or admin only.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "An http or https feed URL."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The import, now queued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "import": {
                      "$ref": "#/components/schemas/Import"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/imports": {
      "get": {
        "tags": [
          "Imports"
        ],
        "summary": "List recent imports",
        "operationId": "listImports",
        "description": "The twenty most recent, newest first.",
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "Recent imports.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "imports": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Import"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/imports/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Imports"
        ],
        "summary": "Check an import",
        "operationId": "getImport",
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The import.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "import": {
                      "$ref": "#/components/schemas/Import"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such import on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/oauth/device/code": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Start a device grant",
        "operationId": "createDeviceCode",
        "security": [],
        "description": "For a client that cannot open a browser. Show the user `verification_uri_complete`, then poll the token endpoint at `interval` seconds.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "client_id"
                ],
                "properties": {
                  "client_id": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "string",
                    "description": "Recorded and ignored. Every token is full-access."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The grant. `device_code` is returned once and stored only as a digest.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceCode"
                }
              }
            }
          },
          "401": {
            "description": "Unknown or disabled client_id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/oauth/token": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Redeem a grant for a token",
        "operationId": "createToken",
        "security": [],
        "description": "Takes either grant. A device grant is polled: `authorization_pending` until the user approves, `slow_down` if you are polling faster than `interval`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type",
                  "client_id"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "authorization_code",
                      "urn:ietf:params:oauth:grant-type:device_code"
                    ]
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string",
                    "description": "Authorization code grant."
                  },
                  "redirect_uri": {
                    "type": "string",
                    "description": "Authorization code grant. Must match the one the code was issued for."
                  },
                  "code_verifier": {
                    "type": "string",
                    "description": "Authorization code grant. PKCE is mandatory and S256 only."
                  },
                  "device_code": {
                    "type": "string",
                    "description": "Device grant."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "The grant was rejected, or is still pending.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthError"
                }
              }
            }
          },
          "403": {
            "description": "The approver may no longer authorize \u2014 the account lost API access, or they are no longer an owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthError"
                }
              }
            }
          },
          "429": {
            "description": "Polling too quickly (`slow_down`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/oauth/identity": {
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Who is this token",
        "operationId": "getIdentity",
        "description": "Confirms a token works and says what it can expect \u2014 including when API access runs out.",
        "responses": {
          "401": {
            "description": "The token is missing, unknown, revoked, expired or stale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The account may not use the API, the key is a machine key reaching outside the content API, or the user's role does not allow it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The token's owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Identity"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/oauth/revoke": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Revoke a token",
        "operationId": "revokeToken",
        "security": [
          {
            "bearerAuth": []
          },
          {}
        ],
        "description": "Invalidates the token server-side, so signing out is not just a local delete.\n\nSend the token you are revoking: either as the bearer credential, which is what a client signing itself out normally does, or as `token` in the body. Sending neither still answers 200 and revokes nothing.\n\nAlways answers 200 whether or not the token existed (RFC 7009 \u00a72.2), and works for an account that has lost API access \u2014 being unable to sign out because a plan lapsed would be exactly backwards.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "Defaults to the bearer token on the request."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer <token>`. Get one with `updates login`, or from Account \u2192 API Keys."
      }
    },
    "schemas": {
      "PostSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "A hashid, not an integer."
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "Draft",
              "Scheduled",
              "Published"
            ]
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "category_id": {
            "type": "integer"
          },
          "is_public": {
            "type": "boolean"
          },
          "portal_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Where the post reads on the public changelog."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Post": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "A hashid, not an integer."
          },
          "title": {
            "type": "string"
          },
          "content": {
            "type": [
              "string",
              "null"
            ],
            "description": "HTML."
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "Draft",
              "Scheduled",
              "Published"
            ]
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Null for a draft; a future time means scheduled."
          },
          "is_public": {
            "type": "boolean"
          },
          "override_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "portal_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "cover_image": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "category_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "category": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "color": {
                "type": "string"
              }
            }
          },
          "author_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "user_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "account_id": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Category": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string",
            "description": "Hex, e.g. #3B82F6."
          }
        }
      },
      "Import": {
        "type": "object",
        "description": "Poll `imported`/`skipped` for progress and `error` for why a failed import stopped.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The feed the import is reading."
          },
          "status": {
            "type": "string",
            "description": "`pending`, `processing`, `completed` or `failed`. A long archive stays `processing` across several batches."
          },
          "imported": {
            "type": "integer",
            "description": "Posts created so far, across every batch of this import."
          },
          "skipped": {
            "type": "integer",
            "description": "Entries the account already had. Counted on the first batch only \u2014 on a continuation they are mostly what this same import just created."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the import failed, when it did."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Anything worth reporting about an import that otherwise succeeded, such as stopping at the batch limit."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DeviceCode": {
        "type": "object",
        "properties": {
          "device_code": {
            "type": "string"
          },
          "user_code": {
            "type": "string",
            "description": "What the human types. Eight characters."
          },
          "verification_uri": {
            "type": "string",
            "format": "uri"
          },
          "verification_uri_complete": {
            "type": "string",
            "format": "uri",
            "description": "The same page with the code filled in."
          },
          "expires_in": {
            "type": "integer"
          },
          "interval": {
            "type": "integer",
            "description": "Seconds between polls. A floor, not a suggestion."
          }
        }
      },
      "TokenResponse": {
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string"
          },
          "token_type": {
            "type": "string",
            "const": "Bearer"
          },
          "account": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "tier": {
                "type": "string",
                "enum": [
                  "free",
                  "pro",
                  "business"
                ]
              }
            }
          }
        }
      },
      "Identity": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "account": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "tier": {
                "type": "string",
                "enum": [
                  "free",
                  "pro",
                  "business"
                ]
              },
              "api_access": {
                "type": "boolean",
                "description": "Whether this token still works."
              },
              "api_trial_ends_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When the new-account API trial runs out."
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "A human-readable sentence. Say what went wrong and, where there is one, what to do about it."
          },
          "errors": {
            "description": "Validation failures, keyed by field, when a record was rejected.",
            "type": [
              "object",
              "array"
            ]
          }
        }
      },
      "OauthError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "An RFC 6749 / RFC 8628 code \u2014 `authorization_pending`, `slow_down`, `access_denied`, `expired_token`, `invalid_grant`, `invalid_client`, `invalid_request`, `unsupported_grant_type`."
          },
          "error_description": {
            "type": "string"
          }
        }
      }
    }
  }
}
