API Reference
Technical documentation for the Pumpkin Hub REST API (Axum / Rust).
Next up: User Experience Improvements, Collections & Curations.
Base URL
http://localhost:8080/api/v1
All endpoints are prefixed with /api/v1. Versioning is handled by
nesting in the Axum router, allowing the addition of /api/v2 without
impacting existing routes.
Common Headers
| Header | Direction | Description |
|---|---|---|
Content-Type | Request / Response | application/json |
x-request-id | Response | Unique UUID assigned to each request (traceability) |
Content-Encoding | Response | gzip if the client supports compression |
Authorization | Request | Bearer <jwt> — alternative to the cookie for programmatic API clients |
Retry-After | Response | Seconds to wait before retrying (returned on 429 Too Many Requests) |
Rate Limiting
All endpoints are protected by rate limiting. For anonymous or JWT-authenticated requests
a per-IP limiter applies. When a valid X-API-Key header is
present the per-key quotas configured at key creation are used instead.
Auth-sensitive endpoints keep an additional strict per-IP limiter to prevent brute-force.
| Tier | Applies To | Burst | Sustained Rate |
|---|---|---|---|
| IP (General) | All routes without a valid API key | 30 requests | 1 request / second |
| API Key | All routes when authenticated with X-API-Key |
Configured per key (default 30) | Configured per key (default 1 / second) |
| Auth (Strict) | /auth/register, /auth/login, /auth/forgot-password, /auth/reset-password, OAuth entry points |
5 requests | 1 request / 4 seconds |
IP-based thresholds are configurable via environment variables
(RATE_LIMIT_GENERAL_PER_SECOND, RATE_LIMIT_GENERAL_BURST_SIZE,
RATE_LIMIT_AUTH_PER_SECOND, RATE_LIMIT_AUTH_BURST_SIZE).
Per-API-key quotas are set when the key is created via rate_limit_per_second
and rate_limit_burst_size fields.
When a client exceeds the limit, a 429 Too Many Requests response
is returned with a Retry-After header.
Health Check
GET /api/v1/health
Returns the service liveness status including database connectivity. Used by Docker healthchecks and load balancers.
Request
curl -X GET http://localhost:8080/api/v1/health
Response 200 OK
{
"status": "ok",
"service": "pumpkin-hub-api",
"version": "0.1.0",
"database": "connected"
}
Response 503 Service Unavailable
{
"status": "degraded",
"service": "pumpkin-hub-api",
"version": "0.1.0",
"database": "disconnected"
}
| Field | Type | Description |
|---|---|---|
status | string | Service state: "ok" or "degraded" |
service | string | Service name |
version | string | Crate version (from Cargo.toml) |
database | string | Database connectivity: "connected" or "disconnected" |
Public Statistics
GET /api/v1/stats
Returns aggregate platform statistics. No authentication required. Used by the homepage hero section to display community metrics.
Request
curl -X GET http://localhost:8080/api/v1/stats
Response 200 OK
{
"total_plugins": 42,
"total_authors": 15,
"total_downloads": 12870
}
| Field | Type | Description |
|---|---|---|
total_plugins | integer | Total number of active plugins on the platform |
total_authors | integer | Total number of users who have published at least one plugin |
total_downloads | integer | Cumulative download count across all plugins |
Error Handling
All errors are returned in a unified JSON format, via the centralized
AppError type:
{
"error": "Error description"
}
Error Codes
| HTTP Status | Variant | Meaning |
|---|---|---|
| 401 | Unauthorized | Authentication required or invalid token |
| 403 | Forbidden | Authenticated but not authorized (e.g., not the resource owner) |
| 404 | NotFound | Resource not found |
| 409 | Conflict | Resource conflict (e.g., duplicate slug or name) |
| 422 | UnprocessableEntity | Invalid data (details in message) |
| 429 | TooManyRequests | Rate limit exceeded — Retry-After header included |
| 500 | Internal | Internal error (details logged server-side, never exposed to client) |
| 504 | Timeout | Request exceeded the 30-second global timeout |
500) never transmit the root cause to the client.
The message is replaced by "An unexpected error occurred" and the technical
reason is traced server-side via tracing::error!.
CORS Policy
The CORS policy is configured via the ALLOWED_ORIGINS environment variable
(comma-separated values).
| Parameter | Value |
|---|---|
| Allowed Origins | Configurable via ALLOWED_ORIGINS |
| Methods | GET, POST, PUT, PATCH, DELETE |
| Allowed Headers | Content-Type, Authorization |
| Credentials | Enabled (allow_credentials: true) |
| Max Age (preflight) | 3600 seconds (1 hour) |
Authentication
Pumpkin Hub supports multiple authentication methods:
email/password registration, GitHub OAuth, Google OAuth,
and Discord OAuth. After authentication, a signed JWT is stored
in an HttpOnly cookie (SameSite=Lax). Protected
endpoints accept the token either via cookie or via the
Authorization: Bearer <jwt> header.
Email / Password
POST /api/v1/auth/register
Creates a new account with email and password. Passwords are hashed with Argon2id.
Request Body
{
"username": "rustcraftdev",
"email": "alex@example.com",
"password": "my_secure_password"
}
| Field | Type | Constraints |
|---|---|---|
username | string | 1–39 chars, alphanumeric + hyphens/underscores |
email | string | Valid email, max 255 chars, case-insensitive unique |
password | string | 8–128 characters |
Response 200 OK
Returns the UserProfile and sets the pumpkin_hub_token cookie.
Error Cases
| Code | Cause |
|---|---|
| 409 | Username or email already taken |
| 422 | Validation error (bad username, email or password length) |
POST /api/v1/auth/login
Authenticates with email and password.
Request Body
{
"email": "alex@example.com",
"password": "my_secure_password"
}
Response 200 OK
Returns the UserProfile and sets the pumpkin_hub_token cookie.
Error Cases
| Code | Cause |
|---|---|
| 401 | Invalid email or password (or account has no password — OAuth-only) |
OAuth Providers
Each OAuth provider follows the same pattern: GET /auth/{provider} redirects to the
provider, and GET /auth/{provider}/callback handles the return.
GET /api/v1/auth/github
Redirects to GitHub's OAuth page. Scopes: read:user, user:email.
GET /api/v1/auth/google
Redirects to Google's OAuth page. Scopes: openid, email, profile.
GET /api/v1/auth/discord
Redirects to Discord's OAuth page. Scopes: identify, email.
GET /api/v1/auth/{provider}/callback
Handles the OAuth callback. Exchanges the authorization code for an access token, fetches the user's profile, upserts/links in the database, and issues a JWT cookie. Redirects to the frontend after successful authentication.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
code | string | Authorization code from the provider |
state | string | CSRF state to validate against the cookie |
Error Cases
| Code | Cause |
|---|---|
| 401 | Missing CSRF cookie or state mismatch |
| 422 | Provider not configured (Google/Discord) |
| 500 | Token exchange or DB error |
GET /api/v1/auth/me Protected
Returns the currently authenticated user's profile. Requires a valid JWT (cookie or Bearer header).
Response 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "octocat",
"display_name": "The Octocat",
"email": "octocat@github.com",
"avatar_url": "https://avatars.githubusercontent.com/u/1",
"bio": null,
"email_verified": true,
"is_active": true,
"role": "author",
"created_at": "2026-03-06T12:00:00Z"
}
| Field | Type | Description |
|---|---|---|
id | UUID | Internal user identifier |
username | string | Unique username |
display_name | string | null | User's display name |
email | string | null | User's email address |
avatar_url | string | null | Avatar URL — points to /api/v1/users/{id}/avatar after upload, or to OAuth provider URL on first login |
bio | string | null | User bio |
email_verified | boolean | Whether the user’s email has been verified |
is_active | boolean | Account active status (deactivated users cannot log in) |
role | author | moderator | admin | Platform role |
created_at | ISO 8601 | Registration date |
PUT /api/v1/auth/me Protected
Updates the authenticated user's profile fields. All fields are optional — only provided fields are changed.
Request Body application/json
{
"display_name": "The Octocat", // optional, max 100 chars
"bio": "Minecraft plugin dev." // optional, max 500 chars
}
Response 200 OK
Returns the updated UserProfile object (same shape as GET /auth/me).
| Status | Meaning |
|---|---|
| 200 | Profile updated, returns UserProfile |
| 400 | No fields provided / validation error |
| 401 | Not authenticated |
POST /api/v1/auth/avatar Protected
Uploads a new avatar image for the authenticated user using multipart/form-data.
The image is stored as binary (BYTEA) in the database. After a successful upload,
avatar_url in the UserProfile is updated to point to /api/v1/users/{id}/avatar.
Request multipart/form-data
| Field | Type | Constraints |
|---|---|---|
avatar | file | Required. MIME: image/jpeg, image/png, image/webp, image/gif. Max 2 MB. Validated with magic bytes. |
Response 200 OK
Returns the updated UserProfile object with the new avatar_url.
| Status | Meaning |
|---|---|
| 200 | Avatar stored, returns updated UserProfile |
| 400 | Missing field, unsupported MIME type, invalid magic bytes, or file > 2 MB |
| 401 | Not authenticated |
GET /api/v1/users/{id}/avatar
Public endpoint that serves the binary avatar image for the given user UUID.
Returns the raw image bytes with the correct Content-Type header.
Includes Cache-Control: public, max-age=3600.
| Status | Meaning |
|---|---|
| 200 | Binary image with Content-Type (e.g. image/png) |
| 404 | User has no uploaded avatar |
POST /api/v1/auth/logout
Clears the JWT cookie (pumpkin_hub_token), effectively logging the user out. Uses POST method to prevent CSRF-based sign-out attacks via external links.
Response 200 OK
{ "message": "Logged out" }
Email Verification
When a user registers with email/password, a verification email is sent with a time-limited token (24-hour TTL). OAuth-registered accounts are automatically verified. Tokens are hashed with SHA-256 before storage and are single-use.
POST /api/v1/auth/verify-email
Verifies the user's email address using a token received by email.
Request Body
{
"token": "abc123..."
}
Response 200 OK
{ "message": "Email verified" }
Error Cases
| Code | Cause |
|---|---|
| 400 | Invalid or expired token |
POST /api/v1/auth/resend-verification Protected
Resends a verification email for the authenticated user. Requires a valid JWT session.
Response 200 OK
{ "message": "Verification email sent" }
Error Cases
| Code | Cause |
|---|---|
| 400 | Email already verified |
| 401 | Not authenticated |
Password Recovery
A password reset flow allows users to recover access via a time-limited email token (1-hour TTL).
The forgot-password endpoint always returns 200 regardless of whether
the email exists, to prevent email enumeration.
POST /api/v1/auth/forgot-password
Sends a password reset email if the address is associated with an account. Always returns 200 for security.
Request Body
{
"email": "alex@example.com"
}
Response 200 OK
{ "message": "If the email exists, a reset link has been sent" }
POST /api/v1/auth/reset-password
Resets the user's password using a one-time token from the reset email.
Request Body
{
"token": "abc123...",
"new_password": "my_new_secure_password"
}
| Field | Type | Constraints |
|---|---|---|
token | string | One-time token from the reset email |
new_password | string | 8–128 characters |
Response 200 OK
{ "message": "Password reset successful" }
Error Cases
| Code | Cause |
|---|---|
| 400 | Invalid or expired token |
| 422 | Password does not meet requirements (8–128 chars) |
JWT Claims
The JWT is signed with HS256. The secret is configured via JWT_SECRET.
The default TTL is 24 hours (JWT_TTL_SECONDS=86400).
| Claim | Type | Description |
|---|---|---|
sub | UUID | User's internal identifier |
username | string | Username at time of issue |
role | string | User role at time of issue |
iat | Unix timestamp | Issued at |
exp | Unix timestamp | Expiration |
Categories
Categories are classification tags that can be assigned to plugins (max 5 per plugin).
The endpoint returns only active categories ordered by display_order.
Categories are managed via database migrations, not the API.
GET /api/v1/categories
Returns all active categories ordered by display_order, then alphabetically.
Response 200 OK
[
{
"id": "c2000000-0000-0000-0000-000000000009",
"name": "Adventure",
"slug": "adventure",
"description": "Quests, dungeons, RPG mechanics and narrative experiences.",
"icon": "compass",
"display_order": 1,
"created_at": "2026-03-06T12:00:00Z"
}
]
| Field | Type | Description |
|---|---|---|
id | UUID | Category identifier |
name | string | Display name |
slug | string | URL-safe slug for filtering |
description | string | null | Short description of the category |
icon | string | null | Lucide icon name (e.g. compass, swords, database) |
display_order | integer | Sort position in the UI (ascending) |
created_at | ISO 8601 | Creation timestamp |
Available Categories (17)
| Slug | Display Name | Icon |
|---|---|---|
adventure | Adventure | compass |
chat | Chat | message-square |
decoration | Decoration | palette |
economy | Economy | coins |
equipment | Equipment | swords |
game-mechanics | Game Mechanics | gamepad-2 |
library | Library | code |
management | Management | shield |
minigame | Minigame | trophy |
mobs | Mobs | bug |
optimization | Optimization | zap |
security | Security | lock |
social | Social | users |
storage | Storage | database |
transportation | Transportation | arrow-right-left |
utility | Utility | wrench |
world-generation | World Generation | globe |
Plugins
Plugin endpoints handle the full CRUD lifecycle for registry entries. Public endpoints (list, get) require no authentication. Write endpoints (create, update, delete) require a valid JWT and enforce ownership.
GET /api/v1/plugins
Returns a paginated list of active plugins with optional sorting and category filtering.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (≥ 1) |
per_page | integer | 20 | Items per page (1–100) |
sort | string | created_at | Sort field: created_at, updated_at, downloads_total, name |
order | string | desc | Sort order: asc or desc |
category | string | — | Filter by category slug |
Request
curl "http://localhost:8080/api/v1/plugins?page=1&per_page=10&sort=downloads_total&order=desc"
Response 200 OK
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"slug": "fast-chunks",
"name": "Fast Chunks",
"short_description": "Optimized chunk loading for Pumpkin MC",
"downloads_total": 1250,
"icon_url": "https://pub-xxx.r2.dev/plugins/fast-chunks/icon.png",
"author": {
"username": "rustdev",
"avatar_url": "https://avatars.githubusercontent.com/u/1"
},
"categories": [
{ "slug": "performance", "name": "Performance", "icon": "⚡" }
],
"created_at": "2026-03-06T12:00:00Z",
"updated_at": "2026-03-10T08:30:00Z"
}
],
"page": 1,
"per_page": 10,
"total": 42
}
| Field | Type | Description |
|---|---|---|
data | array | Array of plugin summaries |
data[].slug | string | URL-safe unique identifier |
data[].author | object | Author summary (username, avatar_url) |
data[].categories | array | Associated categories (slug, name, icon) |
page | integer | Current page number |
per_page | integer | Items per page |
total | integer | Total matching plugins |
POST /api/v1/plugins Protected
Creates a new plugin. Requires authentication. The authenticated user becomes the plugin author.
Request Body
{
"name": "Fast Chunks",
"short_description": "Optimized chunk loading for Pumpkin MC",
"description": "Detailed description with full feature list...",
"license": "MIT",
"repository_url": "https://github.com/rustdev/fast-chunks",
"website_url": "https://fast-chunks.dev",
"category_ids": [
"uuid-of-performance-category"
]
}
Validation Rules
| Field | Required | Constraints |
|---|---|---|
name | Yes | 3–100 characters, alphanumeric + space/hyphen/underscore |
short_description | Yes | Max 255 characters |
description | No | Max 50,000 characters |
license | No | Max 50 characters |
repository_url | No | Valid URL, max 500 characters |
website_url | No | Valid URL, max 500 characters |
category_ids | No | Array of UUIDs, max 5 categories |
Response 201 Created
Returns the full plugin object (see Get Plugin response format).
Error Cases
| Code | Cause |
|---|---|
| 401 | Missing or invalid authentication |
| 409 | Plugin name conflicts with existing slug |
| 422 | Validation failure (details in error message) |
fast-chunks-2).
GET /api/v1/plugins/:slug
Returns the full details of a single plugin by its slug.
Request
curl http://localhost:8080/api/v1/plugins/fast-chunks
Response 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"slug": "fast-chunks",
"name": "Fast Chunks",
"short_description": "Optimized chunk loading for Pumpkin MC",
"description": "Detailed description with full feature list...",
"license": "MIT",
"repository_url": "https://github.com/rustdev/fast-chunks",
"website_url": "https://fast-chunks.dev",
"downloads_total": 1250,
"icon_url": "https://pub-xxx.r2.dev/plugins/fast-chunks/icon.png",
"is_active": true,
"author": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"username": "rustdev",
"display_name": "Rust Developer",
"avatar_url": "https://avatars.githubusercontent.com/u/1"
},
"categories": [
{ "id": "cat-uuid", "slug": "performance", "name": "Performance", "icon": "⚡" }
],
"created_at": "2026-03-06T12:00:00Z",
"updated_at": "2026-03-10T08:30:00Z"
}
Error Cases
| Code | Cause |
|---|---|
| 404 | Plugin not found or inactive |
PUT /api/v1/plugins/:slug Protected
Updates plugin metadata. Requires authentication and ownership (only the plugin author can update).
Request Body
All fields are optional. Only provided fields are updated (partial update via COALESCE).
{
"name": "Fast Chunks Pro",
"short_description": "Updated description",
"license": "Apache-2.0",
"category_ids": ["uuid-1", "uuid-2"]
}
Validation Rules
Same constraints as Create Plugin, but all fields are optional.
Response 200 OK
Returns the updated full plugin object (see Get Plugin response format).
Error Cases
| Code | Cause |
|---|---|
| 401 | Missing or invalid authentication |
| 403 | Authenticated user is not the plugin author |
| 404 | Plugin not found |
| 409 | Updated name conflicts with existing slug |
| 422 | Validation failure |
DELETE /api/v1/plugins/:slug Protected
Soft-deletes a plugin by setting is_active = false. The plugin data is preserved in the database but no longer appears in listings or detail queries. Requires authentication and ownership.
Request
curl -X DELETE http://localhost:8080/api/v1/plugins/fast-chunks \
-H "Authorization: Bearer <jwt>"
Response 200 OK
{ "message": "Plugin deleted" }
Error Cases
| Code | Cause |
|---|---|
| 401 | Missing or invalid authentication |
| 403 | Authenticated user is not the plugin author |
| 404 | Plugin not found or already deleted |
Versions
Endpoints for managing plugin versions. Supports semver validation, Pumpkin compatibility ranges, changelogs, and download counting.
GET /api/v1/plugins/:slug/versions
Returns all published versions for a plugin, ordered by publication date (newest first). Includes Pumpkin compatibility range and yanked status.
Request
curl http://localhost:8080/api/v1/plugins/pumpkin-guard/versions
Response 200 OK
{
"plugin_slug": "pumpkin-guard",
"total": 3,
"versions": [
{
"id": "uuid",
"version": "0.3.0",
"changelog": "- Configurable thresholds via TOML\n- False-positive reduction",
"pumpkin_version_min": "0.2.0",
"pumpkin_version_max": "0.3.x",
"downloads": 3150,
"is_yanked": false,
"published_at": "2025-11-01T09:00:00Z"
}
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
plugin_slug | string | Slug of the parent plugin |
total | number | Total number of versions |
versions[].version | string | Semver version string |
versions[].changelog | string? | Release notes |
versions[].pumpkin_version_min | string? | Minimum compatible Pumpkin version |
versions[].pumpkin_version_max | string? | Maximum compatible Pumpkin version |
versions[].downloads | number | Download count for this version |
versions[].is_yanked | boolean | Whether the version has been yanked |
versions[].published_at | string | ISO 8601 publication timestamp |
Error Cases
| Code | Cause |
|---|---|
| 404 | Plugin not found or inactive |
POST /api/v1/plugins/:slug/versions
Publish a new version for a plugin. Requires authentication and plugin ownership. Version strings are validated as strict semver.
Request
curl -X POST http://localhost:8080/api/v1/plugins/pumpkin-guard/versions \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"version": "1.0.0",
"changelog": "## Initial Release\n- Core anti-cheat detection\n- Configurable thresholds",
"pumpkin_version_min": "0.1.0",
"pumpkin_version_max": "1.0.0"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
version | string | Yes | Strict semver version (e.g. 1.0.0, 2.1.0-beta.1) |
changelog | string? | No | Release notes, Markdown supported (max 50 000 chars) |
pumpkin_version_min | string? | No | Minimum compatible Pumpkin version (semver) |
pumpkin_version_max | string? | No | Maximum compatible Pumpkin version (semver, must be ≥ min) |
Response 201 Created
{
"id": "uuid",
"version": "1.0.0",
"changelog": "## Initial Release\n- Core anti-cheat detection\n- Configurable thresholds",
"pumpkin_version_min": "0.1.0",
"pumpkin_version_max": "1.0.0",
"downloads": 0,
"is_yanked": false,
"published_at": "2025-12-15T10:30:00Z"
}
Error Cases
| Code | Cause |
|---|---|
| 401 | Missing or invalid authentication token |
| 403 | User is not the plugin author |
| 404 | Plugin not found |
| 409 | Version already exists for this plugin |
| 422 | Validation error (invalid semver, range inconsistency, etc.) |
GET /api/v1/plugins/:slug/versions/:version
Fetch a single version by its semver string. Atomically increments the download counter for both the version and the parent plugin on each request.
Request
curl http://localhost:8080/api/v1/plugins/pumpkin-guard/versions/1.0.0
Response 200 OK
{
"id": "uuid",
"version": "1.0.0",
"changelog": "## Initial Release\n- Core anti-cheat detection",
"pumpkin_version_min": "0.1.0",
"pumpkin_version_max": "1.0.0",
"downloads": 1,
"is_yanked": false,
"published_at": "2025-12-15T10:30:00Z"
}
Error Cases
| Code | Cause |
|---|---|
| 404 | Plugin or version not found |
PATCH /api/v1/plugins/:slug/versions/:version/yank
Yank or restore a version. Yanked versions remain listed but are flagged as unavailable for new installations. Requires authentication and plugin ownership.
Request
curl -X PATCH http://localhost:8080/api/v1/plugins/pumpkin-guard/versions/1.0.0/yank \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "yanked": true }'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
yanked | boolean | Yes | true to yank, false to restore |
Response 200 OK
{
"id": "uuid",
"version": "1.0.0",
"changelog": "...",
"pumpkin_version_min": "0.1.0",
"pumpkin_version_max": "1.0.0",
"downloads": 42,
"is_yanked": true,
"published_at": "2025-12-15T10:30:00Z"
}
Error Cases
| Code | Cause |
|---|---|
| 401 | Missing or invalid authentication token |
| 403 | User is not the plugin author |
| 404 | Plugin or version not found |
Binaries
Binary distribution endpoints allow uploading compiled plugin artifacts per platform and version. Files are stored on S3-compatible storage (MinIO in development, Cloudflare R2 in production) with automatic SHA-256 checksum generation. Downloads use pre-signed URLs for direct client-to-storage transfer.
plugins/{slug}/{version}/{platform}/{filename}.
Each (version, platform) pair allows at most one binary.
GET /api/v1/plugins/:slug/versions/:version/binaries
Returns all binaries uploaded for a given plugin version.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
slug | string | Plugin slug |
version | string | Semver version string |
Response 200 OK
{
"binaries": [
{
"id": "uuid",
"version_id": "uuid",
"platform": "windows",
"file_name": "my-plugin.dll",
"file_size": 2048576,
"checksum_sha256": "a1b2c3d4…",
"storage_key": "plugins/my-plugin/1.0.0/windows/my-plugin.dll",
"content_type": "application/octet-stream",
"uploaded_at": "2025-01-15T10:30:00Z"
}
]
}
POST /api/v1/plugins/:slug/versions/:version/binaries
Upload a compiled binary for a specific version and platform. Requires authentication and plugin ownership. The file is validated against MIME type allowlist and a configurable size limit (default 100 MB).
Request
Multipart form with two fields:
| Field | Type | Description |
|---|---|---|
platform | text | windows, macos or linux |
file | file | Binary file (max 100 MB by default) |
Response 201 Created
{
"binary": {
"id": "uuid",
"version_id": "uuid",
"platform": "windows",
"file_name": "my-plugin.dll",
"file_size": 2048576,
"checksum_sha256": "a1b2c3d4…",
"storage_key": "plugins/my-plugin/1.0.0/windows/my-plugin.dll",
"content_type": "application/octet-stream",
"uploaded_at": "2025-01-15T10:30:00Z"
},
"download_url": "https://…presigned-url…",
"expires_in_seconds": 3600
}
Error Responses
| Status | Condition |
|---|---|
| 401 | Missing or invalid authentication |
| 403 | Not the plugin author |
| 404 | Plugin or version not found |
| 409 | Binary already exists for this (version, platform) pair |
| 422 | Invalid platform, MIME type, file name or file too large |
GET /api/v1/plugins/:slug/versions/:version/download?platform=windows
Returns a pre-signed download URL for the binary matching the requested platform. Also increments the download counters on both the version and the plugin.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | windows, macos or linux |
Response 200 OK
{
"download_url": "https://…presigned-url…",
"checksum_sha256": "a1b2c3d4…",
"file_name": "my-plugin.dll",
"file_size": 2048576,
"platform": "windows",
"expires_in_seconds": 3600
}
Error Responses
| Status | Condition |
|---|---|
| 404 | Plugin, version, or binary for the requested platform not found |
| 422 | Invalid or missing platform parameter |
Dependencies
Endpoints for managing inter-plugin dependency declarations, resolving the full dependency graph and detecting conflicts.
GET /api/v1/plugins/:slug/versions/:version/dependencies
Returns all declared dependencies for a specific version of a plugin.
curl http://localhost:8080/api/v1/plugins/pumpkin-guard/versions/0.3.0/dependencies
Response 200
{
"plugin_slug": "pumpkin-guard",
"version": "0.3.0",
"total": 1,
"dependencies": [
{
"id": "e5000000-...",
"dependency_plugin": {
"id": "b3000000-...",
"name": "PumpkinCore",
"slug": "pumpkin-core"
},
"version_req": ">=0.1.0",
"is_optional": false,
"created_at": "2025-06-15T10:00:00Z"
}
]
}
POST /api/v1/plugins/:slug/versions/:version/dependencies
Declare a new dependency for a version. Requires authentication as the plugin owner. Validates semver range syntax, prevents self-dependencies and circular dependencies.
curl -X POST http://localhost:8080/api/v1/plugins/pumpkin-guard/versions/0.3.0/dependencies \
-H "Content-Type: application/json" \
-d '{"dependency_plugin_id": "b3000000-...", "version_req": ">=0.2.0", "is_optional": false}'
| Field | Type | Description |
|---|---|---|
dependency_plugin_id | UUID | ID of the plugin to depend on |
version_req | string | Semver requirement (e.g. >=0.2.0, ^1.0, ~0.3) |
is_optional | boolean | Whether the dependency is optional (default: false) |
| Status | Description |
|---|---|
| 201 | Dependency created |
| 400 | Invalid semver range, self-dependency, or circular dependency |
| 401 | Unauthenticated |
| 403 | Not the plugin owner |
| 404 | Plugin, version, or dependency plugin not found |
| 409 | Dependency already declared |
PUT /api/v1/plugins/:slug/versions/:version/dependencies/:dependency_id
Update a dependency's version requirement or optional flag. Requires authentication as the plugin owner.
| Field | Type | Description |
|---|---|---|
version_req | string? | New semver requirement |
is_optional | boolean? | New optional flag |
DELETE /api/v1/plugins/:slug/versions/:version/dependencies/:dependency_id
Remove a dependency declaration. Requires authentication as the plugin owner.
GET /api/v1/plugins/:slug/versions/:version/dependencies/graph
Resolves the complete dependency graph via BFS traversal. For each dependency, finds the best matching version using semver compatibility. Returns the full graph and any detected conflicts.
curl http://localhost:8080/api/v1/plugins/economine/versions/1.1.0/dependencies/graph
Response 200
{
"plugin_slug": "economine",
"version": "1.1.0",
"graph": [
{
"plugin_id": "b3000000-...",
"plugin_name": "EconoMine",
"plugin_slug": "economine",
"version": "1.1.0",
"version_id": "d4000000-...",
"dependencies": [
{
"dependency_plugin_id": "b3000000-...",
"dependency_plugin_name": "PumpkinCore",
"dependency_plugin_slug": "pumpkin-core",
"version_req": ">=0.2.0",
"is_optional": false,
"resolved_version": "0.2.0",
"is_compatible": true
}
]
}
],
"conflicts": []
}
Conflict types:
| Type | Description |
|---|---|
no_matching_version | No published version satisfies the semver requirement |
incompatible_ranges | Multiple paths require incompatible version ranges of the same plugin |
circular_dependency | A cycle is detected in the dependency chain |
inactive_plugin | A required plugin has been deactivated or deleted |
GET /api/v1/plugins/:slug/dependants
Returns all plugins (and their versions) that declare a dependency on this plugin. Useful for impact analysis before breaking changes.
curl http://localhost:8080/api/v1/plugins/pumpkin-core/dependants
Response 200
{
"plugin_slug": "pumpkin-core",
"plugin_name": "PumpkinCore",
"total": 4,
"dependants": [
{
"plugin_id": "b3000000-...",
"plugin_name": "PumpkinGuard",
"plugin_slug": "pumpkin-guard",
"version": "0.3.0",
"version_req": ">=0.1.0",
"is_optional": false
}
]
}
Search
GET /api/v1/search
Full-text search across indexed plugins powered by Meilisearch.
| Parameter | Type | Description |
|---|---|---|
q | string | Search query (typo-tolerant) |
category | string | Filter by category slug |
platform | string | Filter by platform (windows, macos, linux) |
pumpkin_version | string | Filter by Pumpkin MC version |
sort | string | Sort: relevance, downloads, newest, oldest, updated, name_asc, name_desc |
page | int | Page number (default: 1) |
per_page | int | Results per page (1-100, default: 20) |
Response includes hits, estimated_total_hits, processing_time_ms, and facet_distribution (counts per category, platform, Pumpkin version).
GET /api/v1/search/suggest
Autocomplete suggestions for partial search queries.
| Parameter | Type | Description |
|---|---|---|
q | string | Partial search query (required) |
limit | int | Max suggestions (default: 5, max: 10) |
GET /api/v1/pumpkin-versions
Returns all known Pumpkin MC release versions, fetched and cached from the official Pumpkin GitHub repository.
Dashboard Analytics
Endpoints providing download statistics and KPIs for authenticated plugin authors. All endpoints require authentication via cookie or Bearer token.
GET /api/v1/dashboard/stats Auth
Returns aggregated KPIs for the authenticated author: total plugins, total downloads, 7-day and 30-day download windows, trend percentage, top plugin, and recent download chart data.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
granularity | string | weekly | Chart granularity: daily, weekly, monthly |
periods | integer | 12 | Number of chart periods (1–52) |
Request
curl -b cookies.txt http://localhost:8080/api/v1/dashboard/stats?granularity=weekly&periods=12
Response 200 OK
{
"total_plugins": 5,
"total_downloads": 12450,
"downloads_last_30_days": 1230,
"downloads_last_7_days": 340,
"downloads_trend_percent": 15.3,
"most_downloaded_plugin": {
"name": "Pumpkin Voice",
"slug": "pumpkin-voice",
"downloads_total": 8200
},
"recent_downloads": [
{ "period": "2026-W01", "downloads": 280 },
{ "period": "2026-W02", "downloads": 340 }
]
}
GET /api/v1/dashboard/downloads Auth
Returns download chart data aggregated across all of the author's plugins.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
granularity | string | weekly | Chart granularity: daily, weekly, monthly |
periods | integer | 12 | Number of chart periods (1–52) |
Response 200 OK
[
{ "period": "2026-W01", "downloads": 280 },
{ "period": "2026-W02", "downloads": 340 }
]
GET /api/v1/plugins/:slug/download-stats
Returns detailed download statistics for a specific plugin including a time-series chart and per-version breakdown. No authentication required.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
granularity | string | weekly | Chart granularity: daily, weekly, monthly |
periods | integer | 12 | Number of chart periods (1–52) |
Request
curl http://localhost:8080/api/v1/plugins/pumpkin-voice/download-stats?granularity=monthly&periods=6
Response 200 OK
{
"plugin_slug": "pumpkin-voice",
"total_downloads": 8200,
"downloads_last_30_days": 650,
"downloads_last_7_days": 180,
"downloads_trend_percent": 12.5,
"chart": [
{ "period": "2026-01", "downloads": 1200 },
{ "period": "2026-02", "downloads": 1350 }
],
"by_version": [
{ "version": "0.3.0", "downloads": 4500, "published_at": "2025-12-01T00:00:00Z" },
{ "version": "0.2.1", "downloads": 3700, "published_at": "2025-10-15T00:00:00Z" }
]
}
API Keys
Manage personal API keys for programmatic access and CI/CD integration. Keys are stored as SHA-256 hashes.
The full key is only returned once at creation. Maximum 10 keys per user.
Key format: phub_ prefix + 8 hex characters (visible prefix) + random secret.
Permission Enforcement
Each API key carries a set of scoped permissions that restrict which endpoints it can access. JWT sessions (cookie / bearer token) always have full access.
| Permission | Allows |
|---|---|
publish | Create / update / delete plugins, create / yank versions |
upload | Upload binaries |
read | Dashboard analytics, notifications |
Admin and moderation endpoints are never accessible via API key — a JWT session is required.
If a key lacks the required permission, a 403 Forbidden response is returned.
Per-Key Rate Limiting
Each API key has its own rate-limit quota (rate_limit_per_second and rate_limit_burst_size),
configured at creation time. When a request uses the X-API-Key header, the global per-IP limiter is
replaced by the key's individual quotas. Default: 1 token / second with a burst of 30.
Audit Trail
Every API-key-authenticated request is automatically logged to api_key_usage_logs with:
api_key_id, action (e.g. plugin.create), resource (request path),
success (boolean), and created_at timestamp. Failed requests (rate limited, permission denied,
handler errors) are recorded as well.
GET /api/v1/api-keys Auth
Lists all API keys for the authenticated user. The key secret is never returned — only the key_prefix.
Response 200 OK
[
{
"id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"name": "CI Deploy Key",
"key_prefix": "phub_ab12cd34",
"permissions": ["publish", "upload"],
"last_used_at": "2026-03-15T14:30:00Z",
"expires_at": "2027-03-15T00:00:00Z",
"rate_limit_per_second": 1,
"rate_limit_burst_size": 30,
"created_at": "2026-03-01T10:00:00Z"
}
]
POST /api/v1/api-keys Auth
Creates a new API key. The full key is returned only in this response — store it securely.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Key label (max 64 characters) |
permissions | string[] | Yes | Scoped permissions: publish, upload, read |
expires_at | string (ISO 8601) | No | Expiration date (must be in the future) |
rate_limit_per_second | integer | No | Token replenish interval in seconds (default 1, min 1) |
rate_limit_burst_size | integer | No | Burst capacity (default 30, max 500) |
Request
curl -X POST -b cookies.txt http://localhost:8080/api/v1/api-keys \
-H "Content-Type: application/json" \
-d '{
"name": "CI Deploy Key",
"permissions": ["publish", "upload"],
"expires_at": "2027-03-15T00:00:00Z",
"rate_limit_per_second": 1,
"rate_limit_burst_size": 60
}'
Response 201 Created
{
"id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"name": "CI Deploy Key",
"key": "phub_ab12cd34_7f8e9d0c1b2a3f4e5d6c7b8a9f0e1d2c",
"key_prefix": "phub_ab12cd34",
"permissions": ["publish", "upload"],
"expires_at": "2027-03-15T00:00:00Z",
"rate_limit_per_second": 1,
"rate_limit_burst_size": 60,
"created_at": "2026-03-01T10:00:00Z"
}
Error Cases
| Code | Cause |
|---|---|
| 422 | Name exceeds 64 chars, invalid permission, expires_at in the past, or invalid rate limit values |
| 409 | Maximum 10 keys per user reached |
DELETE /api/v1/api-keys/:id Auth
Permanently revokes (deletes) an API key. Only the key owner can revoke their own keys.
Request
curl -X DELETE -b cookies.txt http://localhost:8080/api/v1/api-keys/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
Response 200 OK
{ "deleted": true }
Error Cases
| Code | Cause |
|---|---|
| 404 | Key not found or does not belong to the authenticated user |
Notifications
In-app notification system for plugin authors. Notifications are automatically created when a plugin reaches download milestones (10, 50, 100, 500, 1K, 5K, 10K, 50K, 100K downloads). All endpoints require authentication.
GET /api/v1/notifications Auth
Returns a paginated list of notifications for the authenticated user.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
per_page | integer | 20 | Items per page (max 50) |
unread_only | boolean | false | Filter to unread notifications only |
Response 200 OK
{
"notifications": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"kind": "download_milestone",
"title": "Pumpkin Voice reached 1,000 downloads!",
"body": "Your plugin Pumpkin Voice has reached 1,000 total downloads. Congratulations!",
"link": "/plugins/pumpkin-voice",
"is_read": false,
"created_at": "2026-03-15T14:00:00Z"
}
],
"total": 12,
"unread": 3
}
GET /api/v1/notifications/unread-count Auth
Returns the number of unread notifications. Used by the notification bell badge in the UI (auto-refreshes every 30 seconds).
Response 200 OK
{ "count": 3 }
PATCH /api/v1/notifications/:id/read Auth
Marks a single notification as read. Only the notification owner can mark it.
Response 200 OK
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"kind": "download_milestone",
"title": "Pumpkin Voice reached 1,000 downloads!",
"body": "Your plugin Pumpkin Voice has reached 1,000 total downloads. Congratulations!",
"link": "/plugins/pumpkin-voice",
"is_read": true,
"created_at": "2026-03-15T14:00:00Z"
}
Error Cases
| Code | Cause |
|---|---|
| 404 | Notification not found or does not belong to the authenticated user |
POST /api/v1/notifications/read-all Auth
Marks all unread notifications as read for the authenticated user.
Response 200 OK
{ "marked_read": 3 }
Users (Public Profiles)
Public endpoints exposing author profiles and their published plugins. No authentication required.
GET /api/v1/users/:username
Returns the public profile of an author including aggregate statistics.
Request
curl http://localhost:8080/api/v1/users/rustdev
Response 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "rustdev",
"display_name": "Rust Developer",
"avatar_url": "/api/v1/users/550e8400-…/avatar",
"bio": "Minecraft plugin developer",
"role": "author",
"plugin_count": 3,
"total_downloads": 4250,
"created_at": "2026-03-06T12:00:00Z"
}
| Field | Type | Description |
|---|---|---|
id | UUID | User identifier |
username | string | Unique username |
display_name | string | null | Display name |
avatar_url | string | null | Avatar URL |
bio | string | null | User biography |
role | string | author, moderator or admin |
plugin_count | integer | Number of active plugins published by this author |
total_downloads | integer | Cumulative downloads across all the author's plugins |
created_at | ISO 8601 | Registration date |
Error Cases
| Code | Cause |
|---|---|
| 404 | User not found |
GET /api/v1/users/:username/plugins
Returns a paginated list of plugins published by the specified author. Only active plugins are returned.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (≥ 1) |
per_page | integer | 20 | Items per page (1–100) |
Response 200 OK
Returns a paginated list of PluginSummary objects (same format as List Plugins).
Error Cases
| Code | Cause |
|---|---|
| 404 | User not found |
Admin & Moderation
Administration endpoints for platform moderation. All routes require authentication and appropriate role permissions. Two middleware layers enforce access control:
- Staff (
adminormoderator) — required for plugin moderation and audit log viewing - Admin only — required for user management (role changes, account deactivation)
audit_logs table with the actor, action,
target, and details (JSONB).
GET /api/v1/admin/stats Staff
Returns a summary of platform statistics for the admin dashboard.
Response 200 OK
{
"user_count": 42,
"plugin_count": 15,
"active_plugin_count": 12,
"recent_audit_logs": [ … ]
}
Plugin Moderation
GET /api/v1/admin/plugins Staff
Lists all plugins (active and deactivated) with optional search filtering.
| Parameter | Type | Description |
|---|---|---|
search | string | Filter plugins by name (optional) |
page | integer | Page number (default: 1) |
POST /api/v1/admin/plugins/:plugin_id/deactivate Staff
Deactivates a plugin (soft-ban). The plugin becomes invisible in public listings and search.
Response 200 OK
{ "message": "Plugin deactivated" }
POST /api/v1/admin/plugins/:plugin_id/reactivate Staff
Reactivates a previously deactivated plugin.
Response 200 OK
{ "message": "Plugin reactivated" }
User Management
GET /api/v1/admin/users Admin
Lists all users with plugin counts and optional search filtering.
| Parameter | Type | Description |
|---|---|---|
search | string | Filter users by username or email (optional) |
page | integer | Page number (default: 1) |
PATCH /api/v1/admin/users/:user_id/role Admin
Changes a user's platform role. Admins cannot demote themselves.
Request Body
{
"role": "moderator"
}
| Field | Type | Allowed Values |
|---|---|---|
role | string | author, moderator, admin |
Error Cases
| Code | Cause |
|---|---|
| 400 | Cannot change own role (self-demotion prevention) |
| 422 | Invalid role value |
POST /api/v1/admin/users/:user_id/deactivate Admin
Deactivates a user account. Prevents the user from logging in. Admins cannot deactivate themselves.
Response 200 OK
{ "message": "User deactivated" }
POST /api/v1/admin/users/:user_id/reactivate Admin
Reactivates a previously deactivated user account.
Response 200 OK
{ "message": "User reactivated" }
GET /api/v1/admin/audit-logs Staff
Returns a paginated list of admin actions for compliance and review.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
Response 200 OK
{
"data": [
{
"id": "uuid",
"actor_username": "admin_user",
"action": "plugin.deactivate",
"target_type": "plugin",
"target_id": "uuid",
"details": { "plugin_name": "Bad Plugin", "reason": "policy violation" },
"created_at": "2026-03-10T09:15:00Z"
}
],
"page": 1,
"total": 24
}
Recorded Actions
| Action | Target | Details |
|---|---|---|
plugin.deactivate | plugin | Plugin name |
plugin.reactivate | plugin | Plugin name |
user.role_change | user | Old role → new role |
user.deactivate | user | Username |
user.reactivate | user | Username |
Reviews
Community review and rating system. Each user may submit one review per plugin (star rating
1–5, optional title and body). Authors cannot review their own plugin. Hidden reviews are
excluded from average_rating and review_count.
Both fields are included in PluginSummary, PluginResponse, and
Meilisearch search results. The plugin document is automatically reindexed after every
review mutation.
•
average_rating: number — weighted average (0.0 when no reviews)•
review_count: number — count of visible (non-hidden) reviewsThe
/search endpoint also supports sort=rating (descending) and sort=rating_asc.
GET /api/v1/plugins/:slug/reviews
Returns a paginated list of visible reviews for a plugin, with rating distribution and computed average.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (1-based) |
per_page | integer | 10 | Items per page (1–100) |
Response 200 OK
{
"reviews": [
{
"id": "uuid",
"plugin_id": "uuid",
"author": {
"id": "uuid",
"username": "rustcraftdev",
"avatar_url": "https://..."
},
"rating": 5,
"title": "Excellent plugin",
"body": "Works perfectly on 0.3.x.",
"is_hidden": false,
"created_at": "2026-03-10T10:00:00Z",
"updated_at": "2026-03-10T10:00:00Z"
}
],
"total": 1,
"average_rating": 5.0,
"rating_distribution": {
"star_1": 0,
"star_2": 0,
"star_3": 0,
"star_4": 0,
"star_5": 1
}
}
POST /api/v1/plugins/:slug/reviews Protected — write
Submits a new review. One review per user per plugin. Plugin authors cannot review their own plugin.
Request Body
{
"rating": 5, // required: integer 1–5
"title": "...", // optional: max 200 chars
"body": "..." // optional: max 5000 chars
}
| Field | Type | Constraints |
|---|---|---|
rating | integer | Required. 1–5 inclusive. |
title | string | null | Optional. Max 200 characters. |
body | string | null | Optional. Max 5 000 characters. |
Response 201 Created
Returns the created ReviewResponse object.
Error Cases
| Code | Cause |
|---|---|
| 401 | Not authenticated |
| 403 | Missing write permission (API key) |
| 404 | Plugin not found |
| 409 | User has already reviewed this plugin |
| 422 | Plugin author attempted to review their own plugin, or validation failure |
PUT /api/v1/plugins/:slug/reviews/:review_id Protected — owner only
Updates the authenticated user's own review. All fields are optional (patch semantics via COALESCE).
Request Body
{
"rating": 4, // optional
"title": "Updated", // optional
"body": "..." // optional
}
Response 200 OK
Returns the updated ReviewResponse object.
Error Cases
| Code | Cause |
|---|---|
| 401 | Not authenticated |
| 403 | Review does not belong to the authenticated user |
| 404 | Plugin or review not found |
DELETE /api/v1/plugins/:slug/reviews/:review_id Protected
Deletes a review. Allowed for: the review author, the plugin author, admin, and moderator roles.
Response 204 No Content
Error Cases
| Code | Cause |
|---|---|
| 401 | Not authenticated |
| 403 | Not authorized (not owner, not plugin author, not staff) |
| 404 | Plugin or review not found |
PATCH /api/v1/plugins/:slug/reviews/:review_id/hide Protected — author or staff
Toggles the visibility of a review. Hidden reviews are excluded from the displayed list,
average_rating, and review_count. The plugin is reindexed in
Meilisearch after the change.
Request Body
{ "hidden": true }
Response 200 OK
{ "hidden": true }
Error Cases
| Code | Cause |
|---|---|
| 403 | Not the plugin author or staff |
| 404 | Plugin or review not found |
POST /api/v1/plugins/:slug/reviews/:review_id/report Protected — write
Reports a review for abuse. Each user can report a given review only once.
Request Body
{
"reason": "spam", // required: see reason values below
"details": "..." // optional: max 1000 chars
}
| Reason | Description |
|---|---|
spam | Spam or promotional content |
harassment | Harassment or targeted abuse |
misinformation | False or misleading content |
inappropriate | Inappropriate or offensive language |
other | Other reason (use details to elaborate) |
Response 201 Created
Returns the created report object.
Error Cases
| Code | Cause |
|---|---|
| 404 | Review not found |
| 409 | User has already reported this review |
GET /api/v1/admin/review-reports Staff
Returns a paginated list of review abuse reports for staff review.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
per_page | integer | 20 | Items per page |
status | string | pending | Filter by status: pending, resolved, or dismissed |
Response 200 OK
{
"reports": [
{
"id": "uuid",
"review_id": "uuid",
"reporter_id": "uuid",
"reason": "spam",
"details": null,
"status": "pending",
"resolved_by": null,
"resolved_at": null,
"created_at": "2026-03-10T10:00:00Z"
}
],
"total": 3,
"page": 1
}
PATCH /api/v1/admin/review-reports/:report_id/resolve Staff
Resolves or dismisses a review abuse report. The resolved_by field is set to the acting staff member's user ID.
Request Body
{
"status": "resolved" // "resolved" or "dismissed"
}
Response 200 OK
Returns the updated report object.
Error Cases
| Code | Cause |
|---|---|
| 404 | Report not found |
| 422 | Invalid status value |
Media Gallery
Each plugin can have up to 10 media items (images or videos) displayed in a lightbox gallery. Images must be JPEG, PNG, or WebP and cannot exceed 10 MB. Videos must be MP4 or WebM and cannot exceed 100 MB. Files are stored in MinIO (S3-compatible) and served via pre-signed URLs. Write endpoints require plugin ownership.
GET /api/v1/plugins/{slug}/media
Returns all media items for a plugin, ordered by position.
Response 200 OK
[
{
"id": "m1000000-0000-0000-0000-000000000001",
"plugin_id": "550e8400-e29b-41d4-a716-446655440000",
"media_type": "image",
"original_filename": "screenshot.png",
"file_size": 204800,
"mime_type": "image/png",
"url": "https://storage.../media/screenshot.png?X-Amz-...",
"caption": "Main gameplay overview",
"position": 0,
"created_at": "2026-05-01T10:00:00Z"
}
]
| Field | Type | Description |
|---|---|---|
id | UUID | Media item identifier |
plugin_id | UUID | Parent plugin identifier |
media_type | string | image or video |
original_filename | string | Original upload filename |
file_size | integer | File size in bytes |
mime_type | string | MIME type (e.g. image/png, video/mp4) |
url | string | Pre-signed S3 URL (expires in 1 hour) |
caption | string | null | Optional caption text |
position | integer | Display order (0-indexed) |
created_at | ISO 8601 | Upload timestamp |
POST /api/v1/plugins/{slug}/media Auth
Uploads a new media item. The request must use multipart/form-data.
Form Fields
| Field | Required | Description |
|---|---|---|
file | Yes | Binary file payload. Accepted: JPEG, PNG, WebP (≤ 10 MB), MP4, WebM (≤ 100 MB) |
caption | No | Optional caption text (max 200 chars) |
Response 201 Created
Returns the created media item object.
Error Cases
| Code | Cause |
|---|---|
| 400 | Unsupported file type or file exceeds size limit |
| 403 | Not the plugin owner |
| 409 | Plugin already has 10 media items |
PATCH /api/v1/plugins/{slug}/media/{media_id} Auth
Updates the caption of an existing media item.
Request Body
{
"caption": "Updated caption text"
}
Response 200 OK
Returns the updated media item object.
DELETE /api/v1/plugins/{slug}/media/{media_id} Auth
Permanently deletes a media item and its associated file from object storage.
Response 204 No Content
Error Cases
| Code | Cause |
|---|---|
| 403 | Not the plugin owner |
| 404 | Media item not found |
PUT /api/v1/plugins/{slug}/media/reorder Auth
Updates the display order of all media items for a plugin in a single atomic operation.
Request Body
{
"order": [
"m1000000-0000-0000-0000-000000000003",
"m1000000-0000-0000-0000-000000000001",
"m1000000-0000-0000-0000-000000000002"
]
}
The array must contain all media item IDs for the plugin. Items receive positions 0, 1, 2… in the order given.
Response 200 OK
Returns the full reordered list of media item objects.
Error Cases
| Code | Cause |
|---|---|
| 400 | Order array does not contain all media IDs, or contains unknown IDs |
| 403 | Not the plugin owner |
Changelog
Each plugin can have a single changelog document written in
Keep a Changelog-style Markdown.
The record stores the raw Markdown content and an optional source flag indicating whether the
changelog was written manually (manual) or synced from a GitHub release (github).
Write endpoints require plugin ownership.
GET /api/v1/plugins/{slug}/changelog
Returns the changelog for a plugin, if one exists.
Response 200 OK
{
"id": "ch000000-0000-0000-0000-000000000001",
"plugin_id": "550e8400-e29b-41d4-a716-446655440000",
"content": "## [1.2.0] - 2026-04-20\n### Added\n- Async chunk streaming\n### Fixed\n- Memory leak on reload",
"source": "manual",
"created_at": "2026-04-20T14:00:00Z",
"updated_at": "2026-04-25T09:30:00Z"
}
| Field | Type | Description |
|---|---|---|
id | UUID | Changelog record identifier |
plugin_id | UUID | Parent plugin identifier |
content | string | Raw Markdown changelog content (sanitized server-side) |
source | string | manual (inline editor) or github (synced from GitHub release) |
created_at | ISO 8601 | Creation timestamp |
updated_at | ISO 8601 | Last update timestamp |
Error Cases
| Code | Cause |
|---|---|
| 404 | Plugin or changelog not found |
PUT /api/v1/plugins/{slug}/changelog Auth
Creates or replaces the plugin changelog (UPSERT). The content is sanitized
server-side to prevent XSS before being persisted.
Request Body
{
"content": "## [1.3.0] - 2026-05-10\n### Added\n- New biome support\n### Changed\n- Performance improvements",
"source": "manual"
}
| Field | Required | Description |
|---|---|---|
content | Yes | Markdown changelog content (max 64 KB) |
source | No | manual (default) or github |
Response 200 OK
Returns the full changelog object.
Error Cases
| Code | Cause |
|---|---|
| 400 | Content exceeds 64 KB or is empty |
| 403 | Not the plugin owner |
DELETE /api/v1/plugins/{slug}/changelog Auth
Permanently deletes the plugin changelog.
Response 204 No Content
Error Cases
| Code | Cause |
|---|---|
| 403 | Not the plugin owner |
| 404 | Plugin or changelog not found |