Integrating
Call the platform from your own code. Authentication, scopes, errors and limits.
The API is HTTP over JSON. Everything lives under /api on the same host as the dashboard.
https://clikdeploy.com/api
If you run your own instance, use your own host instead. The paths are the same.
A successful response is a JSON object with success: true and the result under data.
GET /api/deployments/dep_123
{
"success": true,
"data": {
"id": "cmd8x1a2b0000abcd",
"appId": "cmd7w0z1y0000wxyz",
"status": "RUNNING",
"createdAt": "2026-07-28T09:12:44.001Z"
}
}List endpoints add a pagination object next to data, and accept ?page= and ?pageSize= query parameters.
GET /api/apps?page=1&pageSize=25
{
"success": true,
"data": [ /* apps */ ],
"pagination": {
"page": 1,
"pageSize": 25,
"total": 7,
"totalPages": 1,
"hasNextPage": false,
"hasPreviousPage": false
}
}Every response carries an x-request-id header. Quote it if you ever need to report a problem — it is the id the platform logs the request under.
Every request is authenticated with a bearer credential in the Authorization header. There are two kinds.
An API key acts as you. Use it for scripts, CI jobs and anything that only touches your own resources.
Create one in the dashboard: open Settings from the account menu, go to the Developer tab, and generate a key. From a terminal, the CLI does the same thing.
clikdeploy user api-keys create "ci-pipeline"
A key looks like cd_live_ followed by 64 hex characters. Send it as a bearer token.
curl https://clikdeploy.com/api/apps \ -H "Authorization: Bearer cd_live_YOUR_KEY"
The key is shown once
If you are building an app that acts on behalf of other ClikDeploy users, you do not ask them for an API key. You send them through ClikOAuth, they approve a set of scopes, and your app receives an access token. That token goes in the same header, and works against the same endpoints — narrowed to the scopes the user approved.
The full authorization flow is on the ClikOAuth page.
A scope is a permission string that says which slice of the platform a credential can touch. It has two parts: an area, and either read or write.
apps:read list and inspect apps apps:write also create, change and deploy them
Write includes read. Granting deploy:write also grants deploy:read, so you never need to ask for both.
There are 50 scopes in total. Twelve of them are admin scopes (admin- prefix) for platform operators; they are never granted to an app that registered itself, and they are necessary but not sufficient — the account still has to hold the admin role. Four are the OpenID Connect identity scopes: openid, profile, email and offline_access.
Here are ten of the ones you are most likely to want.
| Scope | Lets the caller |
|---|---|
meta:read | Read the account profile, preferences, subscription and usage |
apps:read | List apps and read their config, env vars and marketplace entries |
apps:write | Create, update, delete, start/stop/restart apps — and trigger a deploy |
deploy:read | Read deployments and build logs |
deploy:write | Cancel, retry, roll back and promote deployments; manage deploy schedules |
servers:read | List servers, their capacity and their agent status |
servers:write | Provision, connect, resize, start and stop servers |
observe:read | Read logs, metrics, health, SLOs, status pages and activity |
domains:write | Buy domains and attach or detach them from apps |
integrations:read | List connected services, git providers and registry credentials |
The complete list is published in the discovery document, under scopes_supported, so you never have to keep a copy in sync.
curl https://clikdeploy.com/.well-known/oauth-authorization-server
The scope is derived from the path and the method: GET needs the read scope of whichever area owns the path, anything else needs the write scope. The area is the first part of the path, not the last — so a deploy triggered at POST /api/apps/{id}/deploy is an apps:write operation, while GET /api/deployments/{id} is deploy:read.
| Request | Scope required |
|---|---|
GET /api/apps | apps:read |
POST /api/apps/{id}/deploy | apps:write |
GET /api/apps/{id}/logs | apps:read |
GET /api/deployments/{id} | deploy:read |
GET /api/servers | servers:read |
A path that has no scope mapping is refused for an OAuth token rather than allowed by default. If you get a 403 on a path you expected to reach, that is why.
Nothing changes for you. An API key with no scope list attached is a full-access key, and every key issued before scopes existed is one. Scope checks are skipped entirely for those keys, so existing scripts, CI jobs and CLI installs keep working exactly as they did.
Keys you create today are full-access too. Scopes are how you hand out narrower access to other people's tools, through ClikOAuth — not something you have to configure to keep your own key working.
A browser session in the dashboard is also always full access.
Errors are JSON, and the HTTP status is the thing to branch on. Most come back as application/problem+json (RFC 7807).
401 Unauthorized
{
"type": "https://errors.clikdeploy.com/AUTH_REQUIRED",
"title": "Invalid API key",
"status": 401,
"code": "AUTH_REQUIRED",
"traceId": "0af7651916cd43dd8448eb211c80319c"
}A few endpoints answer with { "success": false, "error": "..." } instead, usually for a bad request body. Read the status code first and treat the body as detail.
| Status | What it means |
|---|---|
401 | The credential is missing, malformed, unknown or expired. Nothing about the request itself is wrong — fix the Authorization header. |
403 | You authenticated, but this credential is not allowed to do this. Either a scope is missing, or the resource is not yours. |
404 | No such resource under your account. |
429 | Rate limited. See below. |
When the 403 is a scope problem, the response carries a WWW-Authenticate header naming the scope to ask for. That is what lets an OAuth client send the user back to re-authorize instead of just failing.
403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
error_description="This request requires additional scope", scope="apps:write"
{
"type": "https://errors.clikdeploy.com/FORBIDDEN",
"title": "Insufficient scope. This request requires: apps:write",
"status": 403,
"code": "FORBIDDEN",
"traceId": "0af7651916cd43dd8448eb211c80319c"
}Limits are per user and are applied per route family, not to the API as a whole. These are the ones you are likely to meet.
| Operation | Limit |
|---|---|
General API | 100 requests per minute |
POST /api/apps/{id}/deploy | 60 deploys per hour |
POST /api/user/api-keys | 20 key creations per hour |
Rate-limited responses include the current state of your bucket, on both successful and refused requests.
X-RateLimit-Limit: 100 X-RateLimit-Remaining: 97 X-RateLimit-Reset: 1785318000
When you go over, you get a 429 with a Retry-After header in seconds. The body repeats it.
429 Too Many Requests
{
"success": false,
"error": "Rate limit exceeded. Please try again later.",
"code": "RATE_LIMIT_EXCEEDED",
"retryAfter": 34,
"reset": 1785318000
}Wait for Retry-After and retry. Other route families have their own limits; read the headers rather than assuming a number.
This is the whole flow — find an app, redeploy it, watch it finish, read its logs. Export your key once and every command below works as written.
export CLIK_KEY=cd_live_YOUR_KEY export CLIK_API=https://clikdeploy.com
curl -s "$CLIK_API/api/apps" \ -H "Authorization: Bearer $CLIK_KEY"
This doubles as a credential check: a 200 means the key is good. Each item in data is an app. The fields you need next are id, name, status and domain.
{
"success": true,
"data": [
{
"id": "cmd7w0z1y0000wxyz",
"name": "storefront",
"status": "RUNNING",
"domain": "storefront.example.com",
"port": 3000,
"serverId": "cmd6v9y0x0000stuv"
}
],
"pagination": { "page": 1, "pageSize": 25, "total": 1, "totalPages": 1,
"hasNextPage": false, "hasPreviousPage": false }
}status is one of RUNNING, STOPPED, DEPLOYING, RESTARTING, RECOVERING, ERROR or NOT_FOUND.
Post to the app's deploy endpoint. An empty body redeploys the app as it is configured today, which is what you usually want.
curl -s -X POST "$CLIK_API/api/apps/cmd7w0z1y0000wxyz/deploy" \
-H "Authorization: Bearer $CLIK_KEY" \
-H "Content-Type: application/json" \
-d '{}'The response is the new deployment row. Keep its id — that is what you poll.
{
"success": true,
"data": {
"id": "cmd8x1a2b0000abcd",
"appId": "cmd7w0z1y0000wxyz",
"status": "QUEUED",
"gitBranch": "main",
"createdAt": "2026-07-28T09:12:44.001Z"
},
"message": "Deployment started"
}Optional fields you can send in the body:
gitCommit — deploy a specific commit rather than the branch head.envVars — an object of string values, merged for this deploy.dockerImage — deploy a specific image instead of building from source.port — the port your app listens on, if it changed.pullImage — true to force a fresh pull of the image.curl -s "$CLIK_API/api/deployments/cmd8x1a2b0000abcd" \ -H "Authorization: Bearer $CLIK_KEY"
data.status moves from QUEUED to RUNNING and then settles on SUCCESS, FAILED, CANCELLED or ROLLED_BACK. On a failure, data.error holds the reason and data.errorCode a stable code.
Poll every 5 seconds until it settles
while true; do
STATUS=$(curl -s "$CLIK_API/api/deployments/cmd8x1a2b0000abcd" \
-H "Authorization: Bearer $CLIK_KEY" | jq -r .data.status)
echo "$STATUS"
case "$STATUS" in SUCCESS|FAILED|CANCELLED|ROLLED_BACK) break ;; esac
sleep 5
doneBuild output for the same deployment is at GET /api/deployments/{id}/logs.
Once the deployment succeeds, this returns the container's output as a single string. limit is the number of lines, default 2000, capped at 10000.
curl -s "$CLIK_API/api/apps/cmd7w0z1y0000wxyz/logs?limit=200" \ -H "Authorization: Bearer $CLIK_KEY"
{
"success": true,
"data": "2026-07-28T09:13:02Z listening on :3000\n..."
}If the container cannot be reached, the response carries "fallback": true and data is the stored deployment log replayed instead of live output. Check that flag before concluding the app is healthy.