Skip to main content

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​

MethodPathAccess
GET/appsRead
GET/apps/:hashed_idRead
GET/apps/:hashed_id/statusRead
GET/apps/:hashed_id/usageRead
POST/apps/sourceWrite
POST/apps/source/:token/detectWrite
GET/apps/source/:tokenRead
POST/apps/source/githubWrite
POST/apps/source/gitlabWrite
POST/apps/source/bitbucketWrite
POST/apps/source/imageWrite
POST/appsWrite
PUT/apps/:hashed_id/settingsWrite
PUT/apps/:hashed_id/gitWrite
PUT/apps/:hashed_id/imageWrite
POST/apps/:hashed_id/deploy-hookWrite
POST/apps/:hashed_id/deploy-hook/rotateWrite
POST/deploy-hooks/apps/:hashed_id/:secretNone, the URL is the credential
GET/apps/:hashed_id/envRead
PUT/apps/:hashed_id/envWrite
GET/apps/:hashed_id/env/valueRead
GET/apps/:hashed_id/build-envRead
PUT/apps/:hashed_id/build-envWrite
GET/apps/:hashed_id/build-env/valueRead
GET/apps/:hashed_id/build-argsRead
PUT/apps/:hashed_id/build-argsWrite
GET/apps/:hashed_id/build-args/valueRead
GET/apps/:hashed_id/logs/streamRead
GET/apps/:hashed_id/runsRead
POST/apps/:hashed_id/runsWrite
POST/apps/:hashed_id/execWrite
GET/apps/:hashed_id/terminalWrite
POST/apps/:hashed_id/terminal/wakeWrite
POST/apps/:hashed_id/terminal/heartbeatWrite
POST/apps/:hashed_id/pauseWrite
POST/apps/:hashed_id/resumeWrite
DELETE/apps/:hashed_idWrite

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"
}
FieldTypeDescription
hashed_idstringThe app's id
app_namestringThe name you gave it
app_labelstringA name unique among the account's apps (-1, -2 appended when taken)
portintegerThe port the app listens on. It is passed to the app as the PORT environment variable
start_commandstringThe 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
builderstringHow the live version was built: dockerfile, buildpacks or static. Empty for an image app, and for a version built before this was recorded
schedulestringA 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_atstring or nullWhen a cron job runs next. null for any other app, and for a paused cron job
statusstringSee App statuses
language, frameworkstringWhat detection found in the source, for display only
source_filename, source_sizestring, integerThe source archive of the live build
uploadedbooleanWhether the app's source has arrived
git_providerstringGitHub, GitLab or Bitbucket. Only meaningful when repo_full_name is set
repo_full_name, repo_url, repo_branchstringThe repository and branch the app deploys from. Empty for an uploaded app
auto_deploybooleanWhether a push to repo_branch builds a new version
git_connectedbooleanWhether the git connection the repository is read through still exists
image, image_tagstringThe image and tag an image app runs. Empty for an app built from source
image_usernamestringThe username an image app's image is pulled with, empty for a public image. The token is never returned
image_refstringThe image the app is running. Empty until a build has deployed
dockerfile_argsobject listThe ARGs the source's Dockerfile declares, each with a name and a default (or null)
versionstringThe version of the build being served. Empty until the first deploy
urlstringThe address the app is served on, or empty when no domain points to it
domainsobject listEvery domain the app is served on, with its domain_type (Subdomain or CustomDomain)
created_atstringWhen 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.

StatusMeaning
PendingWaiting for the source to be uploaded
Queued, Building, Deploying, StartingA build is running through its stages
ReadyRunning and serving its URL, or for a cron job, deployed and running on its schedule
FailedThe first build failed. An app that was already running stays Ready when a later build fails
Pausing, PausedBeing stopped, or stopped
Terminating, TerminatedBeing 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.

