For developers
The HTTP API
Five endpoints. A bearer token, a zip, and no SDK to adopt.
The base URL is your instance — https://app.lemonadehost.com. Every request carries the token:
Authorization: Bearer lm_…
Endpoints
| Method | Path | Does |
|---|---|---|
GET | /api/deploy/whoami | Which site this token reaches, and whether it is serving. |
POST | /api/deploy/{siteId} | Publish a zip. Replaces the whole site. |
GET | /api/deploy/{siteId}/deployments | Recent publishes and their outcomes. |
GET | /api/deploy/{siteId}/files | The live files, as text. |
POST | /api/deploy/{siteId}/restore | Put a previous release back. |
Check the token first
curl -s https://app.lemonadehost.com/api/deploy/whoami \
-H "Authorization: Bearer $LEMONADE_TOKEN"
{
"ok": true,
"site": "st_…",
"name": "My site",
"subdomain": "mysite",
"url": "https://mysite.lemonhosted.com",
"serving": true
}
serving is the useful one: it is false when the site would accept files but not show them — unpaid, or held for review. A CI job can find that out without opening a support thread.
Publish
curl -X POST https://app.lemonadehost.com/api/deploy/st_… \
-H "Authorization: Bearer $LEMONADE_TOKEN" \
-H "Content-Type: application/zip" \
--data-binary @site.zip
Put index.html at the ROOT of the zip. Nothing refuses an archive without one — the publish succeeds and the site's home address then serves a 404, which is a worse way to find out. A single wrapper folder is the usual cause, and only the GitHub door strips one; this endpoint publishes the archive exactly as sent.
Limits
| Limit | Default |
|---|---|
| Compressed upload | 256 MB |
| Any single file | 25 MB |
| Files per site | 20,000 |
| Total extracted | 1 GB |
| Restore window | 7 days |
Status codes
| Code | Means |
|---|---|
401 | Missing, malformed or revoked token. |
404 | No such site — or not this token's site. The two are deliberately indistinguishable. |
409 | A deploy is already running, or the site is managed by the editor. |
413 | Too large. See the table above. |
429 | Rate limited. Retry-After says how long. |
400 | Empty body, or the upload was interrupted. |
Every refusal, with the fix, is on when a publish is refused.
Reading files back
GET /api/deploy/{siteId}/files returns the live text files as JSON. Binary files are listed but not returned, the total is capped, and the response says when it truncated — so an assistant can tell the difference between "that is the whole site" and "that is as much as fitted".
Read before you replace. A publish replaces everything, so an edit is: read the files, change the one you meant, send the whole set back.