Skip to main content

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​

MethodPathAccess
GET/apps/:hashed_id/buildsRead
POST/apps/:hashed_id/buildsWrite
POST/apps/:hashed_id/builds/:build_hashed_id/uploadedWrite
POST/apps/:hashed_id/builds/gitWrite
POST/apps/:hashed_id/builds/imageWrite
POST/apps/:hashed_id/builds/:build_hashed_id/retryWrite
GET/apps/:hashed_id/builds/:build_hashed_id/logsRead
GET/apps/:hashed_id/builds/:build_hashed_id/logs/streamRead

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"
}
FieldTypeDescription
hashed_idstringThe build's id
versionstringThe version number, 1, 2, 3 and so on. A string; sort builds by created_at, not by version
attemptinteger1, increased by each retry of the same version
statusstringSee Build statuses
source_filename, source_sizestring, integerThe source archive
uploadedbooleanWhether the source has arrived
commit_sha, commit_messagestringThe commit built, for a build from git. Empty for an upload
source_imagestringThe image:tag deployed, for a build of an image app. Empty otherwise
image_refstringThe 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
messagestringA one line summary, for example why the build failed
started_at, finished_atstring or nullWhen the build started and ended
created_atstringWhen the build was created

Build statuses​

StatusMeaningSettled
PendingWaiting for the source to be uploaded or fetchedNo
QueuedWaiting for a builderNo
BuildingBuilding the imageNo
DeployingRolling the image outNo
StartingWaiting for the new version to startNo
ReadyThe version is liveYes
FailedThe build or deploy failed. message says why, the log has the detailsYes
SupersededA later version replaced itYes
CanceledThe build was canceledYes

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.

FieldTypeRequiredDescription
source.namestringYesThe archive's file name
source.typestringYesMust be application/zip
source.sizeintegerYesThe 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"
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.

FieldTypeRequiredDescription
uploadedbooleanYestrue 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​

StatusFieldMessage
400sourcePlease 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
deploy.py
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.

StatusMessage
400This app was not created from a repository.
400This app is already building, wait for that build to finish!
400This app's GitHub connection is gone. Reconnect GitHub and try again. (and the same for GitLab and Bitbucket)
400This app is paused. Resume it before deploying a new version!
403This 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.

FieldTypeRequiredDescription
tagstringNoThe 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.

StatusFieldMessage
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!
400tagghcr.io/acme/api has no tag v2.
400passwordThe 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.

StatusMessage
400Only a failed build can be retried!
400This build never received its source, deploy a new version instead!
400This app is already building, wait for that build to finish before retrying!
400This app is paused. Resume it before deploying a new version!
404App 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.

HeaderDescription
Last-Event-IDOptional. 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"
Response
: 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

LineMeaning
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: endThe build has finished printing. Close the connection, then read the build's final status from the builds list
: open, : pingComments 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.