Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 131 additions & 17 deletions api/getting-started.mdx
Original file line number Diff line number Diff line change
@@ -1,39 +1,153 @@
---
title: Getting Started
description: Introduction to the LocalOps API
description: Authentication, response format and error handling for the LocalOps API
---

## Overview

Welcome to the LocalOps API documentation. This API allows you to programmatically manage your environments, services,
and resources.
The LocalOps API lets you manage environments, services, deployments, custom domains and secrets programmatically - from
the LocalOps SDK, a CLI wrapper or your CI pipeline.

It is a small, deliberately separate surface from the LocalOps console. Every operation the API supports is listed under
**Endpoints** in the sidebar.

<Note>
An **environment** is also referred to as a *Space* in the console. The `envId` path parameter is the environment's
unique identifier.
</Note>

## Base URL

```
https://sdk.localops.co
```

## Authentication

All API requests require authentication using an API key. Include your API key in the request headers:
Every endpoint except `GET /health` requires your account API token, sent as a bearer token:

```bash
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://sdk.localops.co/v1/environments
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://sdk.localops.co/v1/deployments/3c7a4d81-0000-0000-0000-000000000000
```

## Base URL
The token is issued per account. Owners and admins can read it from the LocalOps console.

The base URL for all API requests is:
A missing header, a non-`Bearer` header, a malformed token, or a token that matches no account are all rejected the same
way:

```json
{
"error_code": "unauthorized",
"message": "You are not authorized to do this action"
}
```
https://sdk.localops.co/v1

Your account also needs an active plan. If your subscription is not `active` or `past_due`, requests are rejected with
`403` and `You don't have an active plan`. Accounts without a subscription, such as BYOC accounts, pass this check.

<Warning>
The API token is the tenancy boundary for every request. Environment, service, deployment and custom domain
identifiers are always looked up within your account, so an identifier belonging to another account behaves exactly
like one that does not exist.
</Warning>

### Attribution in audit logs

API requests are not tied to a user. [Audit log](/team/audit-logs) entries for actions taken through the API show
**`API Token`** as the actor, and deployments created this way have a zero UUID in `created_by_id`.

Service creates, updates and deletes, secret updates, and custom domain creates and verifications are all written to the
audit log.

## Response format

Successful responses are wrapped in an envelope:

```json
{
"message": "success",
"data": {
"...": "endpoint specific payload"
}
}
```

## Response Format
There are two exceptions:

All API responses are returned in JSON format. Successful responses have a `200` status code, while errors return
appropriate HTTP status codes with error details in the response body.
- `GET /health` returns `{ "message": "ok" }`
- `DELETE /v1/environments/{envId}/services/{serviceId}` returns `202 { "message": "accepted" }`

## Errors

- `401 Unauthorized`: The API key is invalid or missing.
- `403 Forbidden`: The API key is not authorized to access the requested resource.
- `404 Not Found`: The requested resource does not exist.
- `429 Too Many Requests`: The API key has exceeded the rate limit.
- `500 Internal Server Error`: An unexpected error occurred on our servers.
Errors are **not** wrapped in the `data` envelope:

```json
{
"error_code": "validation",
"message": "Invalid data",
"errors": [{ "field": "replica_count", "error": "replica_count is a required field" }]
}
```

The `errors` array is present only on validation failures, and can be `null` when the failure has no field level detail.

| Status | `error_code` | When |
| ------ | -------------- | ------------------------------------------------------------------------------------------------------------------- |
| `401` | `unauthorized` | Missing, malformed or unknown API token |
| `403` | `forbidden` | Inactive plan, a deploy on a service being deleted, or a plan without preview environments |
| `404` | `notfound` | Unknown environment, service, deployment, custom domain, connection or commit |
| `409` | `conflict` | Deleting a protected service |
| `422` | `validation` | Field validation failures, a missing or malformed body, duplicate secret keys, a missing image tag or chart version |
| `500` | `unknown` | Unexpected failure. Message: `Something went wrong. Please try again later` |
| `500` | `validation` | A business rule violation - see below |

<Warning>
Check `error_code` rather than the HTTP status to decide whether a request was at fault. Some business rule
violations, such as `Ops Json is only supported for docker image source` or `Enable Preview is allowed only for
service type web`, are returned as `500` with `error_code: validation`.
</Warning>

### Always send a body

Endpoints that accept a request body reject a zero byte body with `422`:

