/projects/{slug}/environments/{environmentSlug}/variables/applyThe transactional way to sync an environment. A sync client should use this instead of N separate create, update and delete calls. Everything runs inside one database transaction behind a per-environment advisory lock, so the environment is never left half-written and two concurrent applies cannot interleave.
baseline is required and is how drift is detected. Send the full set of variables you last read, as { uniqueId, name, updatedAt } for every live variable in the environment, using the id and updatedAt you received from the list endpoint. Before touching anything, the server compares the baseline against the live rows and rejects the whole request with 409 variable.baseline_drifted if any live variable is missing from your baseline, any baseline entry no longer exists, or any updatedAt differs by even a millisecond. The drifted names are listed in message, sorted, and nowhere else. On a 409, re-read the environment and rebuild the change set. A partial baseline is not a partial check, it is guaranteed drift, because every live variable you omitted counts as drifted.
value in creates and updates is ciphertext encrypted client-side with the workspace key. Gotcha: this endpoint does not run the v1:/v2: prefix check that POST .../variables runs, it only enforces a length of 1 to 20000. Plaintext sent here is stored as plaintext and then propagates into every later version snapshot. Validate the prefix in your client.
Apply is value-only and name-only. creates insert name and value and take the column defaults for type, isSecret, visibility, required and defaultValue. updates set value alone. There is no rename and no defaultValue path here, use PATCH .../variables/{name} or PATCH /variables/{id} for those. Names are compared without regard to case throughout apply, so a creates entry for api_key conflicts with a live API_KEY unless the same request deletes it.
Caps: at most 1000 baseline entries, and at most 1000 items across creates plus updates plus deletes combined. At least one of the three must be non-empty. The request body may be at most 1 MiB (1,048,576 bytes), checked before anything else; a larger one is refused with 413 request.body_too_large, so a sync of many large values must be split across requests.
Requires the variables:write scope.
curl --request POST \
--url 'https://api.envless.cloud/projects/<slug>/environments/<environmentSlug>/variables/apply' \
--header 'Authorization: Bearer ev_sk_xxxxxxxxxxxx' \
--header 'Content-Type: application/json' \
--data '{
"baseline": [
{
"uniqueId": "<string>",
"name": "STRIPE_SECRET_KEY",
"updatedAt": "2026-05-30T10:14:22.000Z"
}
],
"creates": [
{
"name": "SENTRY_DSN",
"value": "v1:8Wm3kQ...ciphertext"
}
],
"updates": [
{
"uniqueId": "<string>",
"value": "v1:8Wm3kQ...ciphertext",
"name": "SENTRY_DSN"
}
],
"deletes": [
{
"uniqueId": "<string>"
}
]
}'https://api.envless.cloud/projects/{slug}/environments/{environmentSlug}/variables/applyResponse 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.
Path parameters
slugstringrequiredProject slug or project uniqueId.
environmentSlugstringrequiredEnvironment slug.
Query parameters
productstringProduct slug, to disambiguate a project slug shared across products.
Request body
application/json · required
baselineobject[]requiredmax items 1000Every live variable as you last read it. May be empty only when the environment is genuinely empty. Omitting a live variable is drift.
Show child attributesHide child attributes
uniqueIdstringrequiredThe variable
idfrom a read.namestringrequiredUsed only to name the variable in the drift message. Send it back exactly as you read it, no pattern is enforced here so a legacy name still round-trips.
updatedAtstringrequireddate-timeCompared for exact millisecond equality against the live row.
createsobject[]New variables. Name must match
^[A-Za-z_][A-Za-z0-9_]*$and be 1 to 100 characters, is stored with the casing you send, and must not repeat within the request. Repeats inside the request and collisions with live variables are judged without regard to case, soapi_keycollides with an existingAPI_KEYyou are not deleting in the same request.Show child attributesHide child attributes
namestringrequiredmin length 1max length 100pattern ^[A-Za-z_][A-Za-z0-9_]*$Variable name. Stored as typed, compared without regard to case.
valuestringrequiredmin length 1max length 20000pattern ^v[12]:CIPHERTEXT, starting with
v1:orv2:. A value without the prefix fails the request with400 variable.ciphertext_invalid.
updatesobject[]Value replacements, optionally with a rename.
valueis always written; sendnameas well to rename the variable in the same transaction.Show child attributesHide child attributes
uniqueIdstringrequiredvaluestringrequiredmin length 1max length 20000pattern ^v[12]:CIPHERTEXT, starting with
v1:orv2:. A value without the prefix fails the request with400 variable.ciphertext_invalid.namestringmin length 1max length 100pattern ^[A-Za-z_][A-Za-z0-9_]*$Optional new name, which renames the variable. Validated like a
createsname, and unique within the request without regard to case.
deletesobject[]Soft deletes by id. A name freed by a delete in the same request can be reused by a create in that request, including under a different casing.
Show child attributesHide child attributes
uniqueIdstringrequired
Responses
200OK
Apply a set of variable changes atomically succeeded.
successbooleanrequiredcodeintegerrequiredThe HTTP status of this response, repeated in the body.
messagestringrequirederrornullrequireddataobjectrequiredShow child attributesHide child attributes
addedintegerrequiredupdatedintegerrequiredremovedintegerrequired
_reqobjectShow child attributesHide child attributes
reqIdstringresTimeinteger | null
{
"success": true,
"code": 200,
"message": "OK",
"error": null,
"data": {
"added": 1,
"updated": 1,
"removed": 1
},
"_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_invalid-baselineis missing or not an array (error.field: "baseline"), all three change arrays are empty, the 1000 item cap is exceeded, an item has the wrong shape, or the body is not JSON (request.body_must_be_json). Also 400request.body_invalidfor a rejected name, a value outside 1 to 20000 characters, and, counterintuitively, for exceeding the plan's variable cap, whose message reads like a plan error but arrives as 400 rather than 402.variable.ciphertext_invalid- acreatesorupdatesvalue does not start withv1:orv2:; nothing is written anderror.fieldnames the field.
401Unauthorized
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_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.
402Payment Required
plan.upgrade_required - the project is locked because the workspace exceeds its plan's project limit.
403Forbidden
auth.key_project_restricted- the key's project allowlist excludes this project, orauth.product_restrictedfor an inaccessible private product.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.
404Not Found
project.not_found - unknown project, or environment.not_found for an unknown environment slug.
409Conflict
variable.baseline_drifted - the environment changed under you. The drifted names are only in message. The same code is reused for two neighbouring conflicts, a creates name already taken by a variable you are not deleting, and an updates or deletes id that is no longer live, so read message to tell them apart. 409 project.ambiguous_product also lands on this status when the project slug is not unique.
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.