Something went wrong. Try again.
A Deno-powered backend service for Plants vs. Zombies: MODDED. [Read-only GitHub mirror] docs.pvzm.net
express typescript expressjs plant deno jspvz pvzm game online backend plants-vs-zombies zombie javascript plants modded vs plantsvszombies openapi pvz noads
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783openapi: 3.0.3info: title: PVZM Backend API description: "API for the Plants vs. Zombies: MODDED level sharing platform. Supports level uploading, downloading, browsing, favoriting, reporting, and admin management." version: 0.6.5 contact: url: https://pvzm.net
servers: - url: https://backend.pvzm.net description: Production
tags: - name: Health description: Health check endpoints - name: Config description: Server configuration - name: Levels description: Public level operations - name: Auth description: GitHub OAuth authentication - name: Admin description: Admin level management
paths: /api/health: get: tags: [Health] summary: Health check operationId: getHealth responses: "200": description: Server is healthy content: application/json: schema: type: object required: [status, timestamp, version] properties: status: type: string example: ok timestamp: type: string format: date-time version: type: string example: 0.6.5
/api/config: get: tags: [Config] summary: Get server configuration operationId: getConfig responses: "200": description: Current server configuration content: application/json: schema: type: object required: [turnstileEnabled, turnstileSiteKey, moderationEnabled] properties: turnstileEnabled: type: boolean turnstileSiteKey: type: string nullable: true moderationEnabled: type: boolean
/api/levels: get: tags: [Levels] summary: List levels description: Retrieve a paginated, filterable list of levels. operationId: getLevels parameters: - name: page in: query schema: type: integer default: 1 minimum: 1 - name: limit in: query schema: type: integer default: 10 minimum: 1 - name: sort in: query schema: type: string enum: [plays, recent, favorites, featured] default: plays - name: reversed_order in: query schema: type: string enum: ["true", "false"] default: "false" - name: author in: query description: Filter by author name (partial match) schema: type: string - name: is_water in: query schema: type: string enum: ["true", "false"] - name: version in: query schema: type: integer - name: token in: query description: One-time token to fetch a specific level by ID. Ignores all other filters when provided. schema: type: string responses: "200": description: Paginated list of levels content: application/json: schema: type: object required: [levels, pagination] properties: levels: type: array items: $ref: "#/components/schemas/LevelSummary" pagination: $ref: "#/components/schemas/Pagination" "429": $ref: "#/components/responses/RateLimited"
post: tags: [Levels] summary: Upload a level description: | Upload a new level in IZL3 binary format. Rate limited to 1 upload per 60 seconds per IP.
The request body must be sent as raw binary (Content-Type: application/octet-stream) containing the IZL3 level file.
Validation includes: IZL3 format check, name/author content moderation (OpenAI + bad-words filter), plant/zombie placement rules, and optional Turnstile CAPTCHA verification. operationId: createLevel parameters: - name: author in: query required: true description: Author name (max 11 characters) schema: type: string maxLength: 11 - name: turnstileResponse in: query description: Cloudflare Turnstile CAPTCHA token (required if Turnstile is enabled) schema: type: string responses: "201": description: Level created successfully content: application/json: schema: type: object required: [id, name, author, created_at, sun, is_water, version] properties: id: type: integer name: type: string author: type: string created_at: type: integer description: Unix timestamp sun: type: integer is_water: type: boolean version: type: integer "400": $ref: "#/components/responses/BadRequest" "415": description: Unsupported media type (Content-Type must be application/octet-stream) content: application/json: schema: $ref: "#/components/schemas/Error" "429": $ref: "#/components/responses/RateLimited"
/api/levels/{id}: get: tags: [Levels] summary: Get a level by ID operationId: getLevel parameters: - $ref: "#/components/parameters/LevelId" responses: "200": description: Level details content: application/json: schema: $ref: "#/components/schemas/LevelSummary" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound"
/api/levels/{id}/download: get: tags: [Levels] summary: Download a level file description: Downloads the IZL3 level file. Increments the play count. Rate limited to 5 downloads per 5 seconds per IP. operationId: downloadLevel parameters: - $ref: "#/components/parameters/LevelId" responses: "200": description: Level file download headers: Content-Disposition: schema: type: string example: attachment; filename="MyLevel.izl3" content: application/octet-stream: schema: type: string format: binary "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited"
/api/levels/{id}/report: post: tags: [Levels] summary: Report a level operationId: reportLevel parameters: - $ref: "#/components/parameters/LevelId" requestBody: required: true content: application/json: schema: type: object required: [reason] properties: reason: type: string description: Description of why the level is being reported responses: "200": description: Report submitted content: application/json: schema: type: object required: [success] properties: success: type: boolean example: true "404": $ref: "#/components/responses/NotFound"
/api/levels/{id}/favorite: post: tags: [Levels] summary: Toggle favorite on a level description: Toggles the favorite state for the requesting IP. Rate limited to 30 actions per 10 seconds per IP. operationId: toggleFavorite parameters: - $ref: "#/components/parameters/LevelId" responses: "200": description: Favorite toggled content: application/json: schema: type: object required: [success, level] properties: success: type: boolean example: true level: type: object required: [id, name, author, favorites] properties: id: type: integer name: type: string author: type: string favorites: type: integer "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited"
# --- Auth ---
/api/auth/github: get: tags: [Auth] summary: Initiate GitHub OAuth login description: Redirects the user to GitHub for OAuth authentication. operationId: githubLogin responses: "302": description: Redirect to GitHub OAuth
/api/auth/github/callback: get: tags: [Auth] summary: GitHub OAuth callback description: Handles the OAuth callback from GitHub. Redirects to `/admin.html` on success. operationId: githubCallback parameters: - name: code in: query schema: type: string responses: "302": description: Redirect to admin page on success
/api/auth/status: get: tags: [Auth] summary: Check authentication status operationId: getAuthStatus responses: "200": description: Authentication status content: application/json: schema: oneOf: - type: object required: [authenticated, user] properties: authenticated: type: boolean enum: [true] user: type: object required: [username, displayName, profileUrl, avatarUrl] properties: username: type: string displayName: type: string profileUrl: type: string avatarUrl: type: string - type: object required: [authenticated] properties: authenticated: type: boolean enum: [false]
/api/auth/logout: get: tags: [Auth] summary: Logout operationId: logout responses: "200": description: Logged out successfully
# --- Admin ---
/api/admin/levels: get: tags: [Admin] summary: List levels (admin) description: Paginated level list with search. Requires GitHub OAuth. operationId: getAdminLevels security: - githubOAuth: [] parameters: - name: page in: query schema: type: integer default: 1 - name: limit in: query schema: type: integer default: 10 - name: q in: query description: Search query (searches name, author, ID) schema: type: string responses: "200": description: Admin level listing content: application/json: schema: type: object required: [levels, total, page, limit, totalPages] properties: levels: type: array items: $ref: "#/components/schemas/LevelRecord" total: type: integer page: type: integer limit: type: integer totalPages: type: integer "401": $ref: "#/components/responses/Unauthorized"
/api/admin/levels/{id}: put: tags: [Admin] summary: Update a level description: Update level metadata. Requires GitHub OAuth or a valid one-time token. operationId: updateLevel security: - githubOAuth: [] - oneTimeToken: [] parameters: - $ref: "#/components/parameters/LevelId" - name: token in: query description: One-time admin token (alternative to OAuth) schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string author: type: string sun: type: integer is_water: type: integer enum: [0, 1] difficulty: type: integer favorites: type: integer plays: type: integer featured: type: integer enum: [0, 1] featured_at: type: integer nullable: true responses: "200": description: Level updated content: application/json: schema: type: object required: [success, level] properties: success: type: boolean example: true level: $ref: "#/components/schemas/LevelRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound"
delete: tags: [Admin] summary: Delete a level description: Permanently deletes a level, its file, and all associated data. Requires GitHub OAuth or a valid one-time token. operationId: deleteLevel security: - githubOAuth: [] - oneTimeToken: [] parameters: - $ref: "#/components/parameters/LevelId" - name: token in: query description: One-time admin token (alternative to OAuth) schema: type: string responses: "200": description: Level deleted content: application/json: schema: type: object required: [success] properties: success: type: boolean example: true "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound"
/api/admin/levels/{id}/token: post: tags: [Admin] summary: Generate a one-time token for a level description: Creates a single-use token scoped to a specific level, allowing unauthenticated edit/delete access. operationId: generateToken security: - githubOAuth: [] parameters: - $ref: "#/components/parameters/LevelId" responses: "200": description: Token generated content: application/json: schema: type: object required: [token, level_id] properties: token: type: string example: token_abc123... level_id: type: integer "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound"
/api/admin/levels/{id}/feature: post: tags: [Admin] summary: Feature a level description: Marks a level as featured. Requires GitHub OAuth. operationId: featureLevel security: - githubOAuth: [] parameters: - $ref: "#/components/parameters/LevelId" responses: "200": description: Level featured content: application/json: schema: type: object required: [success, level] properties: success: type: boolean example: true level: $ref: "#/components/schemas/LevelRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound"
delete: tags: [Admin] summary: Unfeature a level description: Removes the featured status from a level. Requires GitHub OAuth. operationId: unfeatureLevel security: - githubOAuth: [] parameters: - $ref: "#/components/parameters/LevelId" responses: "200": description: Level unfeatured content: application/json: schema: type: object required: [success, level] properties: success: type: boolean example: true level: $ref: "#/components/schemas/LevelRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound"
components: securitySchemes: githubOAuth: type: http scheme: bearer description: GitHub OAuth2 session-based authentication (via browser cookies) oneTimeToken: type: apiKey in: query name: token description: Single-use token scoped to a specific level ID
parameters: LevelId: name: id in: path required: true schema: type: integer description: Level ID
schemas: LevelSummary: type: object required: [id, name, author, created_at, sun, is_water, favorites, plays, difficulty, version, featured, featured_at] properties: id: type: integer name: type: string author: type: string created_at: type: integer description: Unix timestamp sun: type: integer is_water: type: boolean favorites: type: integer plays: type: integer difficulty: type: integer version: type: integer featured: type: integer enum: [0, 1] featured_at: type: integer nullable: true description: Unix timestamp thumbnail: type: array nullable: true description: Array of plant placement tuples [plantIndex, eleLeft, eleTop, eleWidth, eleHeight, zIndex] items: type: array items: type: number
LevelRecord: type: object required: [id, name, author, sun, is_water, difficulty, favorites, plays, version, featured, featured_at, logging_data] properties: id: type: integer name: type: string author: type: string sun: type: integer is_water: type: integer enum: [0, 1] difficulty: type: integer favorites: type: integer plays: type: integer version: type: integer featured: type: integer enum: [0, 1] featured_at: type: integer nullable: true logging_data: type: string nullable: true description: JSON string containing Discord/Bluesky message IDs for logging management
Pagination: type: object required: [total, page, limit, pages] properties: total: type: integer page: type: integer limit: type: integer pages: type: integer
Error: type: object required: [error, message] properties: error: type: string message: type: string retryAfterSeconds: type: number description: Present on 429 responses
responses: BadRequest: description: Invalid input or validation failure content: application/json: schema: $ref: "#/components/schemas/Error" NotFound: description: Resource not found content: application/json: schema: $ref: "#/components/schemas/Error" Unauthorized: description: Authentication required content: application/json: schema: $ref: "#/components/schemas/Error" RateLimited: description: Too many requests headers: Retry-After: schema: type: integer content: application/json: schema: $ref: "#/components/schemas/Error"