API Reference

Technical documentation for the Pumpkin Hub REST API (Axum / Rust).

Active development — Phase 3 In Progress
Implemented: multi-provider auth, Plugin CRUD, user profile management, avatar upload, creator dashboard, version management, binary storage & distribution (S3-compatible upload, SHA-256 checksums, pre-signed download URLs), full-text search (Meilisearch), dependency graph, rate limiting, email verification & password recovery, public author profiles, admin moderation & audit logs, download analytics & KPIs, API key management (permission enforcement, per-key rate limiting, usage audit trail), notification center, review system (star ratings, moderation, abuse reporting, Meilisearch sync), media gallery (image/video upload, lightbox, reordering), changelog tab (markdown rendering, inline editor, GitHub-sync source), plugin categories (17 categories, soft-deletion, display ordering).
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

HeaderDirectionDescription
Content-TypeRequest / Responseapplication/json
x-request-idResponseUnique UUID assigned to each request (traceability)
Content-EncodingResponsegzip if the client supports compression
AuthorizationRequestBearer <jwt> — alternative to the cookie for programmatic API clients
Retry-AfterResponseSeconds 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.

TierApplies ToBurstSustained 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"
}
FieldTypeDescription
statusstringService state: "ok" or "degraded"
servicestringService name
versionstringCrate version (from Cargo.toml)
databasestringDatabase 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
}
FieldTypeDescription
total_pluginsintegerTotal number of active plugins on the platform
total_authorsintegerTotal number of users who have published at least one plugin
total_downloadsintegerCumulative 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 StatusVariantMeaning
401UnauthorizedAuthentication required or invalid token
403ForbiddenAuthenticated but not authorized (e.g., not the resource owner)
404NotFoundResource not found
409ConflictResource conflict (e.g., duplicate slug or name)
422UnprocessableEntityInvalid data (details in message)
429TooManyRequestsRate limit exceeded — Retry-After header included
500InternalInternal error (details logged server-side, never exposed to client)
504TimeoutRequest exceeded the 30-second global timeout
Security
Internal errors (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).

ParameterValue
Allowed OriginsConfigurable via ALLOWED_ORIGINS
MethodsGET, POST, PUT, PATCH, DELETE
Allowed HeadersContent-Type, Authorization
CredentialsEnabled (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.

Account Linking
When an OAuth provider returns an email that already exists in the database, the provider is linked to the existing account. This allows users to sign in with multiple providers while maintaining a single identity.
CSRF Protection
A short-lived CSRF state is stored in an HttpOnly cookie during OAuth login and validated at callback. This prevents CSRF attacks on the OAuth flow.

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"
}
FieldTypeConstraints
usernamestring1–39 chars, alphanumeric + hyphens/underscores
emailstringValid email, max 255 chars, case-insensitive unique
passwordstring8–128 characters
Response 200 OK

Returns the UserProfile and sets the pumpkin_hub_token cookie.

Error Cases
CodeCause
409Username or email already taken
422Validation 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
CodeCause
401Invalid 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
ParameterTypeDescription
codestringAuthorization code from the provider
statestringCSRF state to validate against the cookie
Error Cases
CodeCause
401Missing CSRF cookie or state mismatch
422Provider not configured (Google/Discord)
500Token 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"
}
FieldTypeDescription
idUUIDInternal user identifier
usernamestringUnique username
display_namestring | nullUser's display name
emailstring | nullUser's email address
avatar_urlstring | nullAvatar URL — points to /api/v1/users/{id}/avatar after upload, or to OAuth provider URL on first login
biostring | nullUser bio
email_verifiedbooleanWhether the user’s email has been verified
is_activebooleanAccount active status (deactivated users cannot log in)
roleauthor | moderator | adminPlatform role
created_atISO 8601Registration 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).

StatusMeaning
200Profile updated, returns UserProfile
400No fields provided / validation error
401Not 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

FieldTypeConstraints
avatarfileRequired. 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.

StatusMeaning
200Avatar stored, returns updated UserProfile
400Missing field, unsupported MIME type, invalid magic bytes, or file > 2 MB
401Not 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.

