Overview
A typical integration looks like this: your service holds an API client record in minihost’s database (minihost_api_clients) with scopes upload, download, and optionally publish. Each request sends the same bearer credential; the server checks the scope for that route.
- Upload —
POST /api/minihost/uploadwith multipart fieldfile. - Download —
GET /api/minihost/download?id=…with the same bearer (private to your app). - Follow-up — optional
POST /api/minihost/upload_follow_upto store JSON metadata or mark handling time. - Publish — optional
POST /api/minihost/publishing_uploadto register a long-cache public URL for a ready upload.
Replace YOUR_ORIGIN below with your deployed host (for example https://www.2nfro.com).
Authentication
Every service call must include an Authorization header. Minihost uses a single string with the client key and secret separated by the first dot (not HTTP Basic like Trading 212’s Base64 key:secret, but the idea is the same: key + secret on every request).
Authorization: Bearer <client_key>.<secret>client_keyis the public label (also used as the uploadappbucket).secretis the raw secret; it is verified with a hash in the database (never logged in full by this API).- If the header is missing or malformed, the API returns
401with a JSON body{ "error": "…" }.
Scopes
Each API client has a scopes array. The route must match:
| Scope | Used for |
|---|---|
| upload | POST /api/minihost/upload, POST /api/minihost/upload_follow_up |
| download | GET /api/minihost/download |
| publish | POST /api/minihost/publishing_upload |
Building the header (examples)
cURL (upload):
curl -X POST "YOUR_ORIGIN/api/minihost/upload" \
-H "Authorization: Bearer YOUR_CLIENT_KEY.YOUR_SECRET" \
-F "file=@./artifact.zip"Python:
import requests
CLIENT_KEY = "YOUR_CLIENT_KEY"
SECRET = "YOUR_SECRET"
headers = {"Authorization": f"Bearer {CLIENT_KEY}.{SECRET}"}
r = requests.post(
"YOUR_ORIGIN/api/minihost/upload",
headers=headers,
files={"file": open("artifact.zip", "rb")},
timeout=300,
)
r.raise_for_status()
print(r.json())CORS
Browser fetch from allowed origins receives Access-Control-Allow-Origin (and preflight for OPTIONS). Built-in origins include production 2nfro hosts and common localhost ports. Set MINIHOST_CORS_ORIGINS to a comma-separated list for extra hosts. Server-side callers do not need CORS.
Service endpoints
All paths below are under your deployment origin. Successful JSON bodies are illustrative; exact fields match the handlers in this repo.
POST /api/minihost/upload
Auth: Bearer with upload scope. Body: multipart/form-data with field name file (filename and content-type come from the part). Max size defaults to MINIHOST_MAX_MB (50 MB if unset; server caps at 500 MB).
Response 200 JSON (success):
{
"uploadId": "…",
"uploadFileName": "artifact.zip",
"sizeKb": 123.4,
"finishedAt": "2026-04-17T12:00:00.000Z",
"downloadPath": "/api/minihost/download?id=…",
"storagePath": "…",
"archiveRelativePath": "…"
}Common errors: 400 missing file, 413 too large, 401 auth, 503 DB/storage not configured.
GET /api/minihost/download
Auth: Bearer with download scope. Query: id = upload id from the upload response.
Example:
curl -L "YOUR_ORIGIN/api/minihost/download?id=UPLOAD_ID" \
-H "Authorization: Bearer YOUR_CLIENT_KEY.YOUR_SECRET" \
-o downloaded.binResponse 200: binary stream with Content-Type from upload, Content-Disposition: attachment. Purged or deactivated uploads return 404 JSON.
POST /api/minihost/upload_follow_up
Auth: Bearer with upload scope. Body: JSON. Requires uploadId or upload_id, plus at least one of: metadata object app_payload / appPayload, extra top-level keys merged into payload, or app_handled_at / appHandledAt (ISO-8601 string).
Example — attach metadata:
curl -X POST "YOUR_ORIGIN/api/minihost/upload_follow_up" \
-H "Authorization: Bearer YOUR_CLIENT_KEY.YOUR_SECRET" \
-H "Content-Type: application/json" \
-d '{"uploadId":"UPLOAD_ID","app_payload":{"build":42,"channel":"stable"}}'Optional: app_payload exactly { "_PURGE": true } purges that uploadId (no metadata update). To attach metadata to a new upload and remove an old one, use _PURGE_FOR_REPLACEMENT with the old id string.
POST /api/minihost/publishing_upload
Auth: Bearer with publish scope. Body: JSON with upload_id (must exist for your client key and be ready when publishing). Optional status: published (default) or unpublished.
curl -X POST "YOUR_ORIGIN/api/minihost/publishing_upload" \
-H "Authorization: Bearer YOUR_CLIENT_KEY.YOUR_SECRET" \
-H "Content-Type: application/json" \
-d '{"upload_id":"UPLOAD_ID","status":"published"}'Response 200 includes cache_url when status is published (browser-facing path under /minihost/cache/…, see below).
Public cache URLs
These endpoints do not use Bearer tokens. They only serve files that are ready and have a published row in minihost_uploads_publish. Responses use long-lived cache headers (public, max-age=31536000, immutable) and Content-Disposition: inline.
GET YOUR_ORIGIN/minihost/cache/UPLOAD_ID— short public URL (canonical for sharing).GET YOUR_ORIGIN/api/minihost/cache/UPLOAD_ID— same bytes and headers,/apiprefix.
curl -L "YOUR_ORIGIN/minihost/cache/UPLOAD_ID" -o public.binThe origin in cache_url from publishing follows NEXT_PUBLIC_MINIHOST_PUBLIC_ORIGIN when set, otherwise the request host.
Operator endpoints (dashboard)
These are for humans or tools using the admin account — not for the per-app API keys above. Authentication is a signed session cookie after login.
POST /api/minihost/admin-login
Body: JSON { "username", "password" }. Response: { "ok": true } and Set-Cookie for the dashboard.
curl -c cookies.txt -X POST "YOUR_ORIGIN/api/minihost/admin-login" \
-H "Content-Type: application/json" \
-d '{"username":"OPERATOR","password":"…"}'POST /api/minihost/admin-logout
Clears the admin session cookie. No body required.
GET /api/minihost/admin-download
307 redirect to /minihost/dashboard/admin-download with the same query string (some proxies strip cookies on /api/*; the dashboard path keeps the session).
GET /minihost/dashboard/admin-download
Auth: admin session cookie. Query: id = upload id. Optional inline=1 for inline display.
POST /api/minihost/admin-purge-unhandled
Auth: admin session. Purges uploads that are still unhandled past the server cutoff. Response includes counts and cutoff timestamp.
OPTIONS (preflight)
Most minihost API routes implement OPTIONS with 204 and CORS headers for browser clients.
For debugging audit inserts, the server can echo status via response headers when MINIHOST_AUDIT_ECHO is enabled — see lib/minihost/api-audit.ts.