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.