StatusMeaning
200Binary image with Content-Type (e.g. image/png)
404User 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
CodeCause
400Invalid 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
CodeCause
400Email already verified
401Not 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"
}
FieldTypeConstraints
tokenstringOne-time token from the reset email
new_passwordstring8–128 characters
Response 200 OK
{ "message": "Password reset successful" }
Error Cases
CodeCause
400Invalid or expired token
422Password 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).

ClaimTypeDescription
subUUIDUser's internal identifier
usernamestringUsername at time of issue
rolestringUser role at time of issue
iatUnix timestampIssued at
expUnix timestampExpiration

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"
  }
]
FieldTypeDescription
idUUIDCategory identifier
namestringDisplay name
slugstringURL-safe slug for filtering
descriptionstring | nullShort description of the category
iconstring | nullLucide icon name (e.g. compass, swords, database)
display_orderintegerSort position in the UI (ascending)
created_atISO 8601Creation timestamp

Available Categories (17)

SlugDisplay NameIcon
adventureAdventurecompass
chatChatmessage-square
decorationDecorationpalette
economyEconomycoins
equipmentEquipmentswords
game-mechanicsGame Mechanicsgamepad-2
libraryLibrarycode
managementManagementshield
minigameMinigametrophy
mobsMobsbug
optimizationOptimizationzap
securitySecuritylock
socialSocialusers
storageStoragedatabase
transportationTransportationarrow-right-left
utilityUtilitywrench
world-generationWorld Generationglobe

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

ParameterTypeDefaultDescription
pageinteger1Page number (≥ 1)
per_pageinteger20Items per page (1–100)
sortstringcreated_atSort field: created_at, updated_at, downloads_total, name
orderstringdescSort order: asc or desc
categorystringFilter 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
}
FieldTypeDescription
dataarrayArray of plugin summaries
data[].slugstringURL-safe unique identifier
data[].authorobjectAuthor summary (username, avatar_url)
data[].categoriesarrayAssociated categories (slug, name, icon)
pageintegerCurrent page number
per_pageintegerItems per page
totalintegerTotal 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

FieldRequiredConstraints
nameYes3–100 characters, alphanumeric + space/hyphen/underscore
short_descriptionYesMax 255 characters
descriptionNoMax 50,000 characters
licenseNoMax 50 characters
repository_urlNoValid URL, max 500 characters
website_urlNoValid URL, max 500 characters
category_idsNoArray of UUIDs, max 5 categories

Response 201 Created

Returns the full plugin object (see Get Plugin response format).

Error Cases

CodeCause
401Missing or invalid authentication
409Plugin name conflicts with existing slug
422Validation failure (details in error message)
Slug Generation
The slug is automatically generated from the plugin name (lowercased, spaces/special characters replaced by hyphens). If a collision is detected, a numeric suffix is appended (e.g., 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

CodeCause
404Plugin 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

CodeCause
401Missing or invalid authentication
403Authenticated user is not the plugin author
404Plugin not found
409Updated name conflicts with existing slug
422Validation 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

CodeCause
401Missing or invalid authentication
403Authenticated user is not the plugin author
404Plugin 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

FieldTypeDescription
plugin_slugstringSlug of the parent plugin
totalnumberTotal number of versions
versions[].versionstringSemver version string
versions[].changelogstring?Release notes
versions[].pumpkin_version_minstring?Minimum compatible Pumpkin version
versions[].pumpkin_version_maxstring?Maximum compatible Pumpkin version
versions[].downloadsnumberDownload count for this version
versions[].is_yankedbooleanWhether the version has been yanked
versions[].published_atstringISO 8601 publication timestamp

Error Cases

CodeCause
404Plugin 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

FieldTypeRequiredDescription
versionstringYesStrict semver version (e.g. 1.0.0, 2.1.0-beta.1)
changelogstring?NoRelease notes, Markdown supported (max 50 000 chars)
pumpkin_version_minstring?NoMinimum compatible Pumpkin version (semver)
pumpkin_version_maxstring?NoMaximum 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

CodeCause
401Missing or invalid authentication token
403User is not the plugin author
404Plugin not found
409Version already exists for this plugin
422Validation 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

CodeCause
404Plugin 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

FieldTypeRequiredDescription
yankedbooleanYestrue 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

CodeCause
401Missing or invalid authentication token
403User is not the plugin author
404Plugin 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.

Storage
Binaries are stored on S3-compatible object storage. The key pattern is 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

ParameterTypeDescription
slugstringPlugin slug
versionstringSemver 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:

FieldTypeDescription
platformtextwindows, macos or linux
filefileBinary 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

StatusCondition
401Missing or invalid authentication
403Not the plugin author
404Plugin or version not found
409Binary already exists for this (version, platform) pair
422Invalid 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

ParameterTypeRequiredDescription
platformstringYeswindows, 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

StatusCondition
404Plugin, version, or binary for the requested platform not found
422Invalid 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}'
FieldTypeDescription
dependency_plugin_idUUIDID of the plugin to depend on
version_reqstringSemver requirement (e.g. >=0.2.0, ^1.0, ~0.3)
is_optionalbooleanWhether the dependency is optional (default: false)
StatusDescription
201Dependency created
400Invalid semver range, self-dependency, or circular dependency
401Unauthenticated
403Not the plugin owner
404Plugin, version, or dependency plugin not found
409Dependency 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.

