Products

Update a product

Partial update: only the keys you send are touched.

PATCH/products/{slug}

Partial update: only the keys you send are touched. Sending an icon key of any value (including null) is rejected with 400 request.body_invalid and field icon - product icons are upload-only from the dashboard. That check runs before the product is resolved, so an icon key plus an unknown slug still returns 400, not 404. Flipping isPrivate from false to true automatically grants the key owner access to the product, so an integrator cannot lock themselves out with one call; flipping it back to false does not revoke any grant already recorded. Renaming the slug is validated against the workspace, not the product. Locked products (over the plan cap) reject every PATCH with 402 product.locked.

Requires the products:write scope.

curl --request PATCH \
  --url 'https://api.envless.cloud/products/<slug>' \
  --header 'Authorization: Bearer ev_sk_xxxxxxxxxxxx' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Billing",
  "slug": "billing",
  "isPrivate": false
}'
Try itPATCHhttps://api.envless.cloud/products/{slug}
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

  • slugstringrequired

    Product slug, or the product's uniqueId. A UUID-shaped value is resolved as an id only.

Request body

application/json · required

  • namestringmin length 3max length 50
  • slugstringmin length 3max length 50pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$

    Not slugified on update - it is validated as-is, so send an already-valid slug.

  • isPrivateboolean

    Set true to restrict the product to explicitly granted members. Doing so grants the calling key's owner access in the same request.

Responses

200OK

Update a product succeeded.

  • successbooleanrequired
  • codeintegerrequired

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

  • messagestringrequired
  • errornullrequired
  • dataobjectrequired
    Show child attributesHide child attributes
    • idstringrequired
    • namestringrequired
    • slugstringrequired
    • iconstring | null
    • isPrivatebooleanrequired
    • lockedboolean

      True when the product exceeds the plan limit and is read-only until you upgrade.

    • createdAtstringrequireddate-time
    • updatedAtstring | nulldate-time
  • _reqobject
    Show child attributesHide child attributes
    • reqIdstring
    • resTimeinteger | null
{
    "success": true,
    "code": 200,
    "message": "OK",
    "error": null,
    "data": {
        "id": "<string>",
        "name": "<string>",
        "slug": "<string>",
        "icon": "<string>",
        "isPrivate": true,
        "locked": true,
        "createdAt": "2026-05-30T10:14:22.000Z",
        "updatedAt": "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 not parseable as JSON.
  • request.body_invalid - an icon key is present at all (field icon), or name/slug fails length or slug-pattern validation.

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.

402Payment Required

product.locked - the product sits beyond the plan's product allowance. data carries { locked: true, resource: 'product', plan }.

403Forbidden

  • auth.key_project_restricted - the key is scoped to a list of project ids.
  • auth.scope_missing - the key lacks products:write.
  • auth.workspace_two_factor_required - the workspace requires two-factor authentication and the key's owner has not turned it on.

404Not Found

product.not_found - no product matches, or it is private and the key owner has no grant.

409Conflict

product.slug_conflict - another product in the workspace already owns the new slug.

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.