Create, list, and delete projects

This page covers the basic project lifecycle through the API: read the public plan catalogs, create the project with POST /v1/project, list it, and delete it with DELETE /v1/project/:id. To read or change an existing project, see Project information.

Authentication and permissions

Creating, listing, and deleting accept an organization API Key (x-api-key: znf_... or Authorization: Bearer znf_...) or a user token with x-organization-id. The plan catalogs are public and require no authentication.

OperationMinimum scope
Create an HTTP application or scheduled Jobproject.create on organization:*
Create PostgreSQL, MariaDB, or ClickHousedatabase.create on organization:*
Create Valkey (Key-Value, Cache, or Queue)managed_service.create on organization:*
List projectsNo extra scope; the list only includes projects the credential can read
Delete a projectproject.delete on project:<project-id>

owner has full access. Without the scope that matches config.type_project, the API returns 403 with insufficient organization permission.

Public catalogs

Read the catalogs before creating a project: they list the accepted plans, current prices, and what each plan includes. Monetary values are numbers in BRL cents. Do not hard-code prices in automations.

RouteContentLimit
GET /v1/project/plansHTTP application plans50 per minute
GET /v1/project/job/plansScheduled Job plans60 per minute
GET /v1/project/storage/plansStorage price50 per minute
GET /v1/project/database/plansDatabase plans and capacity per engine10 per minute
GET /v1/project/database/catalogDatabase engines and configuration fields100 per minute

HTTP application plans

GET /v1/project/plans
{
  "status": "success",
  "message": "get plans with success",
  "data": [
    {
      "plan": "premium",
      "prices": { "hourly": 0, "monthly": 0, "yearly": 0 },
      "features": ["..."],
      "permissions": { },
      "capabilities": {
        "logs": true,
        "metrics": true,
        "healthcheck": true,
        "autoscaling": true,
        "custom_subdomain": true,
        "network_access": true
      }
    }
  ]
}

The prices in the example are illustrative; read the values from the response. The list is sorted by hourly price, and free plans return 0 prices. The capabilities object tells, with boolean values, what the plan includes. logs is always true, because application logs are available on every plan; the other fields tell whether the plan includes metrics, health check, auto-scaling, a custom subdomain, and network access control. Prefer capabilities to decide what to offer in integrations.

Scheduled Job plans

GET /v1/project/job/plans returns the job-* plans with price_per_minute, currency, payment_mode: "per_minute", features, and permissions. When Jobs are temporarily unavailable, the route returns 503 with SCHEDULED_JOBS_UNAVAILABLE. Details in Scheduled Jobs.

Storage

GET /v1/project/storage/plans returns one entry per storage type:

{
  "status": "success",
  "message": "get storage prices with success",
  "data": [
    {
      "storage": "1gb_persistente",
      "prices": { "hourly": 0, "monthly": 0, "yearly": 0 },
      "persistent": true
    }
  ]
}

Prices refer to 1 GB. The hourly value is the monthly value divided by 720, and the yearly value is 12 times the monthly value.

Database plans

GET /v1/project/database/plans returns the db-* plans (PostgreSQL, MariaDB, and Key-Value) and the analytics-* plans (ClickHouse). Each item has plan, prices (hourly, monthly, yearly), features, and engines, with the capacity accepted per engine:

{
  "plan": "db-basic",
  "prices": { "hourly": 0, "monthly": 0, "yearly": 0 },
  "features": ["..."],
  "engines": {
    "postgresql": {
      "instances": { "min": 1, "default": 1, "max": 3, "editable": true },
      "storage": { "min": 1, "default": 1, "max": 250, "editable": true, "included": 0, "step": 1 },
      "versions": ["15", "16", "17", "18"],
      "default_version": "18",
      "billing": { "instances": "per_instance", "storage": "per_instance_gb" }
    }
  }
}

The numbers in the example are illustrative. If the catalog is unavailable, the route returns 503 with database catalog is temporarily unavailable.

Database engine catalog