FieldTypeDescription
version_reqstring?New semver requirement
is_optionalboolean?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:

TypeDescription
no_matching_versionNo published version satisfies the semver requirement
incompatible_rangesMultiple paths require incompatible version ranges of the same plugin
circular_dependencyA cycle is detected in the dependency chain
inactive_pluginA 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
    }
  ]
}

GET /api/v1/search

Full-text search across indexed plugins powered by Meilisearch.

ParameterTypeDescription
qstringSearch query (typo-tolerant)
categorystringFilter by category slug
platformstringFilter by platform (windows, macos, linux)
pumpkin_versionstringFilter by Pumpkin MC version
sortstringSort: relevance, downloads, newest, oldest, updated, name_asc, name_desc
pageintPage number (default: 1)
per_pageintResults 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.

ParameterTypeDescription
qstringPartial search query (required)
limitintMax 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

ParameterTypeDefaultDescription
granularitystringweeklyChart granularity: daily, weekly, monthly
periodsinteger12Number 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

ParameterTypeDefaultDescription
granularitystringweeklyChart granularity: daily, weekly, monthly
periodsinteger12Number 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

ParameterTypeDefaultDescription
granularitystringweeklyChart granularity: daily, weekly, monthly
periodsinteger12Number 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.

PermissionAllows
publishCreate / update / delete plugins, create / yank versions
uploadUpload binaries
readDashboard 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

FieldTypeRequiredDescription
namestringYesKey label (max 64 characters)
permissionsstring[]YesScoped permissions: publish, upload, read
expires_atstring (ISO 8601)NoExpiration date (must be in the future)
rate_limit_per_secondintegerNoToken replenish interval in seconds (default 1, min 1)
rate_limit_burst_sizeintegerNoBurst 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
CodeCause
422Name exceeds 64 chars, invalid permission, expires_at in the past, or invalid rate limit values
409Maximum 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
CodeCause
404Key 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

ParameterTypeDefaultDescription
pageinteger1Page number
per_pageinteger20Items per page (max 50)
unread_onlybooleanfalseFilter 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
CodeCause
404Notification 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"
}
FieldTypeDescription
idUUIDUser identifier
usernamestringUnique username
display_namestring | nullDisplay name
avatar_urlstring | nullAvatar URL
biostring | nullUser biography
rolestringauthor, moderator or admin
plugin_countintegerNumber of active plugins published by this author
total_downloadsintegerCumulative downloads across all the author's plugins
created_atISO 8601Registration date

Error Cases

CodeCause
404User 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

ParameterTypeDefaultDescription
pageinteger1Page number (≥ 1)
per_pageinteger20Items per page (1–100)

Response 200 OK

Returns a paginated list of PluginSummary objects (same format as List Plugins).

Error Cases

CodeCause
404User not found

Admin & Moderation

Administration endpoints for platform moderation. All routes require authentication and appropriate role permissions. Two middleware layers enforce access control:

Audit Trail
All admin actions (plugin deactivation, user role changes, account management) are automatically logged in the 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.

