turboEnv

HTTP API

The turboEnv endpoints, what each returns, and how to generate a typed client in any language from the published OpenAPI spec.

A handful of endpoints behind a bearer token. Everything the CLI and the MCP server do, they do through these.

Writes are POST /v1/env, POST /v1/ciphertext, and DELETE /v1/projects/{slug}. A token created in the dashboard cannot call them.

https://app.turboenv.workers.dev/api

Authenticate with the token in an Authorization header:

curl -s https://app.turboenv.workers.dev/api/v1/env \
  -H "Authorization: Bearer $TURBOENV_TOKEN"

Which parameters you must send depends on your credential. A project token carries its own scope, so project and environment are optional. A turboenv login credential covers a whole team and has no implicit target, so project is required — see Tokens and access.

GET /v1/env

The one you want at startup. Returns the resolved variables, decrypted.

{
  "project": "checkout-service",
  "environment": "production",
  "version": 14,
  "createdAt": "2026-07-02T09:31:00Z",
  "variables": { "DATABASE_URL": "postgres://...", "PORT": "3000" }
}

Defaults to the latest version. Pass version to pin one, which is what a reproducible deploy should do.

Parameter
project Project slug. Required for a login credential
environment Environment name. Required unless the token is environment-scoped
version Pin a version instead of taking the latest

GET /v1/variables

The same shape, but the values are not decrypted. You get each variable's name and whether it is marked secret.

Reach for this whenever the question is "what does this application expect to be configured" rather than "what are the values". It answers most tooling questions without any secret leaving the database, which matters especially if the caller is a language model — see AI agents.

POST /v1/env

Saves a new version. This is what turboenv push calls.

Requires a turboenv login credential. A dashboard token is read-only and gets a 403, because tokens live in CI and container images and a leaked one should not be able to change what your next deployment reads.

curl -X POST "$API/v1/env?project=checkout-service&environment=development" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "base_version": 14,
    "message": "rotate Stripe key",
    "variables": [
      { "key": "STRIPE_SECRET_KEY", "value": "sk_live_..." },
      { "key": "PORT", "value": "3000", "isSecret": false }
    ]
  }'

Two things to get right:

base_version is the version you built this from — 0 for an environment with no versions yet. If somebody saved in the meantime you get a 409 and nothing is written, rather than silently overwriting their work.

variables is the entire environment. Anything you omit will not exist in the new version. To change one value, read the environment first and send it back with that one edited.

isSecret defaults to true, so a value is protected unless you say otherwise.

Returns 201 with the new version number, which is your next base_version.

GET and POST /v1/ciphertext

The customer-managed equivalents of /v1/env. They return and accept a public project encryption descriptor plus AES-GCM ciphertext; the customer key and plaintext are never API fields. /v1/env rejects customer-managed projects, and /v1/ciphertext rejects service-managed projects, so an old client fails closed instead of treating ciphertext as an environment value.

Use the published OpenAPI schemas and @turboenv/customer-crypto protocol rather than inventing a wire format. Writes authenticate the intended next version and use the same base_version concurrency check.

GET /v1/project

Describes what your token can actually reach: the project it belongs to and the environments it may read, each with the version a reader would currently get.

Start here when all you hold is a token, because it tells you which values environment will accept. An environment-scoped token sees only its own environment — it cannot enumerate the siblings it has no access to.

GET /v1/projects

Every project your team owns, for a login credential. A project token is scoped to one and sees only that one. turboenv init uses this to build its picker.

DELETE /v1/projects/{slug}

Permanently removes a project, its environments, every version and variable in them, and every token issued for it. Returns counts of what it destroyed.

Requires a turboenv login credential. A project token is refused with 403, for the same reason it cannot write, only more so: a credential sitting in CI or in an agent's MCP configuration must not be able to destroy the configuration it was given to read.

curl -s -X DELETE https://app.turboenv.workers.dev/api/v1/projects/checkout-service \
  -H "Authorization: Bearer $TURBOENV_TOKEN"
{ "destroyed": { "environments": 3, "versions": 27, "variables": 104, "tokens": 2 } }

There is no undo, and anything deploying with one of those tokens stops being able to read its configuration immediately. turboenv delete wraps this with a typed confirmation.

GET /v1/versions

The history for an environment: version number, the message left at save time, when it was saved, and how many variables it held. Paginated with a nextCursor.

Useful for finding the version you want to pin, or for confirming what changed before you roll back.

Errors

Errors are JSON with a single error field.

Status Meaning
400 A required parameter is missing — usually project on a login credential
401 Missing, unknown, revoked or expired token
403 The token exists but is not scoped to what you asked for, or is read-only and tried to write or delete
404 No such project or environment for you
409 Somebody saved since your base_version
429 Rate limited

The distinction between 403 and 404 is deliberate. Asking for a project in another team returns 404, not 403, because a 403 would confirm that the project exists and turn the parameter into a way to discover what other teams have.

Generating a client

We publish no per-language SDKs on purpose. Maintaining hand-tuned clients for three languages was more release machinery than an API this size justifies, so the spec is the product surface instead:

https://app.turboenv.workers.dev/openapi.json
https://app.turboenv.workers.dev/openapi.yaml

It is unauthenticated and CORS-enabled, so anything that finds a turboEnv URL can work out how to call it without being told. Point a generator at it and you get a client shaped the way your codebase wants:

npx @openapitools/openapi-generator-cli generate \
  -i https://app.turboenv.workers.dev/openapi.json \
  -g rust -o ./turboenv-rust

The document is a build input rather than documentation written afterwards: it is what the Worker serves, a test fails if the committed JSON drifts from the YAML source, and another fails if the spec describes a path the router does not serve. A spec that lies cannot reach production.

If you are self-hosting, your own deployment serves its own spec at the same paths, so a generated client always matches the instance it came from.