Apps API endpoints
These endpoints manage App Engine apps: they upload or import source code, detect its language and port, create the app, set its environment variables and build settings, stream its logs, run commands in it, and pause, resume or delete it. Every new version of an app is a build; the build endpoints are on App builds, and the git connections an import uses are on Git.
All paths are relative to https://cloider.com/api. See Authentication for the token header.
At a glance
The app object
{
"hashed_id": "a8q2w",
"app_name": "blog",
"app_label": "blog",
"port": 8080,
"start_command": "",
"builder": "buildpacks",
"schedule": "",
"next_run_at": null,
"status": "Ready",
"language": "JavaScript",
"framework": "Next.js",
"source_filename": "blog.zip",
"source_size": 1843200,
"uploaded": true,
"git_provider": "GitHub",
"repo_full_name": "acme/blog",
"repo_url": "https://github.com/acme/blog",
"repo_branch": "main",
"auto_deploy": true,
"git_connected": true,
"image": "",
"image_tag": "",
"image_username": "",
"image_ref": "registry.cloider.com/acme-apps/blog-a8q2w:3",
"dockerfile_args": [],
"version": "3",
"url": "https://blog.cloider.app",
"domains": [
{"domain_name": "blog.cloider.app", "domain_type": "Subdomain"},
{"domain_name": "blog.example.com", "domain_type": "CustomDomain"}
],
"created_at": "2026-10-01T09:20:31.774208Z"
}
| Field | Type | Description |
|---|---|---|
hashed_id | string | The app's id |
app_name | string | The name you gave it |
app_label | string | A name unique among the account's apps (-1, -2 appended when taken) |
port | integer | The port the app listens on. It is passed to the app as the PORT environment variable |
start_command | string | The command the app starts with, or empty to let the source or the image decide. A buildpack build has it in its Procfile; an image app and a Dockerfile build run it with sh -c in place of the image's ENTRYPOINT and CMD |
builder | string | How the live version was built: dockerfile, buildpacks or static. Empty for an image app, and for a version built before this was recorded |
schedule | string | A cron job's cron schedule, in UTC. Empty for an app that serves on its port. A cron job has no port, URL or domains |
next_run_at | string or null | When a cron job runs next. null for any other app, and for a paused cron job |
status | string | See App statuses |
language, framework | string | What detection found in the source, for display only |
source_filename, source_size | string, integer | The source archive of the live build |
uploaded | boolean | Whether the app's source has arrived |
git_provider | string | GitHub, GitLab or Bitbucket. Only meaningful when repo_full_name is set |
repo_full_name, repo_url, repo_branch | string | The repository and branch the app deploys from. Empty for an uploaded app |
auto_deploy | boolean | Whether a push to repo_branch builds a new version |
git_connected | boolean | Whether the git connection the repository is read through still exists |
image, image_tag | string | The image and tag an image app runs. Empty for an app built from source |
image_username | string | The username an image app's image is pulled with, empty for a public image. The token is never returned |
image_ref | string | The image the app is running. Empty until a build has deployed |
dockerfile_args | object list | The ARGs the source's Dockerfile declares, each with a name and a default (or null) |
version | string | The version of the build being served. Empty until the first deploy |
url | string | The address the app is served on, or empty when no domain points to it |
domains | object list | Every domain the app is served on, with its domain_type (Subdomain or CustomDomain) |
created_at | string | When the app was created |
App statuses
An app's status follows its latest build while one runs, so a list of apps shows how far a deploy has got.
| Status | Meaning |
|---|---|
Pending | Waiting for the source to be uploaded |
Queued, Building, Deploying, Starting | A build is running through its stages |
Ready | Running and serving its URL, or for a cron job, deployed and running on its schedule |
Failed | The first build failed. An app that was already running stays Ready when a later build fails |
Pausing, Paused | Being stopped, or stopped |
Terminating, Terminated | Being deleted, or deleted |
List apps
GET /apps
Returns the account's apps, newest first, without deleted ones.
Access: any token.
curl https://cloider.com/api/apps \
-H "Authorization: Bearer $CLOIDER_TOKEN"
The response is a list of app objects.
Get an app
GET /apps/:hashed_id
Returns one app. An unknown id answers 404 with App not found!.
Access: any token.
curl https://cloider.com/api/apps/a8q2w \
-H "Authorization: Bearer $CLOIDER_TOKEN"
Get an app's status
GET /apps/:hashed_id/status
Returns the status as plain text, for example Building or Ready.
Access: any token.
curl https://cloider.com/api/apps/a8q2w/status \
-H "Authorization: Bearer $CLOIDER_TOKEN"
To wait for a deploy, poll the builds list instead: the build says whether it ended Ready or Failed, and why.
Get an app's usage
GET /apps/:hashed_id/usage
Returns the app with its compute usage and cost: the last 31 days in daily_usage (with cpu in millicores, memory in MiB and compute_cost per day) and this month's totals in monthly_usage (avg_cpu_usage, avg_memory_usage, compute_total_cost, total_cost, days, forecast_this_month, forecast_next_month), plus forecast_rest_of_day, date and month. It is the app's entry from GET /usage and the data of the Insights tab.
Access: any token.
curl https://cloider.com/api/apps/a8q2w/usage \
-H "Authorization: Bearer $CLOIDER_TOKEN"
Create an app from uploaded source
Creating an app from code on your machine takes five requests. The source is a zip archive of your project, up to 100 MB. The bytes go straight to storage through a presigned URL, never through the API.
This is what cloider deploy does the first time it deploys a folder. See Deploy from your computer.
Step 1: get an upload URL
POST /apps/source
Stages an upload and returns a token that names it, and a presigned URL to PUT the archive to.
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
source.name | string | Yes | The archive's file name, for example blog.zip |
source.type | string | Yes | Must be application/zip |
source.size | integer | Yes | The archive's exact size in bytes, at most 100 MB |
cd blog && zip -qr ../blog.zip . -x 'node_modules/*' '.git/*' && cd ..
curl -s -X POST https://cloider.com/api/apps/source \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"source\": {\"name\": \"blog.zip\", \"type\": \"application/zip\", \"size\": $(wc -c < blog.zip)}}"
{
"token": "9f2c4e7a1b3d5f60718293a4b5c6d7e8",
"presigned_url": "https://s3.eu-central-2.cloider.com/...uploads/9f2c4e7a...?X-Amz-Signature=..."
}
Errors on source: Please select your app folder, a zip file or an HTML file! (size 0), The app source exceeded the allowed size of 100MB!, Invalid app source! (not application/zip). A staged upload expires after two hours.
Step 2: upload the archive
PUT the zip to the presigned URL with the same size and Content-Type: application/zip. Don't send your API token to it.
curl -X PUT "$PRESIGNED_URL" \
-H "Content-Type: application/zip" \
--data-binary @blog.zip
Step 3: start detection
POST /apps/source/:token/detect
Asks Cloider to read the archive and detect the language, framework, port and a suggested name. It answers 200 at once and runs in the background.
Access: Read & Write token.
curl -X POST https://cloider.com/api/apps/source/$TOKEN/detect \
-H "Authorization: Bearer $CLOIDER_TOKEN"
An expired or unknown token answers 404 with This upload has expired, upload your app again!.
Step 4: read the detection result
GET /apps/source/:token
Returns what detection found. Poll it every second or two until status is Done, Failed or Unavailable.
Access: any token.
curl https://cloider.com/api/apps/source/$TOKEN \
-H "Authorization: Bearer $CLOIDER_TOKEN"
{
"status": "Done",
"app_name": "blog",
"language": "JavaScript",
"framework": "Next.js",
"port": 3000,
"message": "",
"build_args": [{"name": "NEXT_PUBLIC_API_URL", "default": null}]
}
| Field | Description |
|---|---|
status | Pending (waiting for the upload), Fetching (an import is being downloaded), Detecting, Done, Failed (detection gave up, but the app can still be created and built) or Unavailable (an import that couldn't be fetched, the one status an app can't be created from) |
app_name | A name that passes the naming rules, from the project's own name or the archive's |
language, framework | What was detected, possibly empty |
port | The detected port, or 0 when there is no opinion |
message | Why detection failed or the import is unavailable |
build_args | The ARGs declared by a Dockerfile in the source |
git | Only for an import: provider, repo_full_name, repo_url, branch, commit_sha and commit_message |
Step 5: create the app
POST /apps
Creates the app from a staged source and starts its first build right away. Optionally claims a free subdomain and sets variables in the same request.
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
app_name | string | Yes | 2 to 64 characters: lower case letters, digits and -, starting with a letter, not ending with - |
port | integer | Yes, unless schedule is set | 1 to 65535, the port the app listens on |
source_token | string | Yes | The token from step 1, or from an import |
start_command | string | No | One line, up to 1000 characters. A buildpack build writes it into a Procfile; an image app and an app built from a Dockerfile run it with sh -c in place of the image's ENTRYPOINT and CMD |
schedule | string | No | Makes the app a cron job run on this cron schedule, in UTC, for example 0 3 * * *. Five fields or a shorthand like @daily. A cron job takes no port or subdomain |
subdomain | string | No | A full free subdomain to claim, for example blog.cloider.app. Empty for none |
env_vars | object list | No | Runtime environment variables, see Variables |
build_env_vars | object list | No | Build variables |
build_args | object list | No | Dockerfile build arguments |
curl -X POST https://cloider.com/api/apps \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"app_name": "blog",
"port": 3000,
"source_token": "9f2c4e7a1b3d5f60718293a4b5c6d7e8",
"subdomain": "blog.cloider.app",
"env_vars": [
{"key": "NODE_ENV", "value": "production", "secret": false},
{"key": "DATABASE_URL", "value": "postgresql://orders:...@orders-db-d7f2m.db.cloider.internal:20417/orders-db", "secret": true}
]
}'
{
"hashed_id": "a8q2w",
"build_hashed_id": "u9e3k",
"version": "1"
}
The app is created even when a later part of the request fails. Those failures come back as extra fields of a successful response, not as errors: subdomain_error (the subdomain couldn't be claimed), env_error, build_env_error and build_args_error (the variables couldn't be saved; set them again with the variable endpoints). Follow the first build with GET /apps/a8q2w/builds until its status is Ready or Failed.
| Status | Field | Message |
|---|---|---|
400 | app_name | App name must not be empty, App name can not be upper case, App name must not have special characters except "-", App name must be at least 2 characters, App name must not exceed 64 characters, and the like |
400 | port | App port must not be empty, App port must be between 1 and 65535 |
400 | start_command | Start command must be at most 1000 characters, Start command must be a single line |
400 | schedule | A schedule has five fields: minute, hour, day of month, month and day of week., This schedule never runs, check the day of month and month., and a message naming the field that is out of range |
400 | subdomain | A cron job has no port to serve a domain on, leave this empty. |
400 | source | Your app source is no longer available, upload it again!, Your repository is still being fetched, give it a moment and try again!, or the import's own message |
400 | subdomain | This subdomain is already taken. Please try another one., You already own this subdomain. Connect it to the app from Domains instead., and the subdomain naming rules |
400 | env_vars.<KEY> | Per variable messages, see Variables |
400 | _ | Your account is suspended. Upgrade to a paid plan to continue using our services., Couldn't prepare your app's registry, try again in a moment!, Operation failed, Couldn't create the app! |
400 | _ | App created but its first build could not be started!, with data set to {"hashed_id": "..."} of the app that was created |
403 | _ | This API token is read only! |
Check a subdomain before you create the app with GET /domains/availability.
Import source from a git repository
POST /apps/source/github
POST /apps/source/gitlab
POST /apps/source/bitbucket
Instead of uploading, you can stage the source from a repository on a connected GitHub, GitLab or Bitbucket account. The import downloads the latest commit of the branch in the background and runs detection on it; you then poll GET /apps/source/:token and create the app with POST /apps, exactly as for an upload. An app created this way deploys every push to its branch.
Access: Read & Write token, and a git connection made in the dashboard. See Git to list connections, repositories and branches.
| Field | Type | Required | Description |
|---|---|---|---|
installation_hashed_id | string | Yes | The connection's hashed_id from GET /git/<provider>/installations |
repo_full_name | string | Yes for GitHub and Bitbucket | owner/repo (GitHub) or workspace/repo (Bitbucket). For GitLab, a display hint |
project_id | integer | Yes for GitLab | The GitLab project's numeric id |
branch | string | Yes | The branch to deploy |
curl -X POST https://cloider.com/api/apps/source/github \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"installation_hashed_id": "g2h6v", "repo_full_name": "acme/api", "branch": "main"}'
{"token": "4b8e1d0c7a2f9e6b3d5c8a1f0e7d2b9c"}
The import starts with the status Fetching. When the repository can't be read, the status becomes Unavailable with a message such as We could not read acme/api at main. The branch may be gone, or the repository may have no commits yet. or acme/api is over the 100MB limit.
| Status | Field | Message |
|---|---|---|
400 | source | That repository name is not one we can read., Pick a project to import., Pick a branch to deploy from. |
400 | _ | This GitHub installation is suspended. Re-enable it there to import from it. (and the same for the other providers) |
404 | _ | This GitHub connection is no longer available, reconnect it! |
Create an app from an image
POST /apps/source/image
Stages a Docker image you built as the app's source, instead of an upload or a repository. Cloider looks the tag up on
the image's registry before answering, so the response is already Done and there is nothing to poll: create the app
with its token through POST /apps, leaving out start_command, build_env_vars and
build_args, which an image app refuses. See Deploy a Docker image.
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
image | string | Yes | The image the way docker pull takes it, for example nginx or ghcr.io/acme/api. A tag written into it is used when tag is empty |
tag | string | No | The tag, latest when empty. Digests aren't accepted |
username | string | No | The registry login's username, for a private image |
password | string | With username | The registry login's token or password, ideally one that can only pull |
curl -X POST https://cloider.com/api/apps/source/image \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"image": "ghcr.io/acme/api", "tag": "v1", "username": "acme", "password": "'"$GHCR_TOKEN"'"}'
{
"token": "4b8e1d0c7a2f9e6b3d5c8a1f0e7d2b9c",
"status": "Done",
"app_name": "api",
"language": "",
"framework": "",
"port": 8080,
"message": "",
"build_args": [],
"image": {"image": "ghcr.io/acme/api", "tag": "v1", "digest": "sha256:9c1f3a...", "username": "acme"}
}
port is the one TCP port the image exposes, or 0 when it exposes none or several. The first version deploys the
image at digest.
| Status | Field | Message |
|---|---|---|
400 | image | Enter the image to deploy. |
400 | image | ghcr.io/acme/api does not exist or is private. If it is private, add a login for its registry. |
400 | image | This image has no linux/amd64 build, which is what apps run on. Build it with --platform linux/amd64. |
400 | image | This registry is not on a public address, apps can only pull from public registries. |
400 | tag | ghcr.io/acme/api has no tag v1. |
400 | username | Enter the username this token belongs to. |
400 | password | Enter the token or password for this login., The registry refused this login, check the username and the token, and that it can pull ghcr.io/acme/api. |
Change an image app's image or login
PUT /apps/:hashed_id/image
Changes the image an image app runs, its tag, or its registry login. The new values are checked with the registry and only saved once it accepts them; the app then deploys when the tag points at a different image than the one running.
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
image | string | Yes | The image, as for POST /apps/source/image |
tag | string | No | The tag, latest when empty |
username | string | No | The login's username. Empty pulls without a login |
password | string or null | No | The login's token. null keeps the saved one, for the same username |
curl -X PUT https://cloider.com/api/apps/a8q2w/image \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"image": "ghcr.io/acme/api", "tag": "v3", "username": "acme", "password": null}'
{"applied": "build", "build": {"hashed_id": "u9e3k", "version": "4", "status": "Queued"}}
applied is build when a new version started, the build object in
build, or none when the tag still points at the running image. The errors are those of
POST /apps/source/image, plus This app was not created from an image.
Deploy hook
POST /apps/:hashed_id/deploy-hook
POST /apps/:hashed_id/deploy-hook/rotate
POST /deploy-hooks/apps/:hashed_id/:secret
An image app's deploy hook is a URL that deploys the app when it receives a POST, with no API token: for a CI job to
call after it pushed the image, or for a registry's push webhook. POST /apps/:hashed_id/deploy-hook returns the URL,
creating it the first time, and /rotate replaces it so the previous URL stops working. Both answer
{"url": "https://cloider.com/api/deploy-hooks/apps/a8q2w/..."} and need a Read & Write token.
Calling the URL pulls the app's tag again. A tag in the query string (?tag=v2) or in a JSON body ({"tag": "v2"})
deploys that tag instead, and the app follows it from then on.
curl -X POST "https://cloider.com/api/deploy-hooks/apps/a8q2w/<secret>?tag=v2"
{"deployed": true, "build_hashed_id": "u9e3k", "version": "4", "image_ref": "ghcr.io/acme/api@sha256:9c1f3a..."}
| Status | Meaning |
|---|---|
202 | A new version is deploying |
200 | {"deployed": false}: the tag still points at the running image |
404 | The app or the secret is wrong, or the URL was regenerated |
409 | The app is already deploying, is paused or being deleted, or the account is suspended |
422 | The image can't be read, with the message in errors._, for example ghcr.io/acme/api has no tag v2. |
Change the port, schedule or start command
PUT /apps/:hashed_id/settings
Changes the port the app listens on, or a cron job's schedule, and its start command. Saving them also applies them to the running app: a changed port or schedule redeploys the current image. A changed start command builds the current source again, or only redeploys the current image for an image app and an app built from a Dockerfile (builder is dockerfile), which run it in place of the image's ENTRYPOINT and CMD.
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
port | integer | Yes, unless the app is a cron job | 1 to 65535. Send the current port to keep it. A cron job ignores it |
start_command | string | No | One line, up to 1000 characters. Empty clears it |
schedule | string | No | A cron job's new schedule, in UTC. Empty keeps the one it has. Refused on any other app: an app's kind is fixed when it is created |
curl -X PUT https://cloider.com/api/apps/a8q2w/settings \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"port": 8080, "start_command": "node server.js"}'
{"applied": "build"}
applied says what happened: none (nothing changed, or the app has never deployed, so its next build picks the settings up), redeploy (the port, the schedule, or the start command of an image or Dockerfile app changed and the running image is being redeployed) or build (a buildpack app's start command changed and a new build started).
| Status | Message |
|---|---|
400 | This app is paused. Resume it before deploying a new version!, This app is being deleted! |
400 | This app is building, wait for that build to finish before changing its settings! |
400 | The settings were saved but the app could not be redeployed! |
See Settings.
Change the branch or auto deploy
PUT /apps/:hashed_id/git
For an app created from a repository, sets the branch it deploys from and whether a push to that branch deploys automatically.
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
branch | string | Yes | The branch name |
auto_deploy | boolean | Yes | true to deploy every push to the branch. Leaving it out turns auto deploy off |
curl -X PUT https://cloider.com/api/apps/a8q2w/git \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"branch": "production", "auto_deploy": true}'
It answers 200 with an empty body. Errors: This app was not created from a repository. and Pick a branch to deploy from. (on branch). To deploy the branch's latest commit now, use POST /apps/:hashed_id/builds/git.
Environment variables, build variables and build arguments
An app has three lists of variables, each read and replaced with its own endpoints:
| List | Endpoints | Read by | Applied |
|---|---|---|---|
| Environment (runtime) | /apps/:hashed_id/env | The running app | Saving restarts the app |
| Build variables | /apps/:hashed_id/build-env | Buildpack builds (no Dockerfile) | Saving starts a build of the current source |
| Build arguments | /apps/:hashed_id/build-args | Dockerfile builds, as ARG (secret ones as build secrets) | Saving starts a build of the current source |
Each variable is an object:
| Field | Type | Description |
|---|---|---|
key | string | 1 to 128 characters, a letter or _ first, then letters, digits and _ |
value | string or null | The value, up to 16 KB. In a read, null for a secret. In a write, null keeps the value already stored |
secret | boolean | true to store the value as a secret, which is never returned by the list endpoints |
A list can hold up to 100 variables and 64 KB in total. See Environment variables and Build configuration.
List variables
GET /apps/:hashed_id/env
GET /apps/:hashed_id/build-env
GET /apps/:hashed_id/build-args
Returns one of the lists, with secret values as null.
Access: any token.
curl https://cloider.com/api/apps/a8q2w/env \
-H "Authorization: Bearer $CLOIDER_TOKEN"
[
{"key": "NODE_ENV", "secret": false, "value": "production"},
{"key": "DATABASE_URL", "secret": true, "value": null}
]
Reveal a secret value
GET /apps/:hashed_id/env/value?key=:key
GET /apps/:hashed_id/build-env/value?key=:key
GET /apps/:hashed_id/build-args/value?key=:key
Returns one variable's value as plain text, secret or not.
Access: any token, including read only tokens.
curl -G https://cloider.com/api/apps/a8q2w/env/value \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
--data-urlencode "key=DATABASE_URL"
postgresql://orders:Xk29fQ7pLm3vT8wZ@orders-db-d7f2m.db.cloider.internal:20417/orders-db
Errors: 400 No variable was asked for!, 404 Variable not found!.
Replace the runtime environment
PUT /apps/:hashed_id/env
Replaces the app's environment with the list you send, then restarts the app so it picks the values up. A variable missing from the list is removed. A variable sent with "value": null keeps its stored value, so you can send back the list you read without losing secrets.
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
env_vars | object list | Yes | The complete new list |
curl -X PUT https://cloider.com/api/apps/a8q2w/env \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"env_vars": [
{"key": "NODE_ENV", "value": "production", "secret": false},
{"key": "DATABASE_URL", "value": null, "secret": true},
{"key": "STRIPE_KEY", "value": "sk_live_xxxxxxxx", "secret": true}
]
}'
{"success": true}
To add one variable without touching the others, read the list, change it, and send it back:
curl -s https://cloider.com/api/apps/a8q2w/env -H "Authorization: Bearer $CLOIDER_TOKEN" \
| jq '{env_vars: (map(select(.key != "LOG_LEVEL")) + [{key: "LOG_LEVEL", value: "debug", secret: false}])}' \
| curl -X PUT https://cloider.com/api/apps/a8q2w/env \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" --data-binary @-
Validation errors are keyed by the variable: env_vars.<KEY> with Variable name must start with a letter or underscore and contain only letters, numbers and underscores, Variable name must not exceed 128 characters, This variable is set more than once or This value exceeds the allowed size of 16KB; and on env_vars, An app can have at most 100 environment variables or The environment variables exceed the allowed size of 64KB. The environment could not be saved, try again! means the change wasn't applied.
Replace build variables or build arguments
PUT /apps/:hashed_id/build-env
PUT /apps/:hashed_id/build-args
Replace the build variables or the build arguments, with the same body and rules as the environment (env_vars is the field name for both). Because only a build reads them, saving starts a build of the source the app is serving, so the change reaches the running app.
Access: Read & Write token.
curl -X PUT https://cloider.com/api/apps/a8q2w/build-args \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"env_vars": [{"key": "NEXT_PUBLIC_API_URL", "value": "https://api.example.com", "secret": false}]}'
{"applied": "build"}
applied is build when a build started, or none when the app has never deployed (its first build reads them). Errors besides validation: This app is paused. Resume it before deploying a new version!, This app is building, wait for that build to finish before changing its build arguments!, and The build arguments were saved, and apply from the next build on!.
Stream an app's logs
GET /apps/:hashed_id/logs/stream
Streams what the app prints to standard output and standard error as server-sent events. It starts with the last lines and then follows new output for up to an hour.
Access: any token, including read only tokens. The logs contain whatever the app prints, secrets included.
| Query parameter | Required | Description |
|---|---|---|
lines | No | How many past lines to start with: 100, 200, 500 or 1000. Default 200 |
follow | No | false to send the past lines and end. Default: follow |
run | No | A cron job's run to read, by its name from the runs. Without it, a cron job's stream follows its latest run and moves on to each new one. With it, a followed stream ends once that run has finished. 404 Run not found! on any other app |
curl -N "https://cloider.com/api/apps/a8q2w/logs/stream?lines=100" \
-H "Authorization: Bearer $CLOIDER_TOKEN"
: open
event: reset
data: 1
data: > blog@1.0.0 start
data: > next start -p 8080
data: ready - started server on 0.0.0.0:8080
: ping
data: GET /api/posts 200 in 41ms
| Event | Meaning |
|---|---|
message (lines with no event:) | Log output. A chunk can span several data: lines; join them with newlines to get exactly what was printed |
reset | A fresh tail starts. Sent first on every connection: drop what you showed from an earlier connection |
end | The stream is over (follow=false, the one hour limit, or the tail stopped). Close the connection |
Lines starting with : (: open, : ping) keep the connection alive; ignore them. An app that hasn't deployed yet answers with the single line This app has not deployed a version yet. and end. If the stream drops, reconnect: you get a new tail, announced by reset. See Logs.
Read the log stream in JavaScript
const res = await fetch('https://cloider.com/api/apps/a8q2w/logs/stream?lines=200', {
headers: { Authorization: `Bearer ${process.env.CLOIDER_TOKEN}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const decoder = new TextDecoder();
let buffer = '';
for await (const chunk of res.body) {
buffer += decoder.decode(chunk, { stream: true });
let sep;
while ((sep = buffer.indexOf('\n\n')) !== -1) {
const raw = buffer.slice(0, sep);
buffer = buffer.slice(sep + 2);
let event = 'message';
const data = [];
for (const line of raw.split('\n')) {
if (line.startsWith(':')) continue;
if (line.startsWith('event: ')) event = line.slice(7);
else if (line.startsWith('data: ')) data.push(line.slice(6));
}
if (event === 'end') process.exit(0);
if (event === 'reset') console.log('--- new tail ---');
if (event === 'message') process.stdout.write(data.join('\n') + '\n');
}
}
List a cron job's runs
GET /apps/:hashed_id/runs
Lists a cron job's runs, newest first. The last 5 succeeded and 5 failed runs are kept. A cron job that hasn't deployed yet has none.
Access: any token.
curl https://cloider.com/api/apps/k3v9q/runs -H "Authorization: Bearer $CLOIDER_TOKEN"
[
{
"name": "app-29885940",
"manual": false,
"status": "Failed",
"exit_code": 1,
"message": "Exited with code 1",
"created_at": "2026-10-28T03:00:00Z",
"started_at": "2026-10-28T03:00:00Z",
"finished_at": "2026-10-28T03:00:03Z"
}
]
| Field | Type | Description |
|---|---|---|
name | string | The run's name, which its logs are read by |
manual | boolean | Started with Run now rather than by the schedule |
status | string | Pending, Running, Succeeded or Failed |
exit_code | integer or null | The command's exit code, once it has exited |
message | string | Why a run failed or is stuck, for example Exited with code 1, Stopped after running for 12 hours or The image could not be pulled. Empty otherwise |
created_at, started_at, finished_at | string | When the run was created, started and finished. The last two are null until then |
| Status | Message |
|---|---|
404 | This app is not a cron job, it has no runs! |
400 | Couldn't read the app's runs, try again! |
Run a cron job now
POST /apps/:hashed_id/runs
Starts a run of a cron job now, outside its schedule, on the version it has deployed. The schedule is not changed.
Access: Read & Write token.
curl -X POST https://cloider.com/api/apps/k3v9q/runs -H "Authorization: Bearer $CLOIDER_TOKEN"
{"name": "app-manual-1793185200"}
| Status | Message |
|---|---|
409 | A run is still going, wait for it to finish! |
409 | This app is paused. Resume it to run it!, This app has not been deployed yet!, This app is not a cron job, it has no runs! |
400 | Couldn't start the run, try again! |
Run a command with exec
POST /apps/:hashed_id/exec
Runs one command in the app's running container and streams back its output and exit code. cloider app exec uses it.
Access: Read & Write token. The app must have a running container.
| Field | Type | Required | Description |
|---|---|---|---|
command | string list | Yes | The program and its arguments, run directly, not through a shell. Up to 64 KiB, no NUL bytes |
stdin | string (base64) | No | Standard input, base64 encoded, up to 2 MiB |
timeout | integer | No | Seconds, default 600, at most 3600 |
curl -N -X POST https://cloider.com/api/apps/a8q2w/exec \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": ["sh", "-c", "ps aux | grep node"]}'
The response is the event stream described on cluster exec: base64 stdout and stderr chunks, then exit, timeout or error, then end. Errors before the stream starts:
| Status | Message |
|---|---|
400 | Give a command to run! (on command), and the size and timeout errors of cluster exec |
403 | This API token is read only! |
404 | App not found! |
409 | This app is paused. Resume it to run a command!, This app is being deleted!, This app has not been deployed yet!, This app has no running container right now! |
503 | The command could not be started, try again in a moment! |
Web terminal endpoints
The dashboard's Terminal tab opens a shell in the app's container. Three endpoints drive it; for scripts, exec is simpler.
| Endpoint | What it does | Response |
|---|---|---|
GET /apps/:hashed_id/terminal | Returns the terminal's URL | {"url": "https://cloider.com/terminal?kind=app&..."} |
POST /apps/:hashed_id/terminal/wake | Starts the terminal, which sleeps when unused, and answers once it accepts connections | 200 |
POST /apps/:hashed_id/terminal/heartbeat | Keeps the terminal awake while it is in use | 204 |
All three need a Read & Write token and a deployed, running app. Otherwise they answer 409 with This app is paused. Resume it to open a terminal!, This app is being deleted! or This app has not been deployed yet!. A read only token gets 403 Terminals need a Read & Write API token! for the URL. With an API token, the URL carries a terminal token valid for 2 minutes. See Terminal and exec.
Pause an app
POST /apps/:hashed_id/pause
Stops the app without losing anything: its image, environment and domains stay. The status goes to Pausing, then Paused.
Access: Read & Write token. The app must be Ready.
curl -X POST https://cloider.com/api/apps/a8q2w/pause \
-H "Authorization: Bearer $CLOIDER_TOKEN"
The response is the account's app list. Errors: 400 Only a running app can be paused!, 400 This app is building, wait for that build to finish before pausing it!, 403 for read only tokens and Restricted members.
Resume an app
POST /apps/:hashed_id/resume
Starts a paused app again on the image it was running. The status goes to Starting, then Ready.
Access: Read & Write token. The app must be Paused.
curl -X POST https://cloider.com/api/apps/a8q2w/resume \
-H "Authorization: Bearer $CLOIDER_TOKEN"
The response is the app list. A non paused app answers 400 Only a paused app can be resumed!.
Delete an app
DELETE /apps/:hashed_id
Deletes the app, its builds and its routes. Optionally deletes the free subdomains that were claimed for it in the same request. A custom domain is never deleted this way: it only loses its route to the app.
Access: Read & Write token.
| Field | Type | Required | Description |
|---|---|---|---|
delete_domains | string list | No | Free subdomains of this app to delete with it, for example ["blog.cloider.app"] |
curl -X DELETE https://cloider.com/api/apps/a8q2w \
-H "Authorization: Bearer $CLOIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"delete_domains": ["blog.cloider.app"]}'
The response is the app list, with this app Terminating. Its status then becomes Terminated. Deleting an app that is already being deleted answers 400 This app is already being deleted!. See Pause, resume and delete.
Related
- App builds: new versions, build logs and retries
- Git: connections, repositories and branches for imports
- Deploy from git and deploy from your computer
- cloider app and cloider deploy