GET /v1/project/database/catalog organizes the same information by engine. Each item has id, name, versions, plans (with id, payment_modes, and capability), configuration_fields (version, instances, and capacity in GB; each plan's limits are in plans[].capability), and connection_fields, which describes the connection data the product provides. The response uses the { "status": "success", "data": [...] } envelope and can return 503 under the same conditions as the plan catalog.

Create a project

POST /v1/project
HeaderRequiredDescription
x-api-key or AuthorizationYesOrganization API Key or user token
x-organization-idWith a user tokenActive organization
Content-Type: application/jsonYesBody format
Idempotency-KeyRequired for Valkey16 to 200 characters among letters, digits, ., _, and -

Limit: 10 requests per minute.

Common fields

FieldTypeRequiredDescription
namestringYes6 to 32 characters, lowercase letters, digits, and hyphens between them
descriptionstringNo1 to 256 characters
planstringYesPlan compatible with the project type (see the catalogs)
payment_modestringYeshourly, monthly, or yearly; Jobs only use per_minute
configobjectYesConfiguration by type; config.type_project is http, postgresql, mariadb, clickhouse, valkey, or job

Send numbers in the body as JSON numbers, not strings. Fields that do not belong to the given type are rejected with 400.

HTTP application from an image

config fieldTypeRequiredDescription
type_projectstringYeshttp
exposurestringYespublic or private
image.urlstringYesImage reference, 8 to 256 characters, without http:// or https://
image.is_publicbooleanYesfalse requires image.authentication
image.authenticationobjectFor a private imageauth_type: "username_password" with username and token, or auth_type: "aws" with aws.access_key_id, aws.secret_access_key, aws.region, and aws.account_id
portintegerYesPort the application listens on
instancesnumberYesInitial number of instances
storage.persistentbooleanYesWhether storage is persistent
storage.capacitynumberYes1 to 250 GB; exactly 1 on the free plan
storage.dir_path_to_persiststringWith persistent: trueAbsolute path to persist, up to 80 characters
envsarrayYesUp to 50 { "name", "value" } items; name with 1 to 120 characters and value with 1 to 32,760. Use [] for none
network_accessobjectYesingress_white_list (1 to 10 items) and ingress_black_list (up to 10 items), each item with cidr and description. See Network access
subdomainstringNo3 to 54 characters; not allowed with exposure: private
custom_domainsarrayNoUp to 20 domains; must be empty with exposure: private
healthcheckobjectNo{ "enabled": true, "path": "/health" }. Requires a plan with capabilities.healthcheck: true. See Health check
autoscalingobjectNoenabled: true, max_instances greater than or equal to instances, and optional CPU and memory targets from 1 to 100. Requires a plan with capabilities.autoscaling: true
curl -X POST "https://api.zenifra.com/v1/project" \
  -H "x-api-key: znf_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-api",
    "plan": "basic",
    "payment_mode": "hourly",
    "config": {
      "type_project": "http",
      "exposure": "public",
      "image": { "url": "ghcr.io/my-org/my-api:1.4.2", "is_public": true },
      "port": 8080,
      "instances": 1,
      "storage": { "persistent": false, "capacity": 1 },
      "envs": [{ "name": "NODE_ENV", "value": "production" }],
      "network_access": {
        "ingress_white_list": [{ "cidr": "0.0.0.0/0", "description": "Public access" }],
        "ingress_black_list": []
      }
    }
  }'
{
  "status": "success",
  "message": "create a project with success",
  "data": {
    "project_id": "6650f1a2b3c4d5e6f7a8b9c0",
    "domain": "<project-domain>",
    "type": "application"
  }
}

For images in private registries, see Private registry.

HTTP application from a Git repository

Replace image with source and build, which must be sent together. The connection must exist first; see Git connections.

FieldTypeRequiredDescription
source.connection_idstringYesOrganization Git connection ID
source.repository_idstringYesRepository selected in the connection (up to 1,024 characters)
source.branchstringYesPublished branch (up to 255 characters)
source.auto_deploybooleanYesPublishes on every push to the branch
source.version_deployobjectNoenabled, event (tag or release), tag_pattern, and include_prereleases. Cannot be enabled together with auto_deploy
build.runtimestringNoRuntime from the GET /v1/git/runtime-catalog catalog
build.versionstringNoVersion accepted by the runtime
build.start_command, build.pre_build_command, build.build_commandstringNoUp to 256 characters
build.dockerfile_pathstringNoUp to 256 characters
build.context_pathstringNoBuild root directory, relative to the repository root. Default ".". See Build root directory
{
  "name": "my-api",
  "plan": "basic",
  "payment_mode": "hourly",
  "config": {
    "type_project": "http",
    "exposure": "public",
    "source": {
      "connection_id": "<connection-id>",
      "repository_id": "<repository-id>",
      "branch": "main",
      "auto_deploy": true
    },
    "build": { "start_command": "npm start" },
    "port": 3000,
    "instances": 1,
    "storage": { "persistent": false, "capacity": 1 },
    "envs": [],
    "network_access": {
      "ingress_white_list": [{ "cidr": "0.0.0.0/0", "description": "Public access" }],
      "ingress_black_list": []
    }
  }
}

