> ## Documentation Index
> Fetch the complete documentation index at: https://docs.techulus.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Public API reference

> Automate service configuration, deployments, and observability.

The public API is available under `/api/v1`. The `tc` CLI uses this API directly.

## Authentication

Send your CLI API key in the `X-API-Key` header on every resource request:

```bash theme={null}
curl https://cloud.example.com/api/v1/me \
  --header "X-API-Key: $TECHULUS_API_KEY"
```

Browser cookies and bearer tokens cannot access resource endpoints. A bearer token or browser session can only create an API key with `POST /api/v1/api-keys`. An API key cannot create another API key.

`tc auth login` completes the device flow, creates an API key, and stores it locally. `tc auth logout` only removes the local credentials.

### Create an API key

```http theme={null}
POST /api/v1/api-keys
Authorization: Bearer <device-access-token>
Content-Type: application/json
```

```json theme={null}
{
  "name": "CLI workstation",
  "metadata": {
    "machineName": "workstation",
    "platform": "darwin/arm64"
  }
}
```

`name` is required and can contain up to 32 characters. The response contains the secret once:

```json theme={null}
{
  "apiKey": "tcl_...",
  "keyId": "...",
  "name": "CLI workstation"
}
```

## Authorization and service identity

Roles are global for the current Techulus Cloud installation. There are no project-level permissions.

* `reader`, `developer`, and `admin` can read resources.
* `developer` and `admin` can change configuration and deploy services.

Service operations use the globally unique service ID:

```text theme={null}
/api/v1/services/{serviceId}
```

Nested project and environment paths remain available for browsing collections. Service responses include an authoritative `target` with the containing project, environment, and service labels.

## Errors

Every API error uses the same shape:

```json theme={null}
{
  "message": "Resource not found",
  "code": "NOT_FOUND"
}
```

Use `code` for automation. Do not match the human-readable `message`.

## Identity and collections

| Method | Path | Description |
| - | - | - |
| `GET` | `/api/v1/me` | Return the API-key user, role, and key-backed session |
| `GET` | `/api/v1/projects` | List projects |
| `GET` | `/api/v1/projects/{projectId}/environments` | List environments in a project |
| `GET` | `/api/v1/projects/{projectId}/environments/{environmentId}/services` | List services in an environment |
| `GET` | `/api/v1/services/{serviceId}` | Return the target and canonical service configuration |

Collection responses use an opaque keyset cursor:

```json theme={null}
{
  "services": [
    {
      "id": "...",
      "name": "web",
      "hostname": "web",
      "source": { "type": "image", "image": "nginx:1.27" },
      "createdAt": "2026-07-20T00:00:00.000Z"
    }
  ],
  "nextCursor": null
}
```

Pass `nextCursor` as `?cursor=...`. `limit` defaults to 100 and accepts values from 1 through 100.

## Service resources

The paths in this table are relative to `/api/v1/services/{serviceId}`.

| Method | Path suffix | Description |
| - | - | - |
| `GET`, `PUT` | `/configuration` | Read safe current/active configuration or atomically replace all managed configuration |
| `POST` | `/configuration/plan` | Validate and plan a complete managed configuration replacement |
| `GET` | `/status` | Read source, latest build and rollout, and persisted deployments |
| `POST` | `/deploy` | Queue an image rollout or GitHub build |
| `GET` | `/logs` | Search logs and optionally long poll |
| `GET` | `/rollouts` | List rollouts with cursor pagination |
| `GET` | `/rollouts/{rolloutId}` | Read a contained rollout and its deployments |
| `GET` | `/rollouts/{rolloutId}/logs` | Search bounded rollout logs |
| `GET` | `/builds` | List GitHub builds with cursor pagination |
| `GET` | `/metrics` | Read metrics with explicit provider state |
| `GET` | `/revisions` | Read a redacted configuration changelog |

Rollout and build collections accept `limit` from 1 through 100 and an opaque `cursor`. Their default limit is 25. Revisions accept their returned opaque `cursor` and return up to 25 items.

## Configuration

`GET /configuration` returns these distinct states:

* `current` is the mutable configuration stored for the service.
* `active` is the immutable specification used by the active deployment.
* `activeRevisionId` and `activeDeploymentId` identify that active state.
* `hasPendingChanges` and `changes` compare current and active state.
* `management.patchable` and `management.blockers` explain whether the API can safely manage the service.

A GitHub service keeps mutable repository settings in `current.source`. Its `active.source` comes from the immutable service revision used by the active deployment. The public projection includes the snapshotted repository, branch, and root directory. It omits repository IDs and authentication details.

Configuration and revision responses never include secret names, values, or ciphertext.

