Projects

Create a project

Creates a project inside a product.

POST/projects

Creates a project inside a product. Both name and slug are required here (unlike POST /products, which derives the slug); the supplied slug is still slugified before validation. The target product is chosen by the product field in the body, which accepts a product slug only - a product uniqueId is not resolved and returns 404. Omit product and the workspace's default product is used, being created on the fly if it does not exist yet. Because slugs are unique per product, the same project slug can exist in several products; the 409 project.slug_conflict is scoped to the chosen product. The plan allowance is counted across the whole workspace, not per product, and is checked before the slug conflict.

Requires the projects:write scope.

curl --request POST \
  --url 'https://api.envless.cloud/projects' \
  --header 'Authorization: Bearer ev_sk_xxxxxxxxxxxx' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Billing",
  "slug": "billing",
  "product": "internal-tools"
}'
Try itPOSThttps://api.envless.cloud/projects

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 3max length 50

    Display name. Required.

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

    Required. Unique within the chosen product only. Slugified server-side before validation.

  • productstring

    Product slug to create the project under. Slug only - ids are not accepted. Defaults to the workspace's default product, created on demand.

Responses

201Created

Create a project 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
    • productobject | nullrequired
      Show child attributesHide child attributes
      • idstringrequired
      • slugstring | null
      • namestring | null
    • createdAtstringrequireddate-time
    • updatedAtstring | nulldate-time
  • _reqobject
    Show child attributesHide child attributes
    • reqIdstring
    • resTimeinteger | null
{
    "success": true,
    "code": 201,
    "message": "OK",
    "error": null,
    "data": {
        "id": "<string>",
        "name": "<string>",
        "slug": "<string>",
        "icon": "<string>",
        "product": {
            "id": "<string>",
            "slug": "<string>",
            "name": "<string>"
        },
        "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 - name or slug is missing or not a string, or either 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

plan.upgrade_required - the workspace already holds its plan's project allowance (Free 1, Dev 3, Team 15; Enterprise is unlimited and never hits this). data carries { limit, current }.

403Forbidden

  • auth.key_project_restricted - the key is scoped to a list of project ids; such keys cannot create projects, since the new project would fall outside their allowlist.
  • auth.scope_missing - the key lacks projects: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 - product names a slug that does not exist in the workspace, or one the key owner has no grant on.

409Conflict

  • plan.limit_reached - reserved for a plan whose project allowance sits at or above the 1,000 fair-use cap. No current tier can trigger it: Free, Dev and Team fail with 402 plan.upgrade_required at their own allowance instead, and Enterprise is unlimited and never hits a plan limit. data carries { limit, current }.
  • project.slug_conflict - the resolved slug is already taken inside the chosen product.
  • product.not_found - the product was deleted while the project was being created.

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.