The 201 response includes the build_id of the first build, which you can follow in Git repository builds. For GitHub repositories, creation still uses the github field; source with the GitHub connection returns 409 with GIT_SOURCE_CREATION_UNAVAILABLE. In github, the optional context_path field follows the same rules as build.context_path.

Build root directory

build.context_path (or github.context_path when creating a GitHub project) sets the build root directory: the repository folder where dependency installation, pre-build, build, and start run. Use this field to publish an application that lives in a subfolder of a monorepo.

  • Optional; the default is ".", the repository root.
  • The value is normalized: surrounding whitespace, a leading ./, and a trailing / are removed. " ./apps/api/ " is stored as "apps/api", and an empty value becomes ".".
  • Only relative paths inside the repository are accepted, with / as the separator and up to 256 characters.
  • Absolute paths (/app), .. or . segments (apps/../api), backslashes (\), and control characters are rejected with 400.
  • Only the contents of the folder go into the build. If it does not exist on the published branch, the build fails stating that the configured root directory does not exist in the repository.
  • The repository .git folder is not included in the build or in the published application. A context_path pointing to .git is treated as a folder that does not exist.
{
  "build": {
    "start_command": "npm start",
    "build_command": "npm run build",
    "context_path": "apps/api"
  }
}

PostgreSQL, MariaDB, and ClickHouse

config fieldTypeRequiredDescription
type_projectstringYespostgresql, mariadb, or clickhouse
versionstringYesPostgreSQL: 15, 16, 17, or 18. MariaDB: 10 or 11. ClickHouse: 26.8.6.5
instancesnumberYesPostgreSQL: 1 to 3 (1 on db-free). MariaDB: exactly 3. ClickHouse: as defined by the plan catalog
storage.persistentbooleanYesData persistence
storage.capacitynumberYes1 to 250 GB; exactly 1 on free plans
envsarrayYesSame rules as the HTTP application; use []
network_accessobjectYesSame format as the HTTP application

Use db-* plans for PostgreSQL and MariaDB (MariaDB is not available on db-free) and analytics-* plans for ClickHouse.

{
  "name": "orders-db",
  "plan": "db-basic",
  "payment_mode": "hourly",
  "config": {
    "type_project": "postgresql",
    "version": "18",
    "instances": 1,
    "storage": { "persistent": true, "capacity": 10 },
    "envs": [],
    "network_access": {
      "ingress_white_list": [{ "cidr": "203.0.113.0/24", "description": "Office" }],
      "ingress_black_list": []
    }
  }
}
{
  "status": "success",
  "message": "create a project with success",
  "data": {
    "project_id": "6650f1a2b3c4d5e6f7a8b9c1",
    "type": "database",
    "database": {
      "connection": {
        "host": "<host>",
        "port": 5432,
        "database": "<database>",
        "username": "<username>",
        "password": "<password>",
        "connectionString": "postgresql://<username>:<password>@<host>:5432/<database>"
      },
      "version": "18",
      "replicas": 1
    }
  }
}

The password only appears at creation and on credential renewal. Store it securely. If the connection data is not ready yet, connection can be null; read it later with GET /v1/project/:id/database/connection. See also Databases and ClickHouse.

Valkey: Key-Value, Cache, and Queue

config fieldTypeRequiredDescription
type_projectstringYesvalkey
profilestringYeskey_value, cache, or queue
versionstringYes9.1.1
storageobjectFor key_value and queue{ "persistent": true, "capacity": 1-250 }; not allowed for cache
network_accessobjectNoSame format, with ingress_white_list from 0 to 10 items

