API keys

Update an API key

Updates a key's label, enabled state, scopes or project fence.

PATCH/keys/{id}

Updates a key's label, enabled state, scopes or project fence. Send only the fields you want to change.

scopes merges, it does not replace. The map you send is layered on top of the key's stored scopes, so omitting a resource leaves it exactly as it was. To take a permission away you must explicitly send that resource with the level none - {"scopes": {"variables": "none"}}. Sending {"scopes": {}} changes nothing. projectIds, by contrast, replaces wholesale: whatever array you send becomes the fence, and [] removes the fence entirely.

The same two reductions as POST /keys apply to the merged result: the ceiling check first (403 auth.scope_ceiling_exceeded if you ask for more than the calling key holds), then a silent clamp down to the calling key owner's workspace role. Read data.scopes back off the response.

A key cannot re-scope, re-fence or disable itself. If id is the key making the request and the body contains scopes, projectIds, or active: false, the request fails with 409 auth.self_mutation_forbidden. Renaming yourself is allowed, and so is active: true on yourself, because neither can lock you out. Use a second key, or the dashboard.

Ordering gotcha: the scope-ceiling and project-subset checks run before the key is looked up, so a request with an over-privileged scopes map against an id that does not exist returns 403, not 404. Fix the scopes first, then trust the 404.

Unrecognised field types are ignored rather than rejected: a non-string name or a non-boolean active is dropped silently. A PATCH that ends up touching nothing succeeds with 200 and the unchanged key.

Keys are plan-independent: the API, SDK and CLI are included on every plan, including Free, so a plan change never switches a key off and active: true works on every tier.

There is no way to change a key's secret here. Rotation is not exposed - create a replacement key and delete this one.

Requires the keys:write scope.

curl --request PATCH \
  --url 'https://api.envless.cloud/keys/<id>' \
  --header 'Authorization: Bearer ev_sk_xxxxxxxxxxxx' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Billing",
  "active": true,
  "scopes": {
    "variables": "read",
    "versions": "read"
  },
  "projectIds": []
}'
Try itPATCHhttps://api.envless.cloud/keys/{id}
Path parameters

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.

Path parameters

  • idstringrequireduuid

    The key's id (UUIDv7). Not the prefix, not the secret; keys have no slug form.

Request body

application/json · required

  • namestringmin length 4max length 34

    New label. 4 to 34 characters. Non-string values are ignored.

  • activeboolean

    false disables the key immediately; every subsequent request with it gets 401. true re-enables it. A key may not send false for itself. Non-boolean values are ignored.

  • scopesobject

    Partial scope map, MERGED onto the key's current scopes. Omitted resources are left untouched - send an explicit none to revoke. Unknown resource names are rejected with 400.

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

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

  • projectIdsstring[]

    REPLACES the project fence with exactly this set of project id values (UUIDv7). [] removes the fence and makes the key workspace-wide.

Responses

200OK

Update 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
  • _reqobject
    Show child attributesHide child attributes
    • reqIdstring
    • resTimeinteger | null
{
    "success": true,
    "code": 200,
    "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"
    },
    "_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 - scopes names an unknown resource or uses a level other than none/read/write, projectIds is not an array of UUIDv7 project ids, or name is outside 4-34 characters.

401Unauthorized

  • 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_user_missing - the key is valid but its owner no longer exists.
  • 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; checked before the key is looked up, so this can mask a 404.
  • 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.

404Not Found

key.not_found - no live key with that id exists in this workspace.

409Conflict

auth.self_mutation_forbidden - the key is patching itself with scopes, projectIds, or active: false.

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.