### Replace configuration

`POST /configuration/plan` accepts the same complete desired configuration as `PUT /configuration`. It performs no writes and returns the authoritative `target`, `action`, `currentVersion`, `desiredVersion`, and structured `changes` (`field`, `from`, and `to`). Service IDs on these flat service routes are authorized installation-globally rather than being scoped by a project ID in the URL.

`PUT /configuration` is atomic and replaces the complete managed configuration. Send the `currentVersion` returned by the plan as one quoted strong ETag (for example, `If-Match: "sha256:…"`). Missing, unquoted, multiple, weak, or otherwise invalid headers are rejected; a service change after planning returns `409 CONFIGURATION_PLAN_STALE`. A successful response returns the authoritative target and the structured change set that was applied. The request must contain exactly `name`, `source`, `hostname`, `ports`, `placement`, `healthCheck`, `startCommand`, `resources`, and `crons`. Omitted or unknown fields are rejected. `hostname` must be concrete and non-null. Use `null` to clear nullable fields, including `resources`.

Replacing the final public HTTP port with a domain automatically disables serverless in the mutable service configuration. The plan reports this side effect as a `serverless.enabled` change before apply. You do not send serverless settings in the replacement request.

```json theme={null}
{
  "name": "web",
  "source": {
    "type": "github",
    "repository": "https://github.com/techulus/cloud",
    "branch": "main",
    "rootDir": "web"
  },
  "hostname": "cloud",
  "ports": [
    {
      "containerPort": 3000,
      "public": true,
      "domain": "cloud.example.com"
    }
  ],
  "placement": {
    "mode": "automatic",
    "replicas": 3
  },
  "healthCheck": {
    "cmd": "curl --fail http://localhost:3000/health",
    "interval": 10,
    "timeout": 5,
    "retries": 3,
    "startPeriod": 30
  },
  "startCommand": null,
  "resources": {
    "cpuCores": 1,
    "memoryMb": 512
  },
  "crons": [
    {
      "path": "/api/cron/digest",
      "schedule": "0 5 * * *"
    }
  ]
}
```

