API keys

Create an API key

Mints a new API key for the workspace and returns the plaintext secret.

POST/keys

Mints a new API key for the workspace and returns the plaintext secret.

data.secret is returned exactly once, on this response, and is unrecoverable. It is not stored in plaintext - only a hash is kept - so no endpoint, dashboard screen or support request can ever show it again. Write it to your secret store before you read anything else off the response. If you lose it, the only remedy is to create a replacement key and delete this one.

The secret looks like ev_sk_<base64url>. prefix on the returned object is its first 12 characters and is safe to log.

Scopes are subject to two independent reductions, in this order.

  1. Ceiling. A key can never mint a key more powerful than itself. If any requested resource:level outranks what the calling key holds, the whole request is rejected with 403 auth.scope_ceiling_exceeded and error.resource lists the offending resource:level pairs. Nothing is created.
  2. Clamp. Whatever survives the ceiling is then clamped down to the permissions of the key owner's workspace role - the human who owns the calling key, not the target key. This clamp is silent: a requested write becomes read if the owner can read but not write that resource, and becomes none if they cannot even read it. Always read data.scopes off the response rather than assuming you got what you asked for; the created key may hold strictly less.

Omitting scopes entirely is legal and produces a key with every resource at none - it authenticates but can call nothing except GET /me.

projectIds fences the key to specific projects and takes project id values (UUIDv7), not slugs. An empty array or an omitted field means workspace-wide. If the calling key is itself project-restricted, projectIds is mandatory and must be a non-empty subset of the caller's own projects, otherwise you get 403 auth.key_project_restricted.

Rotation is deliberately not exposed on the public API. There is no POST /keys/{id}/rotate. To roll a credential: create a replacement key with the scopes you want, deploy it everywhere, confirm the old key's lastUsedAt has stopped advancing, then DELETE /keys/{old_id}. Doing it in that order gives you the overlap window that an in-place rotate would not.

A workspace is capped at 100 keys, counting only live ones.

Scope resources and levels are enumerated on the request schema below. Every resource accepts none, read or write; write implies read.

Requires the keys:write scope.

curl --request POST \
  --url 'https://api.envless.cloud/keys' \
  --header 'Authorization: Bearer ev_sk_xxxxxxxxxxxx' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Billing",
  "scopes": {
    "projects": "read",
    "environments": "read",
    "variables": "write"
  },
  "projectIds": []
}'
Try itPOSThttps://api.envless.cloud/keys

Authorization

  • Authorizationstringrequiredheader

    Bearer token: an Envless API key (ev_sk_…), created in the dashboard or with POST /keys. The key is bound to one workspace; every request runs inside it.

Request body

application/json · required

  • namestringrequiredmin length 4max length 34

    Human label for the key. Required. 4 to 34 characters.

  • scopesobject

    Per-resource access level. Unknown resource names are rejected with 400. Any resource you omit defaults to none. Subject to the ceiling check and then the owner-role clamp described above.

    Keys: workspace, products, product-access, projects, environments, variables, versions, members, roles, keys, webhooks · values enum<string>

    Options: "none", "read", "write"

  • projectIdsstring[]

    Project id values (UUIDv7) to fence this key to. Slugs are rejected. Omit or send [] for a workspace-wide key. Duplicates are de-duplicated.

Responses

201Created

Create an API key succeeded.

  • successbooleanrequired
  • codeintegerrequired

    The HTTP status of this response, repeated in the body.

  • messagestringrequired
  • errornullrequired
  • dataobjectrequired
    Show child attributesHide child attributes
    • idstringrequired
    • namestringrequired
    • prefixstring | null

      The non-secret leading segment, for identifying a key in a list.

    • scopesobjectrequired

      Per-resource access level for an API key.

      Keys: workspace, products, product-access, projects, environments, variables, versions, members, roles, keys, webhooks · values enum<string>

      Options: "none", "read", "write"

    • projectIdsstring[]required

      Projects this key is fenced to. An empty array means unrestricted.

    • activebooleanrequired
    • lastUsedAtstring | nulldate-time
    • keyRollsinteger
    • createdAtstringrequireddate-time
    • lastRolledAtstring | nulldate-time
    • secretstringrequired

      The plaintext API key, e.g. ev_sk_.... Returned on this response only, never again, and unrecoverable if lost. Store it before doing anything else.

  • _reqobject
    Show child attributesHide child attributes
    • reqIdstring
    • resTimeinteger | null
{
    "success": true,
    "code": 201,
    "message": "OK",
    "error": null,
    "data": {
        "id": "<string>",
        "name": "<string>",
        "prefix": "<string>",
        "scopes": {
            "workspace": "none",
            "products": "none"
        },
        "projectIds": [
            "<string>"
        ],
        "active": true,
        "lastUsedAt": "2026-05-30T10:14:22.000Z",
        "keyRolls": 1,
        "createdAt": "2026-05-30T10:14:22.000Z",
        "lastRolledAt": "2026-05-30T10:14:22.000Z",
        "secret": "<string>"
    },
    "_req": {
        "reqId": "<string>",
        "resTime": 1
    }
}

