{
  "name": "PedMono API",
  "version": "1.0.0",
  "baseUrl": "https://pedmono.worker.nitlix.com",
  "authentication": "Session cookies set by auth_login / auth_register / auth_nitlix. Send requests with credentials included. Auth levels: public (anyone), optional (works signed out, more when signed in), user (signed in), admin.",
  "routes": [
    {
      "method": "POST",
      "path": "/rpc",
      "description": "Every RPC event. Body: `{ \"event\": \"<name>\", \"input\": { ... } }`. Answers `{ \"data\": { \"ok\": true, \"data\": ... } }` or `{ \"data\": { \"ok\": false, \"data\": \"<message>\", \"code\"?: \"...\" } }`."
    },
    {
      "method": "GET",
      "path": "/health",
      "description": "Version and the live status of the database, both R2 buckets, email and Nitlix sign-in. 503 when degraded."
    },
    {
      "method": "GET",
      "path": "/docs",
      "description": "This page. `/docs.json` is the same, machine readable."
    },
    {
      "method": "GET",
      "path": "/cover/:postId?v=&share=",
      "description": "A post's cover image from the private bucket. Private posts need a session cookie or a `share` token."
    },
    {
      "method": "GET",
      "path": "/image-proxy?url=",
      "description": "Signed in only. Fetches a remote image so the browser can resize it (avatar or cover from a URL)."
    },
    {
      "method": "GET",
      "path": "/auth/nitlix",
      "description": "Starts a Nitlix sign-in and redirects back to the site's /auth/callback with a ticket for `auth_nitlix`."
    }
  ],
  "events": [
    {
      "event": "live",
      "auth": "optional",
      "description": "Returns the signed-in user, refreshing the short-lived auth token when needed.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "event": "auth_register",
      "auth": "public",
      "description": "Creates an account with a username, email and password, then signs it in.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "username": {
            "type": "string",
            "pattern": "^[a-z0-9_]{3,24}$"
          },
          "email": {
            "type": "string"
          },
          "password": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128
          },
          "remember": {
            "default": false,
            "type": "boolean"
          }
        },
        "required": [
          "username",
          "email",
          "password"
        ]
      }
    },
    {
      "event": "auth_login",
      "auth": "public",
      "description": "Signs in with email and password. `remember` keeps the session for 30 days, otherwise it ends with the browser session.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "password": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "remember": {
            "default": false,
            "type": "boolean"
          }
        },
        "required": [
          "email",
          "password"
        ]
      }
    },
    {
      "event": "auth_nitlix",
      "auth": "public",
      "description": "Completes a Nitlix sign-in. Redeems the one-time ticket against the state cookie set by `GET /auth/nitlix`, linking or creating the account.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "ticket": {
            "type": "string",
            "minLength": 1,
            "maxLength": 512
          },
          "remember": {
            "default": true,
            "type": "boolean"
          }
        },
        "required": [
          "ticket"
        ]
      }
    },
    {
      "event": "sign_out",
      "auth": "optional",
      "description": "Ends the current session and clears the auth cookies.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "event": "auth_reset_request",
      "auth": "public",
      "description": "Emails a password reset link. Limited to 3 per account per hour. Always answers the same way, so it can't be used to discover accounts.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          }
        },
        "required": [
          "email"
        ]
      }
    },
    {
      "event": "auth_reset_check",
      "auth": "public",
      "description": "Checks a reset link before the new password is typed.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          }
        },
        "required": [
          "code"
        ]
      }
    },
    {
      "event": "auth_reset_complete",
      "auth": "public",
      "description": "Sets a new password from a reset link and signs out every existing session.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "password": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128
          }
        },
        "required": [
          "code",
          "password"
        ]
      }
    },
    {
      "event": "password_change",
      "auth": "user",
      "description": "Changes the password (or sets one for Nitlix-only accounts) and signs out every other session.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "currentPassword": {
            "type": "string",
            "maxLength": 128
          },
          "newPassword": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128
          }
        },
        "required": [
          "newPassword"
        ]
      }
    },
    {
      "event": "session_list",
      "auth": "user",
      "description": "Lists the signed-in user's active sessions, newest first.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "event": "session_revoke",
      "auth": "user",
      "description": "Signs out one of the user's sessions.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "event": "profile_update",
      "auth": "user",
      "description": "Updates any of username, email, bio and preferences. Omitted fields stay as they are.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "username": {
            "type": "string",
            "pattern": "^[a-z0-9_]{3,24}$"
          },
          "email": {
            "type": "string"
          },
          "bio": {
            "type": "string",
            "maxLength": 500
          },
          "preferences": {
            "type": "object",
            "properties": {
              "theme": {
                "type": "string",
                "enum": [
                  "system",
                  "light",
                  "dark"
                ]
              },
              "defaultPostType": {
                "type": "string",
                "enum": [
                  "public",
                  "private"
                ]
              },
              "emailOnComment": {
                "type": "boolean"
              }
            }
          }
        }
      }
    },
    {
      "event": "upload_prepare",
      "auth": "user",
      "description": "Step 1 of an upload. Reserves unverified objects and returns presigned R2 URLs to PUT WebP bytes to (Content-Type: image/webp). Confirm with `upload_verify`.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "purpose": {
            "type": "string",
            "enum": [
              "avatar",
              "cover_image"
            ]
          },
          "items": {
            "minItems": 1,
            "maxItems": 8,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "variant": {
                  "type": "string",
                  "maxLength": 16
                },
                "byteSize": {
                  "type": "integer",
                  "exclusiveMinimum": 0,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "variant",
                "byteSize"
              ]
            }
          }
        },
        "required": [
          "purpose",
          "items"
        ]
      }
    },
    {
      "event": "upload_verify",
      "auth": "user",
      "description": "Step 2 of an upload. The worker HEADs each object in R2, checks its real size and type, and marks it verified. A new avatar set replaces the previous one.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "ids": {
            "minItems": 1,
            "maxItems": 8,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            }
          }
        },
        "required": [
          "ids"
        ]
      }
    },
    {
      "event": "avatar_remove",
      "auth": "user",
      "description": "Removes the user's avatar in every size.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "event": "user_profile",
      "auth": "user",
      "description": "A user's public profile by username. List their posts with `post_search` and `author`.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "username": {
            "type": "string",
            "pattern": "^[a-z0-9_]{3,24}$"
          }
        },
        "required": [
          "username"
        ]
      }
    },
    {
      "event": "webhook_update",
      "auth": "user",
      "description": "Sets (or clears, with `url: null`) the webhook notified with a signed `comment.created` event whenever someone comments on one of your posts.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "url": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2048
              },
              {
                "type": "null"
              }
            ]
          },
          "rotateSecret": {
            "default": false,
            "type": "boolean"
          }
        },
        "required": [
          "url"
        ]
      }
    },
    {
      "event": "webhook_test",
      "auth": "user",
      "description": "Sends a signed `webhook.test` event to the configured webhook and reports how it answered.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "event": "post_search",
      "auth": "optional",
      "description": "Searches posts by title, content or tag, with sorting and pagination. Results only include posts the caller can see: public posts, their own, and (for admins) posts they made private. `mine` needs a session.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "q": {
            "default": "",
            "type": "string",
            "maxLength": 200
          },
          "in": {
            "default": "all",
            "type": "string",
            "enum": [
              "all",
              "title",
              "content",
              "tag"
            ]
          },
          "tag": {
            "type": "string",
            "maxLength": 64
          },
          "type": {
            "default": "public",
            "type": "string",
            "enum": [
              "public",
              "private",
              "all"
            ]
          },
          "sort": {
            "default": "new",
            "type": "string",
            "enum": [
              "new",
              "old",
              "likes",
              "comments",
              "title"
            ]
          },
          "author": {
            "type": "string",
            "pattern": "^[a-z0-9_]{3,24}$"
          },
          "mine": {
            "default": false,
            "type": "boolean"
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 10000
          },
          "perPage": {
            "default": 12,
            "type": "integer",
            "minimum": 1,
            "maximum": 50
          }
        }
      }
    },
    {
      "event": "post_get",
      "auth": "optional",
      "description": "A post by id with its comments. Private posts are only visible to their author, or to the admin who made them private.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "event": "post_get_shared",
      "auth": "optional",
      "description": "Opens a post through a temporary access link, even if it's private and the reader is signed out.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "minLength": 3,
            "maxLength": 200
          }
        },
        "required": [
          "token"
        ]
      }
    },
    {
      "event": "post_create",
      "auth": "user",
      "description": "Creates a post. `content` is HTML and is sanitised against a whitelist. `coverId` is a verified `cover_image` upload. Limited by the author's max posts.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "content": {
            "type": "string",
            "maxLength": 100000
          },
          "tags": {
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "type": {
            "default": "public",
            "type": "string",
            "enum": [
              "public",
              "private"
            ]
          },
          "coverId": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 64
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "title",
          "content"
        ]
      }
    },
    {
      "event": "post_update",
      "auth": "user",
      "description": "Edits any field of a post by id. Authors can edit their own posts. Admins can edit public posts and make them private; they keep access to posts they made private, but not to posts their author made private. `coverId: null` removes the cover.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "content": {
            "type": "string",
            "maxLength": 100000
          },
          "tags": {
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "type": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ]
          },
          "coverId": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 64
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "event": "post_delete",
      "auth": "user",
      "description": "Deletes one of your posts along with its comments, likes, share links and cover image.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "event": "post_like",
      "auth": "user",
      "description": "Likes or unlikes a post you can see. A user can like a post once; repeating a like changes nothing.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "liked": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "liked"
        ]
      }
    },
    {
      "event": "post_export",
      "auth": "user",
      "description": "Exports all of your posts. JSON embeds each cover image as base64 so the file can be re-imported anywhere; CSV is a flat table for spreadsheets.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "format": {
            "type": "string",
            "enum": [
              "json",
              "csv"
            ]
          }
        },
        "required": [
          "format"
        ]
      }
    },
    {
      "event": "post_import",
      "auth": "user",
      "description": "Imports posts from JSON (an array, or this API's own export) or XML. Every post is created new; ids in the file are ignored. Invalid entries are skipped and reported.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "format": {
            "type": "string",
            "enum": [
              "json",
              "xml"
            ]
          },
          "content": {
            "type": "string",
            "minLength": 1,
            "maxLength": 26214400
          }
        },
        "required": [
          "format",
          "content"
        ]
      }
    },
    {
      "event": "share_create",
      "auth": "user",
      "description": "Creates a temporary access link to a post you can see. It opens the post for anyone until it expires, is revoked, or you lose access to the post.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "postId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "hours": {
            "type": "number"
          }
        },
        "required": [
          "postId",
          "hours"
        ]
      }
    },
    {
      "event": "share_list",
      "auth": "user",
      "description": "Active share links for a post: all of them for its author, otherwise just the ones you made.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "postId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          }
        },
        "required": [
          "postId"
        ]
      }
    },
    {
      "event": "share_revoke",
      "auth": "user",
      "description": "Revokes a share link. Its creator or the post's author can do this.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "event": "comment_create",
      "auth": "user",
      "description": "Comments on any post you can read. The author's webhook (and email, if they opted in) is notified.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "postId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "content": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          }
        },
        "required": [
          "postId",
          "content"
        ]
      }
    },
    {
      "event": "comment_update",
      "auth": "user",
      "description": "Edits one of your comments.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "content": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          }
        },
        "required": [
          "id",
          "content"
        ]
      }
    },
    {
      "event": "comment_delete",
      "auth": "user",
      "description": "Deletes a comment. Its author can, and so can the post's author and admins who can see the post, to moderate.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "event": "admin_users",
      "auth": "admin",
      "description": "Users by id, username or email, with their post counts and limits.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "q": {
            "default": "",
            "type": "string",
            "maxLength": 100
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 10000
          }
        }
      }
    },
    {
      "event": "admin_promote",
      "auth": "admin",
      "description": "Promotes a user to admin by their user id.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          }
        },
        "required": [
          "userId"
        ]
      }
    },
    {
      "event": "admin_set_max_posts",
      "auth": "admin",
      "description": "Sets how many posts a user may have. Leave out `userId` to apply it to every user at once.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "maxPosts": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          }
        },
        "required": [
          "maxPosts"
        ]
      }
    },
    {
      "event": "admin_logs",
      "auth": "admin",
      "description": "Activity log, newest first. Filter by user (id or username), action (exact, or a prefix ending in `.` like `post.`), IP prefix, user agent and an ISO date range.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "user": {
            "type": "string",
            "maxLength": 64
          },
          "action": {
            "type": "string",
            "maxLength": 64
          },
          "ip": {
            "type": "string",
            "maxLength": 64
          },
          "userAgent": {
            "type": "string",
            "maxLength": 200
          },
          "from": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "to": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 10000
          }
        }
      }
    },
    {
      "event": "admin_logs_export",
      "auth": "admin",
      "description": "The same filters as `admin_logs`, as a CSV file of up to 10000 rows.",
      "input": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "user": {
            "type": "string",
            "maxLength": 64
          },
          "action": {
            "type": "string",
            "maxLength": 64
          },
          "ip": {
            "type": "string",
            "maxLength": 64
          },
          "userAgent": {
            "type": "string",
            "maxLength": 200
          },
          "from": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "to": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          }
        }
      }
    }
  ]
}