The interface between client and Manage server. Anyone using the bundled
client doesn't need this document — it's for custom clients, for debugging,
and for troubleshooting with curl.
Base URL: <MANAGE_SERVER_URL>/api/v1/
Note: the server's actual error strings are still German text, shown
verbatim in the examples below — this document's prose is in English, the
API contract is not.
Every request carries two headers:
X-Manage-Instance: myproject-prod
X-Manage-Token: e4032c4dc51e9100…
The server stores only the SHA-256 hash of the token and compares it in constant time. There is no session, no cookie and no shared password.
Error responses:
| Status | Meaning |
|---|---|
401 |
headers missing, instance unknown, or token wrong — deliberately indistinguishable |
403 |
instance exists but is deactivated |
405 |
wrong HTTP method |
429 |
too many failed authentications from this IP |
Failed logins are rate-limited per IP, so instance ids can't be brute-forced. A successful login resets the counter.
Every error response has the same shape:
{
"success": false,
"error": "Authentifizierung fehlgeschlagen."
}
Returns the release the instance should install.
curl -s https://manage.example.org/api/v1/manifest.php \
-H "X-Manage-Instance: myproject-prod" \
-H "X-Manage-Token: $TOKEN"
{
"success": true,
"latest": "v1.3.0",
"version": "v1.3.0",
"package_url": "https://manage.example.org/api/v1/package.php?version=v1.3.0",
"sha256": "70f17aae44a9afdd948de1767daa61f936bbecb52096753757791e230a22f024",
"size": 2199,
"published_at": "2026-08-20T09:20:43+00:00"
}
404 if no valid release is published.
package_url is built from the server configuration (MANAGE_PUBLIC_URL),
not from the request's Host header. A spoofed header therefore can't
redirect a client to a foreign server.
Returns the release ZIP.
curl -s -o release.zip \
"https://manage.example.org/api/v1/package.php?version=v1.3.0" \
-H "X-Manage-Instance: myproject-prod" \
-H "X-Manage-Token: $TOKEN"
Response: application/zip with Content-Length and
Cache-Control: private, no-store. No JSON on success.
400 on an invalid version format, 404 if the release doesn't exist.
The client compares size and SHA-256 against the manifest and deletes the file on mismatch. A custom client must do the same — without this check, arbitrary code gets deployed.
Accepts a backup archive. multipart/form-data:
| Field | Required | Meaning |
|---|---|---|
backup |
yes | the ZIP file |
filename |
no | backup-YYYYmmdd-HHMMSS[-N].zip; the server assigns a name if omitted |
sha256 |
no | checksum; recomputed and compared server-side |
meta |
no | JSON with trigger, file_count, source_bytes, app_version |
curl -s https://manage.example.org/api/v1/backup.php \
-H "X-Manage-Instance: myproject-prod" \
-H "X-Manage-Token: $TOKEN" \
-F "filename=backup-20260820-092104.zip" \
-F "sha256=824f3f80…" \
-F 'meta={"trigger":"cron","file_count":3}' \
-F "backup=@backup-20260820-092104.zip"
{
"success": true,
"instance": "myproject-prod",
"filename": "backup-20260820-092104.zip",
"size": 427,
"sha256": "824f3f80…",
"retention": 30,
"s3": { "enabled": false, "uploaded": false, "pending": 0 }
}
The server checks in this order: upload error code, is_uploaded_file,
size limit, ZIP signature, filename pattern, checksum after storing. If the
checksum doesn't match, the file is deleted again and 400 is reported.
An existing filename is never overwritten: the server appends -2, -3.
Errors during S3 archiving do not fail the upload — the local copy is stored and gets caught up later.
Status report. application/json:
{
"version": "v1.3.0",
"php_version": "8.3.6",
"disk_free": 12884901888,
"pending_migrations": 0,
"last_backup_at": "2026-08-20T09:21:04+00:00"
}
{
"success": true,
"instance": "myproject-prod",
"latest": "v1.3.0",
"update_available": false,
"server_time": "2026-08-20T09:23:11+00:00"
}
All request fields are optional; missing fields leave the server's current
value unchanged. The response replaces a separate call to manifest.php
for simple monitoring.
Every successfully authenticated request updates last_seen_at and the
instance's latest IP. The overview in the Manage server stays current this
way, even when only backups run and a heartbeat is never sent.