{
  "openapi": "3.1.0",
  "info": {
    "title": "zpe-comments",
    "version": "4.1.1",
    "description": "Comments on zpe.vc/comments: a public thread behind Turnstile and the moderation queue its owner runs. Every operation lives under the one base /_/api/v1, and this document is served at /_/api/v1/openapi.json. A machine call on the app's older addresses answers a 308 naming the versioned one."
  },
  "servers": [
    {
      "url": "/comments",
      "description": "zpe-comments on zpe.vc"
    }
  ],
  "paths": {
    "/_/api/v1/config": {
      "get": {
        "operationId": "getConfig",
        "summary": "One site's comment settings, its Turnstile sitekey and the length limit",
        "tags": [
          "config"
        ],
        "parameters": [
          {
            "name": "site",
            "in": "query",
            "description": "The site's slug — its address under this app's mount.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "This site's comment settings, its Turnstile site key, and its length and edit-window limits.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No site answers to that `site` slug.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/_/api/v1/bootstrap": {
      "get": {
        "operationId": "getBootstrap",
        "summary": "One post's comment tree, with the settings a composer needs",
        "tags": [
          "bootstrap"
        ],
        "parameters": [
          {
            "name": "site",
            "in": "query",
            "description": "The site's slug — its address under this app's mount.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "slug",
            "in": "query",
            "description": "The post's slug, as the site's own URLs spell it.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One post's comment tree, with the settings and limits a composer needs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "`slug` is missing or malformed, so the post cannot be addressed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No site answers to that `site` slug.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/_/api/v1/comments": {
      "get": {
        "operationId": "getComments",
        "summary": "One post's comment tree",
        "tags": [
          "comments"
        ],
        "parameters": [
          {
            "name": "site",
            "in": "query",
            "description": "The site's slug — its address under this app's mount.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "slug",
            "in": "query",
            "description": "The post's slug, as the site's own URLs spell it.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One post's comment tree.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "`slug` is missing or malformed, so the post cannot be addressed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No site answers to that `site` slug.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postComments",
        "summary": "Post an anonymous comment behind a Turnstile answer",
        "tags": [
          "comments"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "A value that names this attempt. Send the same one when retrying and the answer is the row the first call wrote (`200`) instead of a second row; present and empty is refused.",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Nothing new was written: this `Idempotency-Key` already made a comment here, and this is the row the first call wrote.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comment"
                  ],
                  "properties": {
                    "comment": {
                      "type": "object",
                      "required": [
                        "id",
                        "parent_id",
                        "status",
                        "created_at",
                        "body_html",
                        "author",
                        "depth"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "parent_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "approved",
                            "pending",
                            "spam",
                            "deleted"
                          ]
                        },
                        "created_at": {
                          "type": "integer"
                        },
                        "body_html": {
                          "type": "string"
                        },
                        "author": {
                          "type": "object",
                          "required": [
                            "name"
                          ],
                          "properties": {
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "depth": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "The comment was written. `status` is `approved`, or `pending` when the site holds first comments or the blocklist matched; `depth` is 1 for a top-level comment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comment"
                  ],
                  "properties": {
                    "comment": {
                      "type": "object",
                      "required": [
                        "id",
                        "parent_id",
                        "status",
                        "created_at",
                        "body_html",
                        "author",
                        "depth"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "parent_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "approved",
                            "pending",
                            "spam",
                            "deleted"
                          ]
                        },
                        "created_at": {
                          "type": "integer"
                        },
                        "body_html": {
                          "type": "string"
                        },
                        "author": {
                          "type": "object",
                          "required": [
                            "name"
                          ],
                          "properties": {
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "depth": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not one JSON object, `site` or `slug` is missing or malformed, the name or the text is refused, the honeypot field is filled, the Turnstile answer was rejected, or `Idempotency-Key` was sent empty.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing or does not open this account",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The request came from another origin, comments are disabled on this site, or this address is banned.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No site answers to that `site` slug.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "The request is not `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "This address posted too often; `retry-after` says when to try again.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "This deployment holds no Turnstile secret, so no write can be checked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/_/api/v1/comments/{id}": {
      "patch": {
        "operationId": "patchCommentsById",
        "summary": "Edit your own comment inside the edit window",
        "tags": [
          "comments"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The comment's id, as the tree answered it.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The comment's text was replaced; `body_html` and `edited_at` carry the new row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "The body is not one JSON object, or the text is refused — empty, over the length limit, or otherwise unacceptable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The request came from another origin, or this site's edit window is closed or has passed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No comment with that id, or it is not this visitor's to edit. A deleted or spam-marked row answers the same, so a caller learns nothing about a comment it does not own.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "The request is not `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteCommentsById",
        "summary": "Delete your own comment, or any comment on a site you own",
        "tags": [
          "comments"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The comment's id, as the tree answered it.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The comment was deleted: by its author, or by the account that owns its site.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "description": "The request came from another origin.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No comment with that id, or it is neither this visitor's own nor on a site the caller owns.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/_/api/v1/comments/{id}/report": {
      "post": {
        "operationId": "postCommentsByIdReport",
        "summary": "Report a comment to the site's owner, once per reader",
        "tags": [
          "comments"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the comment being reported.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "A value that names this attempt. Send the same one when retrying and the answer is the row the first call wrote (`200`) instead of a second row; present and empty is refused.",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Nothing new was written: this reader already reported this comment, or this `Idempotency-Key` already filed one. `duplicate` is `true`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reported",
                    "duplicate"
                  ],
                  "properties": {
                    "reported": {
                      "type": "boolean"
                    },
                    "duplicate": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "The report was filed and is open for the site's owner.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reported",
                    "duplicate"
                  ],
                  "properties": {
                    "reported": {
                      "type": "boolean"
                    },
                    "duplicate": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not one JSON object, `reason` is missing, or `Idempotency-Key` was sent empty.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing or does not open this account",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The request came from another origin.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No comment answers to that id in any account this app serves.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "The request is not `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/_/api/v1/operator/queue": {
      "get": {
        "operationId": "getOperatorQueue",
        "summary": "The moderation queue, with its open report counts",
        "tags": [
          "operator"
        ],
        "responses": {
          "200": {
            "description": "The moderation queue, with each comment's open report count.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "No key and no session: this caller is not signed in at the door.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The key is revoked, is not granted this app, or belongs to an account the door will not scope here.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its request ceiling; `retry-after` says when to try again.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The door or this deployment's key store did not answer, so the key could not be checked — not a verdict on the key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/operator/moderate": {
      "post": {
        "operationId": "postOperatorModerate",
        "summary": "Moderate, ban or unban with a key instead of the console",
        "tags": [
          "operator"
        ],
        "responses": {
          "200": {
            "description": "The moderation action was applied, and `status` is the comment's new state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "The body is not one JSON object, names no action or an unknown one, or is missing the `comment_id` or `user_id` its action needs.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No key and no session: this caller is not signed in at the door.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The request came from another origin, or the key is revoked, not granted this app, or scoped to an account that may not moderate here.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No comment or commenter answers to that id in this account.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "The request is not `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The key has spent its request ceiling; `retry-after` says when to try again.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The door or this deployment's key store did not answer, so the key could not be checked — not a verdict on the key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key minted on the account's settings page at zpe.vc/home/settings. Send it as `Authorization: Bearer <key>`."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem+json: `type`, `title`, `status`, `code` and `instance`, plus `detail` when a person should read a sentence.",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "instance"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "enum": [
              "bad-request",
              "unauthenticated",
              "malformed",
              "unknown",
              "mismatch",
              "revoked",
              "expired",
              "another-account",
              "scope-refused",
              "grant-refused",
              "forbidden",
              "not-found",
              "method-not-allowed",
              "conflict",
              "gone",
              "payload-too-large",
              "unsupported-media-type",
              "unprocessable",
              "rate-limited",
              "payment-required",
              "not-open",
              "internal",
              "unavailable"
            ]
          },
          "instance": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          }
        }
      }
    }
  },
  "x-surface": {
    "app": "zpe-comments",
    "base": "/_/api/v1",
    "document": "/_/api/v1/openapi.json",
    "generated_by": "zpe-kit machineDocument"
  },
  "x-zpe-routes": 9,
  "x-zpe-documented": 9,
  "x-zpe-internal": [],
  "security": [
    {}
  ]
}