FieldTypeRequiredDescription
source.namestringYesThe archive's file name, for example blog.zip
source.typestringYesMust be application/zip
source.sizeintegerYesThe 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)}}"
Response
{
"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"
Response
{
"status": "Done",
"app_name": "blog",
"language": "JavaScript",
"framework": "Next.js",
"port": 3000,
"message": "",
"build_args": [{"name": "NEXT_PUBLIC_API_URL", "default": null}]
}
FieldDescription
statusPending (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_nameA name that passes the naming rules, from the project's own name or the archive's
language, frameworkWhat was detected, possibly empty
portThe detected port, or 0 when there is no opinion
messageWhy detection failed or the import is unavailable
build_argsThe ARGs declared by a Dockerfile in the source
gitOnly 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.

FieldTypeRequiredDescription
app_namestringYes2 to 64 characters: lower case letters, digits and -, starting with a letter, not ending with -
portintegerYes, unless schedule is set1 to 65535, the port the app listens on
source_tokenstringYesThe token from step 1, or from an import
start_commandstringNoOne 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
schedulestringNoMakes 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
subdomainstringNoA full free subdomain to claim, for example blog.cloider.app. Empty for none
env_varsobject listNoRuntime environment variables, see Variables
build_env_varsobject listNoBuild variables
build_argsobject listNoDockerfile 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}
]
}'
Response
{
"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.

StatusFieldMessage
400app_nameApp 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
400portApp port must not be empty, App port must be between 1 and 65535
400start_commandStart command must be at most 1000 characters, Start command must be a single line
400scheduleA 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
400subdomainA cron job has no port to serve a domain on, leave this empty.
400sourceYour 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
400subdomainThis 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
400env_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.

FieldTypeRequiredDescription
installation_hashed_idstringYesThe connection's hashed_id from GET /git/<provider>/installations
repo_full_namestringYes for GitHub and Bitbucketowner/repo (GitHub) or workspace/repo (Bitbucket). For GitLab, a display hint
project_idintegerYes for GitLabThe GitLab project's numeric id
branchstringYesThe 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"}'
Response
{"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.

StatusFieldMessage
400sourceThat 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.

FieldTypeRequiredDescription
imagestringYesThe 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
tagstringNoThe tag, latest when empty. Digests aren't accepted
usernamestringNoThe registry login's username, for a private image
passwordstringWith usernameThe 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"'"}'
Response
{
"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.

StatusFieldMessage
400imageEnter the image to deploy.
400imageghcr.io/acme/api does not exist or is private. If it is private, add a login for its registry.
400imageThis image has no linux/amd64 build, which is what apps run on. Build it with --platform linux/amd64.
400imageThis registry is not on a public address, apps can only pull from public registries.
400tagghcr.io/acme/api has no tag v1.
400usernameEnter the username this token belongs to.
400passwordEnter 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.

FieldTypeRequiredDescription
imagestringYesThe image, as for POST /apps/source/image
tagstringNoThe tag, latest when empty
usernamestringNoThe login's username. Empty pulls without a login
passwordstring or nullNoThe 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}'
Response
{"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"
Response (202)
{"deployed": true, "build_hashed_id": "u9e3k", "version": "4", "image_ref": "ghcr.io/acme/api@sha256:9c1f3a..."}
StatusMeaning
202A new version is deploying
200{"deployed": false}: the tag still points at the running image
404The app or the secret is wrong, or the URL was regenerated
409The app is already deploying, is paused or being deleted, or the account is suspended
422The 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.

FieldTypeRequiredDescription
portintegerYes, unless the app is a cron job1 to 65535. Send the current port to keep it. A cron job ignores it
start_commandstringNoOne line, up to 1000 characters. Empty clears it
schedulestringNoA 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"}'
Response
{"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).

StatusMessage
400This app is paused. Resume it before deploying a new version!, This app is being deleted!
400This app is building, wait for that build to finish before changing its settings!
400The 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.

FieldTypeRequiredDescription
branchstringYesThe branch name
auto_deploybooleanYestrue 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:

ListEndpointsRead byApplied
Environment (runtime)/apps/:hashed_id/envThe running appSaving restarts the app
Build variables/apps/:hashed_id/build-envBuildpack builds (no Dockerfile)Saving starts a build of the current source
Build arguments/apps/:hashed_id/build-argsDockerfile builds, as ARG (secret ones as build secrets)Saving starts a build of the current source

Each variable is an object:

FieldTypeDescription
keystring1 to 128 characters, a letter or _ first, then letters, digits and _
valuestring or nullThe value, up to 16 KB. In a read, null for a secret. In a write, null keeps the value already stored
secretbooleantrue 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"
Response
[
{"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"
Response
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.

FieldTypeRequiredDescription
env_varsobject listYesThe 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}
]
}'
Response
{"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}]}'
Response
{"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 parameterRequiredDescription
linesNoHow many past lines to start with: 100, 200, 500 or 1000. Default 200
followNofalse to send the past lines and end. Default: follow
runNoA 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"
Response
: 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

EventMeaning
message (lines with no event:)Log output. A chunk can span several data: lines; join them with newlines to get exactly what was printed
resetA fresh tail starts. Sent first on every connection: drop what you showed from an earlier connection
endThe 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
logs.mjs
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"
Response
[
{
"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"
}
]
FieldTypeDescription
namestringThe run's name, which its logs are read by
manualbooleanStarted with Run now rather than by the schedule
statusstringPending, Running, Succeeded or Failed
exit_codeinteger or nullThe command's exit code, once it has exited
messagestringWhy 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_atstringWhen the run was created, started and finished. The last two are null until then
StatusMessage
404This app is not a cron job, it has no runs!
400Couldn'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"
Response
{"name": "app-manual-1793185200"}
StatusMessage
409A run is still going, wait for it to finish!
409This 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!
400Couldn'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.

FieldTypeRequiredDescription
commandstring listYesThe program and its arguments, run directly, not through a shell. Up to 64 KiB, no NUL bytes
stdinstring (base64)NoStandard input, base64 encoded, up to 2 MiB
timeoutintegerNoSeconds, 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:

StatusMessage
400Give a command to run! (on command), and the size and timeout errors of cluster exec
403This API token is read only!
404App not found!
409This 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!
503The 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.

EndpointWhat it doesResponse
GET /apps/:hashed_id/terminalReturns the terminal's URL{"url": "https://cloider.com/terminal?kind=app&..."}
POST /apps/:hashed_id/terminal/wakeStarts the terminal, which sleeps when unused, and answers once it accepts connections200
POST /apps/:hashed_id/terminal/heartbeatKeeps the terminal awake while it is in use204

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.

FieldTypeRequiredDescription
delete_domainsstring listNoFree 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.