# Protocol v1 ## Overview 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: `/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. ## Authentication Every request carries two headers: ```http 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: ```json { "success": false, "error": "Authentifizierung fehlgeschlagen." } ``` ## GET manifest.php Returns the release the instance should install. ```bash curl -s https://manage.example.org/api/v1/manifest.php \ -H "X-Manage-Instance: myproject-prod" \ -H "X-Manage-Token: $TOKEN" ``` ```json { "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. ## GET package.php Returns the release ZIP. ```bash 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. ## POST backup.php 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` | ```bash 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" ``` ```json { "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. ## POST heartbeat.php Status report. `application/json`: ```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" } ``` ```json { "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. ## Side effect of every request 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. ## Next - [09_TROUBLESHOOTING](09_TROUBLESHOOTING.md) – what individual error messages mean - [10_SECURITY](10_SECURITY.md) – handling the token