Error envelope

Every failure below returns the same shape with success: false; branch on error.code, never on message.

  • successbooleanrequired
  • codeintegerrequired

    The HTTP status of this response, repeated in the body.

  • messagestringrequired
  • errorobjectrequired

    Machine-readable failure detail. Null on success.

    Show child attributesHide child attributes
    • codeenum<string>required

      Stable error code. Branch on this, never on message.

      Options: "auth.api_key_required", "auth.api_key_not_found", "auth.api_key_user_missing", "auth.api_key_owner_not_member", "auth.scope_missing", "auth.scope_ceiling_exceeded", "auth.key_project_restricted", "auth.product_restricted", "auth.permission_denied", "auth.self_mutation_forbidden", "auth.step_up_required", "auth.step_up_fresh_required", "auth.cli_token_not_accepted", "auth.two_factor_required", "auth.workspace_two_factor_required", "auth.two_factor_invalid", "auth.two_factor_replayed", "auth.two_factor_locked", "auth.two_factor_not_enabled", "auth.two_factor_already_enabled", "request.body_must_be_json", "request.body_invalid", "request.body_too_large", "request.rate_limited", "request.invalid_pagination", "workspace.not_found", "workspace.slug_conflict", "workspace.limit_reached", "workspace.delete_last", "workspace.delete_too_early", "workspace.delete_has_projects", "domain.not_configured", "domain.not_found", "domain.invalid", "domain.taken", "domain.attempt_limit", "product.not_found", "product.slug_conflict", "product.locked", "project.not_found", "project.ambiguous_product", "project.slug_conflict", "environment.not_found", "environment.slug_conflict", "environment.expose_disabled", "environment.expose_private_product", "variable.not_found", "variable.name_conflict", "variable.ciphertext_invalid", "variable.baseline_drifted", "version.not_found", "member.not_found", "member.already_exists", "member.self_invite", "member.self_mutation", "member.owner_immutable", "member.owner_cannot_leave", "member.seat_limit", "invite.already_pending", "role.owner_not_assignable", "role.limit_reached", "role.immutable", "role.in_use", "invite.not_found", "invite.expired", "role.not_found", "role.name_conflict", "key.not_found", "key.limit_reached", "webhook.not_found", "webhook.delivery_not_found", "plan.limit_reached", "plan.upgrade_required", "request.not_found", "request.method_not_allowed", "request.conflict", "auth.unauthorized", "auth.forbidden", "request.invalid", "server.error"

    • resourcestring

      The resource or scope the failure relates to.

    • fieldstring

      The request field that caused the failure.

  • dataobject | nullrequired

    Null on most failures. A plan limit carries { limit, current }.

  • _reqobject
    Show child attributesHide child attributes
    • reqIdstring
    • resTimeinteger | null
{
    "success": false,
    "code": 400,
    "message": "Something went wrong.",
    "error": {
        "code": "auth.api_key_required",
        "resource": "<string>",
        "field": "<string>"
    },
    "data": {},
    "_req": {
        "reqId": "<string>",
        "resTime": 1
    }
}

400Bad Request

  • request.body_must_be_json - the body is absent or is not parseable JSON.
  • request.body_invalid - name is missing, is not a string, or is outside 4-34 characters (error.field is name); or scopes names an unknown resource, uses a level other than none/read/write, or projectIds is not an array of UUIDv7 project ids.

401Unauthorized

  • auth.api_key_user_missing - the owner of the calling key could not be resolved, so there is no role to clamp the new key's scopes against.
  • auth.api_key_required - the Authorization header is missing, or its value does not start with ev_sk_.
  • auth.api_key_not_found - the key is unknown, inactive or revoked.
  • auth.api_key_owner_not_member - the key's owner is no longer a member of the key's workspace.

403Forbidden

  • auth.scope_ceiling_exceeded - any requested resource:level outranks what the calling key itself holds; error.resource lists the offending pairs and nothing is created.
  • auth.key_project_restricted - the calling key is fenced to projects and projectIds is missing, empty, or contains a project outside that fence.
  • auth.workspace_two_factor_required - the workspace requires two-factor authentication and the key's owner has not turned it on.
  • auth.scope_missing - the key lacks a scope this operation requires; error.resource names it.

409Conflict

key.limit_reached - the workspace already has 100 live keys.

413Payload Too Large

request.body_too_large - the request body is larger than 1 MiB (1,048,576 bytes).

500Internal Server Error

server.error - an unexpected error; retry with backoff.

503Service Unavailable

server.error - the API is in maintenance mode or is missing its configuration; retry with backoff.