```json
{ "error_code": "validation", "message": "Please check your request body" }
```

Send at least `{}`. This matters most when deploying the latest commit on a git service's configured branch, where there
is nothing else to send.

### Rate limits

There are no rate limits on the API today.

## Asynchronous work

Deployments, service deletes, secret writes and custom domain deploys all hand off to the provisioner. The HTTP response
confirms only that the request was accepted and passed synchronous validation.

Observe the real outcome by polling:

- deployment state, with `GET /v1/deployments/{deploymentId}`
- service state, with `GET /v1/environments/{envId}/services/{serviceId}`
- custom domain state, with `GET /v1/environments/{envId}/services/{serviceId}/custom-domains`

There are no idempotency keys. Retrying a deploy creates another deployment. Retrying a preview deploy for the same pull
request reuses the existing preview service, indicated by `is_new: false`, but starts another rollout.

## No list endpoints

The API has no "list services" or "list deployments" endpoint. Keep the identifiers returned when you create a service
or trigger a deployment, or read them from the console.

## Next steps

<Card title="Common workflows" icon="route" href="/api/workflows">
End to end recipes for creating and deploying a service, previewing pull requests, wiring services together and
attaching custom domains.
</Card>

The full endpoint reference, with request and response schemas and a live playground, is under **Endpoints** in the
sidebar.
199 changes: 199 additions & 0 deletions api/workflows.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
---
title: Common workflows
description: End to end recipes for the LocalOps API
---

These recipes chain the endpoints in the sidebar. All of them assume the `Authorization: Bearer <api_token>` header
described in [Getting Started](/api/getting-started), and a base URL of `https://sdk.localops.co`.

## Create a Helm service and deploy it

<Steps>
<Step title="Create the service" icon="plus">
`POST /v1/environments/{envId}/services` with `source: "helm_chart"` and `deploy_now: false`.

Keep `deploy_now` off for Helm services. It deploys without a chart version, so an explicit deploy is almost always
what you want.

```bash
curl -X POST https://sdk.localops.co/v1/environments/$ENV_ID/services \
-H "Authorization: Bearer $LOCALOPS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "checkout api",
"type": "web",
"source": "helm_chart",
"port": 8080,
"helm_chart_repo": "https://charts.example.com",
"helm_chart_name": "checkout",
"helm_values_yml": "replicaCount: 2\n",
"deploy_now": false
}'
```

Keep `data.service.id` from the response - there is no list endpoint to look it up again.

</Step>
<Step title="Write service secrets" icon="key">
`PUT /v1/environments/{envId}/services/{serviceId}/secrets`, if the service needs any.
</Step>
<Step title="Deploy a chart version" icon="rocket">
`POST /v1/environments/{envId}/services/{serviceId}/deploy` with `{"helm_chart_version": "1.4.0"}`.

Keep `data.deployment_id` from the response.

</Step>
<Step title="Poll the deployment" icon="arrows-rotate">
`GET /v1/deployments/{deploymentId}` until `state` is `success` or `failed`.
</Step>
</Steps>

<Note>
Service names on create accept letters and spaces only. Digits, hyphens and underscores are rejected. Updates are
laxer and also allow hyphens.
</Note>

## Deploy a git service from CI

For a GitHub or GitLab service, deploy the head of the configured branch by sending an empty object:

```bash
curl -X POST https://sdk.localops.co/v1/environments/$ENV_ID/services/$SERVICE_ID/deploy \
-H "Authorization: Bearer $LOCALOPS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```

Or pin an exact commit, with an optional note of up to 300 characters:

```json
{ "commit_id": "9f2c1ab", "note": "release 1.4.0" }
```

Then poll `GET /v1/deployments/{deploymentId}`. The deployment row is created synchronously, so you always get an id
back immediately even though the rollout itself runs in the background - a rollout failure shows up as
`state: "failed"`, not as an error on the deploy call.

For a `docker_image` service, send `docker_image_tag` instead. For `helm_chart`, send `helm_chart_version`.

## Deploy a pull request preview

<Steps>
<Step title="Check the parent service" icon="circle-check">
The parent service must use the `github` source, be of type `web`, and have `enable_previews` turned on. Your plan
must include preview environments, otherwise the call returns `403`.

Turn previews on with `PATCH /v1/environments/{envId}/services/{serviceId}` and `{"enable_previews": true}`.