The plan must match the profile: db-* for key_value, cache-* for cache, and queue-* for queue. The Idempotency-Key header is required. When you repeat the same request with the same key, the API returns the same project without creating another one; the same key with a different body returns 409 with IDEMPOTENCY_KEY_CONFLICT.

curl -X POST "https://api.zenifra.com/v1/project" \
  -H "x-api-key: znf_..." \
  -H "Idempotency-Key: session-cache-2026-10-08-001" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "session-cache",
    "plan": "cache-basic",
    "payment_mode": "hourly",
    "config": { "type_project": "valkey", "profile": "cache", "version": "9.1.1" }
  }'

The 201 response (message: "project created") returns project_id, status, type: "managed_service", and managed_service with engine, profile, version, username, host, port, tls, and connection_string. The connection_string with the password only appears in the first response; a repeat with the same key returns the other fields. See Managed services, Cache, and Queues.

Other types

  • Scheduled Jobs (type_project: "job"): job-* plans, payment_mode: "per_minute", and config.job.cron. See Scheduled Jobs.
  • Application from a template: alternative body with template_id and template_revision. See Templates.

Creation errors

CodeSituation
400Invalid body, plan incompatible with the type ("plan" must match "config.type_project"), unknown plan, unsupported version, IDEMPOTENCY_KEY_REQUIRED for Valkey, or invalid payment mode (PAYMENT_MODE_INVALID, SCHEDULED_JOB_PAYMENT_MODE_INVALID)
402Organization blocked due to a pending payment
403Insufficient scope for the project type, or plan without health check (HEALTHCHECK_NOT_AVAILABLE_FOR_PLAN) or auto-scaling
409Free plan limit reached, capacity unavailable (INSUFFICIENT_PLAN_CAPACITY), domain unavailable, idempotency conflict, or Git connection unavailable (GIT_CONNECTION_UNAVAILABLE), or Git source unavailable for direct creation (GIT_SOURCE_CREATION_UNAVAILABLE)
429Rate limit exceeded
503Resource temporarily unavailable, such as PLAN_CAPACITY_TEMPORARILY_UNAVAILABLE or ORGANIZATION_RUNTIME_UNAVAILABLE; try again

Free plan limits

When creation hits a free plan limit, the 409 response includes code and limit, in addition to message:

codeExtra fieldsSituation
FREE_PLAN_INSTANCE_LIMIT_REACHEDlimit: 2The organization already uses the 2 instances of the free plan
FREE_PLAN_INSTANCE_LIMIT_EXCEEDEDlimit: 2, remainingThe requested instances exceed what is left on the free plan; remaining tells how many still fit
DB_FREE_PROJECT_LIMIT_REACHEDlimit: 2The organization already has 2 active db-free projects
{
  "status": "failed",
  "code": "FREE_PLAN_INSTANCE_LIMIT_REACHED",
  "limit": 2,
  "message": "You have reached the limit of 2 instances on the free plan. Delete an existing project to create a new one."
}

Prefer code when handling the error in your code; message is human-readable text.

On free plans, the organization can have at most 2 instances on the free plan, 2 projects on db-free (PostgreSQL and Key-Value combined), and 1 service per profile on the cache-free and queue-free plans.

List projects

GET /v1/project

Accepts page (default 1), limit (1 to 50, default 15), type (http, postgresql, mariadb, or valkey), profile (only with type=valkey), status, search (1 to 256 characters, searches name or description), sort_by (updated_at or created_at), and sort_order (asc or desc). The response has data.projects and data.pagination with page, limit, total, and pages. Limit: 100 requests per minute. The fields of each project are in Project information.

Delete a project

DELETE /v1/project/:id
curl -X DELETE "https://api.zenifra.com/v1/project/6650f1a2b3c4d5e6f7a8b9c0" \
  -H "x-api-key: znf_..."
{
  "status": "success",
  "message": "deleted the project with success"
}

Valkey projects are removed in the background: the response is 202 with { "project_id": "...", "status": "deleting" }. Deletion is permanent; back up your data first. Limit: 10 requests per minute.

CodeSituation
404Project not found in the organization
409Project already deleted, Job runs still being finalized (JOB_RUNS_PENDING), or managed service operation in progress
503Temporary failure releasing the domain; try again

Next steps

Last updated on

On this page