# API

Everything the console does, it does over a public HTTP API. The same API backs the [MCP server](/deploy-with-ai/mcp-server), so anything an AI agent can do to your account, a script can do too.

Base URL:

```
https://api.light-cloud.com
```

Every path below is relative to it. All requests and responses are JSON; send `Content-Type: application/json` on anything with a body.

> [!NOTE]
> This API is stable enough to build on but is not versioned yet. Endpoints are added more often than they change, and breaking changes are announced on the [blog](https://blog.light-cloud.com) before they ship.

## Authenticating

Create an API key in the console - **left sidebar → your workspace name → API keys → New key** - and send it as a bearer token:

```
Authorization: Bearer lc_...
```

```bash
curl -X POST https://api.light-cloud.com/api/applications \
  -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"limit":100}'
```

The secret is shown once, at creation. Keys require a paid plan; the [MCP server](/deploy-with-ai/mcp-server) works on every account including free. See [Authentication](/reference/api/authentication) for access levels, expiry, revocation, and what a key deliberately cannot do.

## A worked example

Deploy a repository and wait for it to go live, start to finish. Each tab is self-contained — no SDK, no shared setup.

```codetabs
### curl
#!/usr/bin/env bash
set -euo pipefail

API=https://api.light-cloud.com
AUTH=(-H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" -H "Content-Type: application/json")

# 1. Let detection work out the build settings rather than guessing them
curl -sS -X POST "$API/api/applications/detect-framework" "${AUTH[@]}"   -d '{"owner":"acme","repo":"shop","branch":"main"}'

# 2. Create it. The production environment comes with it, and the first
#    build starts immediately.
app=$(curl -sS -X POST "$API/api/applications/create" "${AUTH[@]}" -d '{
  "name": "shop",
  "githubRepoUrl": "https://github.com/acme/shop",
  "githubBranch": "main",
  "deploymentType": "container",
  "framework": "nextjs",
  "runtime": "nodejs",
  "buildCommand": "npm run build",
  "environmentVars": { "NODE_ENV": "production" }
}')

APP_ID=$(echo "$app" | jq -r .id)

# 3. Poll until the build settles
while :; do
  status=$(curl -sS -X POST "$API/api/applications/status" "${AUTH[@]}"     -d "{"applicationId":"$APP_ID"}" | jq -r .status)
  case "$status" in
    healthy)         break ;;
    failed|degraded) echo "build failed"; exit 1 ;;
  esac
  sleep 10
done

curl -sS -X POST "$API/api/applications/get" "${AUTH[@]}"   -d "{"applicationId":"$APP_ID"}" | jq -r .url
# https://main-shop-acme.light-cloud.io

### Node
const API = "https://api.light-cloud.com";

// Everything this does is visible: POST, bearer token, JSON in, JSON out.
const api = async (path, body = {}) => {
  const res = await fetch(API + path, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  return res.json();
};

// 1. Let detection work out the build settings
const detected = await api("/api/applications/detect-framework", {
  owner: "acme",
  repo: "shop",
  branch: "main",
});

// 2. Create it — production environment and first build come with it
const app = await api("/api/applications/create", {
  name: "shop",
  githubRepoUrl: "https://github.com/acme/shop",
  githubBranch: "main",
  deploymentType: detected.deploymentType,
  framework: detected.framework,
  runtime: detected.runtime,
  buildCommand: detected.buildCommand,
  environmentVars: { NODE_ENV: "production" },
});

// 3. Poll until the build settles
const settled = ["healthy", "failed", "degraded"];
let status = app.status;

while (!settled.includes(status)) {
  await new Promise((resolve) => setTimeout(resolve, 10_000));
  ({ status } = await api("/api/applications/status", {
    applicationId: app.id,
  }));
}

if (status !== "healthy") throw new Error(`Build ${status}`);

const live = await api("/api/applications/get", { applicationId: app.id });
console.log(live.url);

### Python
import os
import requests

API = "https://api.light-cloud.com"

# Everything this does is visible: POST, bearer token, JSON in, JSON out.
def api(path, **body):
    res = requests.post(
        API + path,
        headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"},
        json=body,
    )
    res.raise_for_status()
    return res.json()

import time

# 1. Let detection work out the build settings
detected = api(
    "/api/applications/detect-framework",
    owner="acme", repo="shop", branch="main",
)

# 2. Create it — production environment and first build come with it
app = api(
    "/api/applications/create",
    name="shop",
    githubRepoUrl="https://github.com/acme/shop",
    githubBranch="main",
    deploymentType=detected["deploymentType"],
    framework=detected["framework"],
    runtime=detected.get("runtime"),
    buildCommand=detected.get("buildCommand"),
    environmentVars={"NODE_ENV": "production"},
)

# 3. Poll until the build settles
SETTLED = {"healthy", "failed", "degraded"}
status = app["status"]

while status not in SETTLED:
    time.sleep(10)
    status = api("/api/applications/status", applicationId=app["id"])["status"]

if status != "healthy":
    raise RuntimeError(f"Build {status}")

print(api("/api/applications/get", applicationId=app["id"])["url"])

### Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

// Everything this does is visible: POST, bearer token, JSON in, JSON out.
static String apiPost(String path, String json) throws Exception {
    var request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.light-cloud.com" + path))
        .header("Authorization", "Bearer " + System.getenv("LIGHT_CLOUD_API_KEY"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build();

    var response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString());

    if (response.statusCode() >= 400) throw new IllegalStateException(response.body());
    return response.body();
}

// 1. Detection
String detected = apiPost("/api/applications/detect-framework",
    "{"owner":"acme","repo":"shop","branch":"main"}");

// 2. Create
String created = apiPost("/api/applications/create", """
    {
      "name": "shop",
      "githubRepoUrl": "https://github.com/acme/shop",
      "githubBranch": "main",
      "deploymentType": "container",
      "framework": "nextjs",
      "runtime": "nodejs",
      "buildCommand": "npm run build",
      "environmentVars": { "NODE_ENV": "production" }
    }
    """);

String appId = jsonField(created, "id"); // your JSON library

// 3. Poll until the build settles
var settled = java.util.Set.of("healthy", "failed", "degraded");
String status = "pending";

while (!settled.contains(status)) {
    Thread.sleep(10_000);
    String body = apiPost("/api/applications/status",
        String.format("{"applicationId":"%s"}", appId));
    status = jsonField(body, "status");
}

if (!status.equals("healthy")) {
    throw new IllegalStateException("Build " + status);
}
```

There is no `targetOrganisationId` anywhere above: a key carries its own organisation. With a session token every body would need it.

## Deploying from CI

The case API keys exist for. Store the key as a repository secret, never in the workflow file.

```yaml
# .github/workflows/deploy.yml
name: Deploy to Light Cloud
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger deploy
        env:
          LIGHT_CLOUD_API_KEY: ${{ secrets.LIGHT_CLOUD_API_KEY }}
          APP_ID: ${{ vars.LIGHT_CLOUD_APP_ID }}
        run: |
          curl -sS --fail-with-body \
            -X POST https://api.light-cloud.com/api/applications/deploy \
            -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \
            -H "Content-Type: application/json" \
            -d "{\"applicationId\":\"$APP_ID\"}"
```

> [!TIP]
> If the repository is connected through the [GitHub App](/create/git-automation), pushes already deploy on their own and this workflow is redundant. Use the API when the trigger is something else - a release, a manual approval, a green run in another pipeline.

## Organisation scope

A key belongs to one organisation, so calls made with one need nothing extra - `targetOrganisationId` is inferred, and passing a different organisation's id is refused.

Sessions are not scoped that way. A person can belong to several organisations, so a call authenticated with a session token must name the one it means:

```json
{ "targetOrganisationId": "org_...", "limit": 100 }
```

Read your organisations from the profile endpoint:

```bash
curl https://api.light-cloud.com/api/auth/profile \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "id": "usr_...",
  "email": "you@example.com",
  "organisations": [
    { "id": "org_...", "name": "Acme Labs", "slug": "acme-labs", "role": "owner" }
  ]
}
```

Your role - or the key's access level - decides what the API lets you do. Each endpoint below names the permission it checks; see [Errors](/reference/api/errors#permissions) for the list.

## POST, mostly

The API is RPC-shaped rather than REST-shaped. Reads and writes are both `POST` with a JSON body, and the verb lives in the path:

```
POST /api/applications          list
POST /api/applications/get      read one
POST /api/applications/create   create
POST /api/applications/delete   delete
```

`GET` is used only where there is no body to send - the profile, the GitHub integration lookups, and [platform config](#platform-config). There is no `PATCH`, and `PUT` appears only under `/api/profile`.

> [!TIP]
> Resource ids are opaque strings. Never build one; always read it back from a list or create call.

## Responses

A successful call returns the resource itself, unwrapped:

```json
{ "id": "app_...", "name": "shop", "status": "healthy" }
```

List endpoints that paginate return an envelope:

```json
{ "items": [], "totalItems": 0, "totalPages": 0, "currentPage": 1 }
```

Failures return an HTTP error status and a `message`, sometimes with a `code`:

```json
{ "message": "Organisation ID is required." }
```

See [Errors](/reference/api/errors) for the status codes and what to do about each.

## Pagination

Endpoints that return an envelope accept these in the body:

| Field | Type | Default | Range |
| --- | --- | --- | --- |
| `page` | number | `1` | 1-100000 |
| `limit` | number | `10` | 1-100 |
| `filter` | string | none | free text, matched against the name |
| `sortColumn` | string | `created_at` | |
| `sortOrder` | string | `desc` | `asc` or `desc` |

Out-of-range values are clamped to the nearest legal one rather than rejected.

## Rate limits

5000 requests per 10 minutes per IP, across all endpoints. Standard `RateLimit-*` headers come back on every response; a breach is a `429`.

Sign-in and MFA verification are limited more tightly and answer `429` with a `Retry-After` header in seconds.

## CORS

Requests with no `Origin` - curl, CI runners, servers, CLIs - are always allowed. Browser requests are allowed only from origins on the platform allowlist, so a browser app you host yourself cannot call this API directly. Put your own backend in front of it.

## Health check

```endpoint
GET /api/health
> Whether the API is up. No authentication — useful as a reachability check before blaming your token.

response 200 | Service is healthy
{
  "status": "ok",
  "timestamp": "2026-08-28T13:31:20.268Z",
  "uptime": 5417.98
}

code curl
curl https://api.light-cloud.com/api/health

code Node
const res = await fetch("https://api.light-cloud.com/api/health");
const { status } = await res.json();

if (status !== "ok") throw new Error("Light Cloud API is not healthy");

code Python
import requests

res = requests.get("https://api.light-cloud.com/api/health", timeout=10)
res.raise_for_status()

if res.json()["status"] != "ok":
    raise RuntimeError("Light Cloud API is not healthy")
```

## Platform catalogue

Read these rather than hardcoding sizes, regions or tiers. The [Limits](/reference/limits) page is written from the same source, but the endpoint is the one that stays current.

```endpoint
GET /api/config/platform
> Regions, container sizes, database tiers and the rest of the catalogue the console fills its pickers from.

response 200 | Catalogue values
{
  "regions": [
    { "id": "europe-west1", "label": "Belgium", "tier": 1 },
    { "id": "us-central1", "label": "Iowa", "tier": 1 }
  ],
  "containerSizes": [
    { "id": "micro", "vcpu": 1, "memory": "512Mi", "maxConcurrency": 80 }
  ]
}

code curl
curl https://api.light-cloud.com/api/config/platform \
  -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY"

code Node
const res = await fetch("https://api.light-cloud.com/api/config/platform", {
  headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}` },
});