</Step>
<Step title="Trigger the preview deploy" icon="code-pull-request">
```bash
curl -X POST https://sdk.localops.co/v1/environments/$ENV_ID/services/$SERVICE_ID/deploy \
-H "Authorization: Bearer $LOCALOPS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "preview": true, "commit_id": "9f2c1ab", "branch": "feat/checkout-v2", "pr_number": 42 }'
```

`commit_id`, `branch` and `pr_number` are all required in preview mode, and `docker_image_tag` and
`helm_chart_version` must be absent.

</Step>
<Step title="Use the returned preview service" icon="link">
The response carries `service_id`, `service_name`, `origin`, `pr_number` and `is_new` - not a deployment id.

A preview service is created for the pull request the first time (`is_new: true`) and reused on later calls
(`is_new: false`). It inherits the parent's type, resources, port and node group, and has `auto_deploy` turned on.

</Step>
<Step title="Poll the preview service" icon="arrows-rotate">
`GET /v1/environments/{envId}/services/{serviceId}` with the returned `service_id`, until `state` is `running`.
</Step>
</Steps>

## Attach a custom domain

<Steps>
<Step title="Register the domain" icon="globe">
`POST /v1/environments/{envId}/services/{serviceId}/custom-domains` with `{"domain": "app.example.com"}`.

The response contains `dns_records`, each with `record_name`, `record_type` and `record_value`.

</Step>
<Step title="Create the DNS records" icon="server">
Add every returned record in your DNS provider - both the certificate validation record and the traffic routing
record.
</Step>
<Step title="Verify" icon="shield-check">
`POST /v1/environments/{envId}/services/{serviceId}/custom-domains/{customDomainId}/verify`.

<Warning>
While DNS is still propagating, verification fails with `500` and `error_code: unknown` rather than a 4xx. Retry
once the records have propagated.
</Warning>

</Step>
<Step title="Confirm" icon="circle-check">
`GET /v1/environments/{envId}/services/{serviceId}/custom-domains` until `active_domain.state` is `deployed`.

Verifying a new domain retires the service's previously active domain.

</Step>
</Steps>

## Wire one service to another

Services inside the same environment reach each other over an in cluster DNS alias.

<Steps>
<Step title="Read the alias" icon="magnifying-glass">
`GET /v1/environments/{envId}/services/{serviceId}` returns `svc_alias`. It is empty until the service is
provisioned, so poll until it is set.
</Step>
<Step title="Publish it as a secret" icon="key">
`PUT /v1/environments/{envId}/services/{serviceId}/secrets` on the consuming service, with the alias as the host -
for example `DB_HOST`.
</Step>
<Step title="Redeploy the consumer" icon="rocket">
Secret writes do not roll themselves out. Trigger a deployment for the consuming service.
</Step>
</Steps>

## Update secrets safely

Both secret endpoints are a **full replacement**, not a merge. Any key you leave out of the array is removed.

<Steps>
<Step title="Read" icon="download">
`GET /v1/environments/{envId}/secrets`, or the service level equivalent.
</Step>
<Step title="Modify" icon="pen">
Change or add entries in the array you just read. Keys must be non empty and unique - a duplicate is rejected with
`422` and `Duplicate secret key: <key>`.
</Step>
<Step title="Write the whole set back" icon="upload">
`PUT` the complete array. Validation runs before anything is written, so a rejected request changes nothing.
</Step>
</Steps>

## Delete a service

<Steps>
<Step title="Remove protection" icon="unlock">
A protected service returns `409` on delete. Clear it first with
`PATCH /v1/environments/{envId}/services/{serviceId}` and `{"is_protected": false}`.

<Warning>
When patching a service whose `type` is `job`, always include `"auto_deploy": false` in the body. Omitting it on
a job typed service surfaces as a `500`.
</Warning>

</Step>
<Step title="Delete" icon="trash">
`DELETE /v1/environments/{envId}/services/{serviceId}` returns `202 {"message": "accepted"}`, meaning the teardown
was accepted and started. A non 2xx means nothing was torn down.
</Step>
<Step title="Poll to completion" icon="arrows-rotate">
`GET /v1/environments/{envId}/services/{serviceId}` through `delete_queued`, `deleting` and `deleted`, or
`delete_failed` on failure.
</Step>
</Steps>
3 changes: 2 additions & 1 deletion docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@
"group": "Getting Started",
"icon": "rocket",
"pages": [
"api/getting-started"
"api/getting-started",
"api/workflows"
]
},
{
Expand Down
Loading