/keysMints 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.
- Ceiling. A key can never mint a key more powerful than itself. If any requested
resource:leveloutranks what the calling key holds, the whole request is rejected with 403auth.scope_ceiling_exceededanderror.resourcelists the offendingresource:levelpairs. Nothing is created. - 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
writebecomesreadif the owner can read but not write that resource, and becomesnoneif they cannot even read it. Always readdata.scopesoff 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": []
}'https://api.envless.cloud/keysResponse headers
Authorization
AuthorizationstringrequiredheaderBearer token: an Envless API key (
ev_sk_…), created in the dashboard or withPOST /keys. The key is bound to one workspace; every request runs inside it.
Request body
application/json · required
namestringrequiredmin length 4max length 34Human label for the key. Required. 4 to 34 characters.
scopesobjectPer-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
idvalues (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.
successbooleanrequiredcodeintegerrequiredThe HTTP status of this response, repeated in the body.
messagestringrequirederrornullrequireddataobjectrequiredShow child attributesHide child attributes
idstringrequirednamestringrequiredprefixstring | nullThe non-secret leading segment, for identifying a key in a list.
scopesobjectrequiredPer-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[]requiredProjects this key is fenced to. An empty array means unrestricted.
activebooleanrequiredlastUsedAtstring | nulldate-timekeyRollsintegercreatedAtstringrequireddate-timelastRolledAtstring | nulldate-timesecretstringrequiredThe plaintext API key, e.g.
ev_sk_.... Returned on this response only, never again, and unrecoverable if lost. Store it before doing anything else.
_reqobjectShow child attributesHide child attributes
reqIdstringresTimeinteger | 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.
successbooleanrequiredcodeintegerrequiredThe HTTP status of this response, repeated in the body.
messagestringrequirederrorobjectrequiredMachine-readable failure detail. Null on success.
Show child attributesHide child attributes
codeenum<string>requiredStable 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"resourcestringThe resource or scope the failure relates to.
fieldstringThe request field that caused the failure.
dataobject | nullrequiredNull on most failures. A plan limit carries
{ limit, current }._reqobjectShow child attributesHide child attributes
reqIdstringresTimeinteger | 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-nameis missing, is not a string, or is outside 4-34 characters (error.fieldisname); orscopesnames an unknown resource, uses a level other than none/read/write, orprojectIdsis 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- theAuthorizationheader is missing, or its value does not start withev_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 requestedresource:leveloutranks what the calling key itself holds;error.resourcelists the offending pairs and nothing is created.auth.key_project_restricted- the calling key is fenced to projects andprojectIdsis 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.resourcenames 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.