ParameterTypeDescription
searchstringFilter plugins by name (optional)
pageintegerPage 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.

ParameterTypeDescription
searchstringFilter users by username or email (optional)
pageintegerPage 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"
}
FieldTypeAllowed Values
rolestringauthor, moderator, admin
Error Cases
CodeCause
400Cannot change own role (self-demotion prevention)
422Invalid 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

ParameterTypeDefaultDescription
pageinteger1Page 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

ActionTargetDetails
plugin.deactivatepluginPlugin name
plugin.reactivatepluginPlugin name
user.role_changeuserOld role → new role
user.deactivateuserUsername
user.reactivateuserUsername

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.

Rating in PluginSummary / SearchHit
Every plugin response now includes two extra fields:
average_rating: number — weighted average (0.0 when no reviews)
review_count: number — count of visible (non-hidden) reviews
The /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

ParameterTypeDefaultDescription
pageinteger1Page number (1-based)
per_pageinteger10Items 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
}
FieldTypeConstraints
ratingintegerRequired. 1–5 inclusive.
titlestring | nullOptional. Max 200 characters.
bodystring | nullOptional. Max 5 000 characters.

Response 201 Created

Returns the created ReviewResponse object.

Error Cases

CodeCause
401Not authenticated
403Missing write permission (API key)
404Plugin not found
409User has already reviewed this plugin
422Plugin 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

CodeCause
401Not authenticated
403Review does not belong to the authenticated user
404Plugin 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

CodeCause
401Not authenticated
403Not authorized (not owner, not plugin author, not staff)
404Plugin 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

CodeCause
403Not the plugin author or staff
404Plugin 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
}
ReasonDescription
spamSpam or promotional content
harassmentHarassment or targeted abuse
misinformationFalse or misleading content
inappropriateInappropriate or offensive language
otherOther reason (use details to elaborate)

Response 201 Created

Returns the created report object.

Error Cases

CodeCause
404Review not found
409User 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

ParameterTypeDefaultDescription
pageinteger1Page number
per_pageinteger20Items per page
statusstringpendingFilter 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

CodeCause
404Report not found
422Invalid 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"
  }
]
FieldTypeDescription
idUUIDMedia item identifier
plugin_idUUIDParent plugin identifier
media_typestringimage or video
original_filenamestringOriginal upload filename
file_sizeintegerFile size in bytes
mime_typestringMIME type (e.g. image/png, video/mp4)
urlstringPre-signed S3 URL (expires in 1 hour)
captionstring | nullOptional caption text
positionintegerDisplay order (0-indexed)
created_atISO 8601Upload timestamp

POST   /api/v1/plugins/{slug}/media   Auth

Uploads a new media item. The request must use multipart/form-data.

Form Fields

FieldRequiredDescription
fileYesBinary file payload. Accepted: JPEG, PNG, WebP (≤ 10 MB), MP4, WebM (≤ 100 MB)
captionNoOptional caption text (max 200 chars)

Response 201 Created

Returns the created media item object.

Error Cases

CodeCause
400Unsupported file type or file exceeds size limit
403Not the plugin owner
409Plugin 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

CodeCause
403Not the plugin owner
404Media 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

CodeCause
400Order array does not contain all media IDs, or contains unknown IDs
403Not 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"
}
FieldTypeDescription
idUUIDChangelog record identifier
plugin_idUUIDParent plugin identifier
contentstringRaw Markdown changelog content (sanitized server-side)
sourcestringmanual (inline editor) or github (synced from GitHub release)
created_atISO 8601Creation timestamp
updated_atISO 8601Last update timestamp

Error Cases

CodeCause
404Plugin 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"
}
FieldRequiredDescription
contentYesMarkdown changelog content (max 64 KB)
sourceNomanual (default) or github

Response 200 OK

Returns the full changelog object.

Error Cases

CodeCause
400Content exceeds 64 KB or is empty
403Not the plugin owner

DELETE   /api/v1/plugins/{slug}/changelog   Auth

Permanently deletes the plugin changelog.

Response 204 No Content

Error Cases

CodeCause
403Not the plugin owner
404Plugin or changelog not found