const { regions, containerSizes } = await res.json();

code Python
import os
import requests

res = requests.get(
    "https://api.light-cloud.com/api/config/platform",
    headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"},
)
res.raise_for_status()

catalogue = res.json()
```

```endpoint
GET /api/config/feature-flags
> Which optional features are on for your account. Some endpoints — the data explorer and database dumps — answer with a "not enabled" message when their flag is off, and this is where you check.

response 200 | Flags and their values
{
  "flags": ["database.shared-pool", "console.github-deploy-feedback"],
  "map": {
    "database.shared-pool": true,
    "console.gitlab": true,
    "console.bitbucket": true
  }
}

code curl
curl https://api.light-cloud.com/api/config/feature-flags \
  -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY"

code Node
const res = await fetch("https://api.light-cloud.com/api/config/feature-flags", {
  headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}` },
});

const { map } = await res.json();
if (!map["database.shared-pool"]) {
  console.log("Shared database pool is off for this account");
}

code Python
import os
import requests

res = requests.get(
    "https://api.light-cloud.com/api/config/feature-flags",
    headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"},
)
res.raise_for_status()

flags = res.json()["map"]
```

```endpoint
GET /api/config/cloudrun
> Container sizes and scaling ranges, with the memory and CPU combinations that are actually valid together.

response 200 | Container catalogue
{
  "sizes": [
    { "id": "micro", "memory": "512Mi", "cpu": "1", "maxConcurrency": 80 }
  ],
  "scaling": { "minInstances": [0, 5], "maxInstances": [1, 100] }
}

code curl
curl https://api.light-cloud.com/api/config/cloudrun \
  -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY"
```

There is also `GET /api/config/cloudrun/compatible?memory=<value>`, which narrows the CPU options to those valid for a given memory setting.

## Reference

- [Authentication](/reference/api/authentication): Sign in, refresh, and where tokens come from.
- [Applications](/reference/api/applications): Create, deploy, rename, move, delete, and domains.
- [Environments](/reference/api/environments): Branches, variables, scaling, logs, and metrics.
- [Deployments](/reference/api/deployments): Deploy history and rollback.
- [Databases](/reference/api/databases): Provision, connect, rotate, dump, and inspect.
- [Uploads](/reference/api/uploads): Deploy a folder without a Git repository.
- [GitHub](/reference/api/github): Installations, repositories, and branches.
- [Errors](/reference/api/errors): Status codes, error codes, and permissions.