`crons` is the complete desired list of UTC service cron definitions. Each item contains exactly `path` and `schedule`. Paths must be unique, origin-relative paths without a query string or fragment. Send an empty list to remove all cron definitions. `current.crons` returns definitions only; runtime status remains outside the configuration fingerprint. Cron-only changes take effect without creating a deployment revision. See [Service crons](/services/configuration#service-crons) for request and secret behavior.

The `tc apply` command sends this complete replacement. A linked `techulus.yml` stores its target identity under `target.serviceId` and desired configuration under `service`. `tc link` writes current cron definitions into the manifest, and `tc apply` treats that manifest list as authoritative. Explicit CLI targeting uses `--service <serviceId>`. During interactive `tc link`, a project with exactly one environment selects it automatically; zero environments is an error and multiple environments prompt for a choice.

The API supports these source variants:

```json theme={null}
{ "type": "image", "image": "nginx:1.27" }
```

```json theme={null}
{
  "type": "github",
  "repository": "https://github.com/owner/repository",
  "branch": "main",
  "rootDir": "services/api"
}
```

GitHub repository URLs are canonical HTTPS `github.com` URLs. `rootDir` is required and must either stay inside the repository or be `null` to clear an existing build root. Source conversion and GitHub repository switching are not supported.

Use automatic placement for stateless services:

```json theme={null}
{ "mode": "automatic", "replicas": 3 }
```

To autoscale an eligible service, replace fixed `replicas` with a range:

```json theme={null}
{
  "mode": "automatic",
  "autoscaling": { "minReplicas": 2, "maxReplicas": 8 }
}
```

Autoscaling requires automatic placement, a stateless non-serverless service
without volumes, and both CPU and memory limits. Minimum and maximum values must
be from 1 through 32, with minimum no greater than maximum. CPU and memory use
fixed 60% targets. Configuration responses expose the mutable policy in
`current.placement` and the deployed policy plus concrete active target in
`active.placement`. Policy changes participate in plan/apply fingerprints.
To disable autoscaling, send the fixed automatic-placement form with `replicas`.

Use manual placement to choose exact servers:

```json theme={null}
{
  "mode": "manual",
  "placements": [
    { "serverId": "server-id", "count": 2 }
  ]
}
```

Manual placement requires online servers with WireGuard configured. Serverless services require proxy servers, including when automatic placement is used. A replacement that removes the final public HTTP domain can move the service to worker placement because the same update disables serverless. Automatic placement is not available for stateful or volume-backed services. Submit replica changes through `placement`; the API rejects a top-level `replicas` field.

CLI manifests remain fixed-replica only in V1. Configure autoscaling through the
dashboard or public configuration API. `tc apply` continues to send the fixed
replica manifest contract.

The API only manages stateless services with HTTP ports. Existing volumes, stateful mode, TCP or UDP ports, TLS passthrough, or invalid resource limits return a conflict with an actionable code.

## Deployments and builds

`POST /deploy` uses the persisted service source. It does not apply configuration first.

* An image service queues a rollout.
* A GitHub service queues a build. A rollout starts after a successful build.

Deploy responses are always HTTP 202:

```json theme={null}
{
  "operation": "rollout",
  "status": "queued",
  "rolloutId": "...",
  "buildId": null
}
```

Image migration can return `status: "migration_started"` and `rolloutId: null`. GitHub returns:

```json theme={null}
{
  "operation": "build",
  "status": "build_queued",
  "rolloutId": null,
  "buildId": null
}
```

Build records are created asynchronously, so `buildId` is initially `null`. `/status` reports `latestBuild` and `latestRollout` independently.

Each GitHub deployment resolves an exact 40-character commit before it queues build work. The control plane creates an immutable service revision that snapshots the service configuration, source provenance, and reserved artifact identity. Every platform build references that revision and reads its repository, branch, commit, root directory, authentication mode, secrets, and final image URI from the revision.

After all platform images succeed, the control plane creates the final manifest and rolls out that same revision. Configuration changes made while a build is running do not alter its inputs. Retrying a failed or cancelled build creates a new revision with a new artifact identity. It never overwrites an artifact reserved by an earlier revision.

## Logs

Service logs accept these query parameters:

| Parameter | Description |
| - | - |
| `q` | Literal, case-insensitive message search; maximum 200 characters |
| `range` | `1h`, `6h`, `24h`, or `7d`; defaults to `24h` |
| `limit` or `tail` | 1 through 1,000 records; defaults to 100 |
| `cursor` | Continue after the last emitted record by using the opaque `nextCursor` value |
| `wait` | Long-poll for 0 through 20 seconds when `cursor` is present |

Round-trip `nextCursor` without decoding or modifying it. When `hasMore` is `true`, request the next page immediately. Otherwise, respect `pollAfterMs` before the next request. Keep the service, `q`, and `range` unchanged while reusing a cursor.

```json theme={null}
{
  "provider": "enabled",
  "logs": [
    {
      "deploymentId": "...",
      "stream": "stdout",
      "message": "server started",
      "timestamp": "2026-07-20T00:00:00.123456789Z"
    }
  ],
  "nextCursor": "eyJ2IjoxLCJ0IjoiLi4uIiwiZSI6Ii4uLiJ9",
  "hasMore": false,
  "pollAfterMs": 250
}
```

When logging is not configured, the response uses `provider: "disabled"` with an empty list. An upstream failure returns `502 LOG_PROVIDER_ERROR`. Log following returns `409 LOG_CURSOR_UNAVAILABLE` if an agent must be upgraded before it can provide deterministic cursors.

Rollout logs accept `q` and `limit`. Their response includes bounded stage messages for the contained rollout.

## Metrics

Metrics accept `range=1h|6h|24h|7d|30d`.

* A configured provider returns `{ "provider": "enabled", "metrics": ... }`. `metrics` can contain successful empty series.
* A disabled provider returns `{ "provider": "disabled", "metrics": null }`.
* An upstream failure returns `502 METRICS_PROVIDER_ERROR`.

The endpoint exposes fixed service metrics. It does not accept raw PromQL.

## CLI commands

The CLI uses the same endpoints documented above:

| Command | Purpose |
| - | - |
| `tc config` | Show current and active service configuration |
| `tc projects` | List projects |
| `tc environments` | List project environments |
| `tc services` | List environment services |
| `tc status` | Show build, rollout, and deployment status |
| `tc logs -q <text>` | Search or follow service logs |
| `tc rollouts` | List rollout history |
| `tc rollout <id>` | Show rollout detail |
| `tc rollout logs <id>` | Fetch rollout logs |
| `tc builds` | List builds when the source supports them |
| `tc metrics` | Query service metrics |
| `tc revisions` | List the redacted revision changelog |

`tc link` stores the selected `target.serviceId` and current managed configuration, including cron definitions, in `techulus.yml`. Relinking a manifest to a different service requires removing `target.serviceId` first. `tc apply` always displays the server-generated plan and prompts before writing; use `tc apply --yes` for noninteractive automation. Image and GitHub services use the same `tc link`, `tc apply`, `tc deploy`, and inspection commands.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.