App builds API endpoints
Every version of a Cloider app is a build: Cloider takes a source archive, builds a container image from it with your Dockerfile or with buildpacks, and deploys it. These endpoints list an app's builds, start new ones from uploaded source or from the app's git branch, retry failed ones, and read or stream their logs. Creating the app itself, which starts its first build, is on Apps.
All paths are relative to https://cloider.com/api. See Builds and deployments for how builds work.
At a glance
| Method | Path | Access |
|---|---|---|
| GET | /apps/:hashed_id/builds | Read |
| POST | /apps/:hashed_id/builds | Write |
| POST | /apps/:hashed_id/builds/:build_hashed_id/uploaded | Write |
| POST | /apps/:hashed_id/builds/git | Write |
| POST | /apps/:hashed_id/builds/image | Write |
| POST | /apps/:hashed_id/builds/:build_hashed_id/retry | Write |
| GET | /apps/:hashed_id/builds/:build_hashed_id/logs | Read |
| GET | /apps/:hashed_id/builds/:build_hashed_id/logs/stream | Read |
An app runs one build at a time. Starting a build while another one is running is refused.
The build object
{
"hashed_id": "u9e3k",
"version": "3",
"attempt": 1,
"status": "Ready",
"source_filename": "blog.zip",
"source_size": 1843200,
"uploaded": true,
"commit_sha": "4f2a9c1e8b7d6a5f3e2c1b0a9d8e7f6c5b4a3d2e",
"commit_message": "Add RSS feed",
"image_ref": "registry.cloider.com/acme-apps/blog-a8q2w:3",
"message": "",
"started_at": "2026-10-19T16:41:02.118207Z",
"finished_at": "2026-10-19T16:44:55.902114Z",
"created_at": "2026-10-19T16:40:58.004011Z"
}
| Field | Type | Description |
|---|---|---|
hashed_id | string | The build's id |
version | string | The version number, 1, 2, 3 and so on. A string; sort builds by created_at, not by version |
attempt | integer | 1, increased by each retry of the same version |
status | string | See Build statuses |
source_filename, source_size | string, integer | The source archive |
uploaded | boolean | Whether the source has arrived |
commit_sha, commit_message | string | The commit built, for a build from git. Empty for an upload |
source_image | string | The image:tag deployed, for a build of an image app. Empty otherwise |
image_ref | string | The image the build pushed, empty until it is pushed. For an image app, the image pinned to the digest its tag pointed at, set from the start |
message | string | A one line summary, for example why the build failed |
started_at, finished_at | string or null | When the build started and ended |
created_at | string | When the build was created |
Build statuses
| Status | Meaning | Settled |
|---|---|---|
Pending | Waiting for the source to be uploaded or fetched | No |
Queued | Waiting for a builder | No |
Building | Building the image | No |
Deploying | Rolling the image out | No |
Starting | Waiting for the new version to start | No |
Ready | The version is live | Yes |
Failed | The build or deploy failed. message says why, the log has the details | Yes |
Superseded | A later version replaced it | Yes |
Canceled | The build was canceled | Yes |
A build stays Pending for at most 15 minutes before it is failed with The source upload did not finish, and a build that stops making progress is failed after 45 minutes with The build did not finish in time, so a stuck build never blocks the next one forever.
List builds
GET /apps/:hashed_id/builds
Returns the app's builds, newest first, without their logs. Poll it every few seconds to follow a deploy.
Access: any token.
curl https://cloider.com/api/apps/a8q2w/builds \
-H "Authorization: Bearer $CLOIDER_TOKEN"
The response is a list of build objects. Wait for a deploy to finish:
BUILD_ID=u9e3k
while :; do
STATUS=$(curl -s https://cloider.com/api/apps/a8q2w/builds \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
| jq -r --arg id "$BUILD_ID" '.[] | select(.hashed_id == $id) | .status')
echo "$STATUS"
case "$STATUS" in Ready|Failed|Superseded|Canceled) break ;; esac
sleep 3
done
Deploy a new version from uploaded source
Deploying a new version of an existing app from a zip archive takes three requests: create the build, upload the archive to the presigned URL it returns, and confirm the upload, which queues the build. This is what cloider deploy does in a directory that is already linked to an app.
Step 1: create the build
POST /apps/:hashed_id/builds
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
source.name | string | Yes | The archive's file name |
source.type | string | Yes | Must be application/zip |
source.size | integer | Yes | The archive's exact size in bytes, at most 100 MB |
RESPONSE=$(curl -s -X POST https://cloider.com/api/apps/a8q2w/builds \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"source\": {\"name\": \"blog.zip\", \"type\": \"application/zip\", \"size\": $(wc -c < blog.zip)}}")
echo "$RESPONSE"
{
"hashed_id": "w2r7m",
"version": "4",
"presigned_url": "https://s3.eu-central-2.cloider.com/...?X-Amz-Signature=..."
}
Step 2: upload the archive
curl -X PUT "$(jq -r .presigned_url <<<"$RESPONSE")" \
-H "Content-Type: application/zip" \
--data-binary @blog.zip
Send exactly the size you declared, with Content-Type: application/zip, and without your API token.
Step 3: confirm the upload
POST /apps/:hashed_id/builds/:build_hashed_id/uploaded
Tells Cloider the archive is in place, which queues the build. If the upload failed, send false instead: the build is marked Failed right away so the app is free for the next one.
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
uploaded | boolean | Yes | true when the PUT succeeded, false when it didn't |
curl -X POST https://cloider.com/api/apps/a8q2w/builds/w2r7m/uploaded \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"uploaded": true}'
It answers 200. Confirming twice is harmless. With "uploaded": false it answers 400 App source did not upload successfully!.
Errors of step 1
| Status | Field | Message |
|---|---|---|
400 | source | Please select your app folder, a zip file or an HTML file!, The app source exceeded the allowed size of 100MB!, Invalid app source! |
400 | _ | This app is already building, wait for that build to finish before deploying a new version! |
400 | _ | This app is paused. Resume it before deploying a new version!, This app is being deleted! |
400 | _ | Your account is suspended. Upgrade to a paid plan to continue using our services. |
400 | _ | Operation failed, Couldn't start the build!, The source upload could not be started! |
403 | _ | This API token is read only! |
404 | _ | App not found! |
The whole deploy in Python
import os
import shutil
import time
import requests
API = "https://cloider.com/api"
APP = "a8q2w"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['CLOIDER_TOKEN']}"
archive = shutil.make_archive("blog", "zip", "blog")
size = os.path.getsize(archive)
res = s.post(f"{API}/apps/{APP}/builds",
json={"source": {"name": "blog.zip", "type": "application/zip", "size": size}})
res.raise_for_status()
build = res.json()
with open(archive, "rb") as f:
put = requests.put(build["presigned_url"], data=f,
headers={"Content-Type": "application/zip", "Content-Length": str(size)})
s.post(f"{API}/apps/{APP}/builds/{build['hashed_id']}/uploaded",
json={"uploaded": put.ok}).raise_for_status()
put.raise_for_status()
while True:
builds = s.get(f"{API}/apps/{APP}/builds").json()
status = next(b["status"] for b in builds if b["hashed_id"] == build["hashed_id"])
print("version", build["version"], status)
if status in ("Ready", "Failed", "Superseded", "Canceled"):
break
time.sleep(3)
Build the latest commit
POST /apps/:hashed_id/builds/git
For an app created from a repository, builds the latest commit of its branch now, the same as a push with auto deploy on. The source is fetched in the background, so the build starts Pending and moves to Queued once the archive has arrived. cloider deploy --git uses it.
Access: Read & Write token.
curl -X POST https://cloider.com/api/apps/a8q2w/builds/git \
-H "Authorization: Bearer $CLOIDER_TOKEN"
The response is the new build object, with status Pending. If the commit can't be fetched, the build fails with a message such as We could not reach GitHub or acme/api is over the 100MB limit.
| Status | Message |
|---|---|
400 | This app was not created from a repository. |
400 | This app is already building, wait for that build to finish! |
400 | This app's GitHub connection is gone. Reconnect GitHub and try again. (and the same for GitLab and Bitbucket) |
400 | This app is paused. Resume it before deploying a new version! |
403 | This API token is read only! |
Deploy a tag of an image app
POST /apps/:hashed_id/builds/image
For an app created from a Docker image, looks the tag up on the image's registry and deploys
the image it points at as a new version. Nothing is built: the build starts Queued with image_ref already set to
the image pinned to its digest. cloider deploy --app <app> uses it.
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
tag | string | No | The tag to deploy, which the app then follows. Empty pulls the app's own tag again |
curl -X POST https://cloider.com/api/apps/a8q2w/builds/image \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tag": "v2"}'
The response is the new build object, with status Queued.
| Status | Field | Message |
|---|---|---|
400 | _ | This app was not created from an image. |
400 | _ | The app is already running the image ghcr.io/acme/api:v2 points at. Push a new image to the tag, or deploy another tag. |
400 | _ | This app is already deploying, wait for that deploy to finish! |
400 | _ | This app is paused. Resume it before deploying a new version! |
400 | tag | ghcr.io/acme/api has no tag v2. |
400 | password | The registry refused this login, check the username and the token, and that it can pull ghcr.io/acme/api. |
403 | _ | This API token is read only! |
An image app can also be deployed without an API token, through its deploy hook.
Retry a failed build
POST /apps/:hashed_id/builds/:build_hashed_id/retry
Runs a failed build again with the same source and version, as the next attempt. Useful when a build failed for a reason outside your code, such as a registry hiccup.
Access: Read & Write token.
curl -X POST https://cloider.com/api/apps/a8q2w/builds/w2r7m/retry \
-H "Authorization: Bearer $CLOIDER_TOKEN"
The response is the build, now Queued with attempt increased by one.
| Status | Message |
|---|---|
400 | Only a failed build can be retried! |
400 | This build never received its source, deploy a new version instead! |
400 | This app is already building, wait for that build to finish before retrying! |
400 | This app is paused. Resume it before deploying a new version! |
404 | App not found!, Build not found! |
Get a build's log
GET /apps/:hashed_id/builds/:build_hashed_id/logs
Returns the complete output of a finished build as plain text. While a build runs, use the stream instead.
Access: any token. The log contains whatever your build printed.
curl https://cloider.com/api/apps/a8q2w/builds/w2r7m/logs \
-H "Authorization: Bearer $CLOIDER_TOKEN" -o build-4.log
Stream a build's log
GET /apps/:hashed_id/builds/:build_hashed_id/logs/stream
Streams a build's output as server-sent events while it runs, and ends with an end event when the build has printed its last line. For a build that is already over, the whole log arrives in one event followed by end.
Access: any token.
| Header | Description |
|---|---|
Last-Event-ID | Optional. The id of the last event you received, to resume a dropped stream where it stopped instead of starting over |
curl -N https://cloider.com/api/apps/a8q2w/builds/w2r7m/logs/stream \
-H "Authorization: Bearer $CLOIDER_TOKEN"
: open
id: 1760891262118-0
data: ===> DETECTING
data: 3 of 4 buildpacks participating
id: 1760891265402-0
data: ===> BUILDING
data: Installing node 22.11.0
: ping
id: 1760891391007-0
data: ===> EXPORTING
event: end
data: 1
| Line | Meaning |
|---|---|
id: | The event's id. Remember the last one to resume with Last-Event-ID |
data: | Output. One chunk can span several data: lines; join them with newlines |
event: end | The build has finished printing. Close the connection, then read the build's final status from the builds list |
: open, : ping | Comments that keep the connection alive. Ignore them |
A stream also ends by itself after 30 minutes. cloider app build logs and the dashboard reconnect with Last-Event-ID when a proxy drops the connection; do the same in your client.
Related
- Apps: create apps and set variables
- Builds and deployments
- Troubleshooting apps
- cloider app and cloider deploy