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.
| Operation | Minimum scope |
|---|---|
| Create an HTTP application or scheduled Job | project.create on organization:* |
| Create PostgreSQL, MariaDB, or ClickHouse | database.create on organization:* |
| Create Valkey (Key-Value, Cache, or Queue) | managed_service.create on organization:* |
| List projects | No extra scope; the list only includes projects the credential can read |
| Delete a project | project.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.
| Route | Content | Limit |
|---|---|---|
GET /v1/project/plans | HTTP application plans | 50 per minute |
GET /v1/project/job/plans | Scheduled Job plans | 60 per minute |
GET /v1/project/storage/plans | Storage price | 50 per minute |
GET /v1/project/database/plans | Database plans and capacity per engine | 10 per minute |
GET /v1/project/database/catalog | Database engines and configuration fields | 100 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| Header | Required | Description |
|---|---|---|
x-api-key or Authorization | Yes | Organization API Key or user token |
x-organization-id | With a user token | Active organization |
Content-Type: application/json | Yes | Body format |
Idempotency-Key | Required for Valkey | 16 to 200 characters among letters, digits, ., _, and - |
Limit: 10 requests per minute.
Common fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 6 to 32 characters, lowercase letters, digits, and hyphens between them |
description | string | No | 1 to 256 characters |
plan | string | Yes | Plan compatible with the project type (see the catalogs) |
payment_mode | string | Yes | hourly, monthly, or yearly; Jobs only use per_minute |
config | object | Yes | Configuration 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 field | Type | Required | Description |
|---|---|---|---|
type_project | string | Yes | http |
exposure | string | Yes | public or private |
image.url | string | Yes | Image reference, 8 to 256 characters, without http:// or https:// |
image.is_public | boolean | Yes | false requires image.authentication |
image.authentication | object | For a private image | auth_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 |
port | integer | Yes | Port the application listens on |
instances | number | Yes | Initial number of instances |
storage.persistent | boolean | Yes | Whether storage is persistent |
storage.capacity | number | Yes | 1 to 250 GB; exactly 1 on the free plan |
storage.dir_path_to_persist | string | With persistent: true | Absolute path to persist, up to 80 characters |
envs | array | Yes | Up to 50 { "name", "value" } items; name with 1 to 120 characters and value with 1 to 32,760. Use [] for none |
network_access | object | Yes | ingress_white_list (1 to 10 items) and ingress_black_list (up to 10 items), each item with cidr and description. See Network access |
subdomain | string | No | 3 to 54 characters; not allowed with exposure: private |
custom_domains | array | No | Up to 20 domains; must be empty with exposure: private |
healthcheck | object | No | { "enabled": true, "path": "/health" }. Requires a plan with capabilities.healthcheck: true. See Health check |
autoscaling | object | No | enabled: 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.
| Field | Type | Required | Description |
|---|---|---|---|
source.connection_id | string | Yes | Organization Git connection ID |
source.repository_id | string | Yes | Repository selected in the connection (up to 1,024 characters) |
source.branch | string | Yes | Published branch (up to 255 characters) |
source.auto_deploy | boolean | Yes | Publishes on every push to the branch |
source.version_deploy | object | No | enabled, event (tag or release), tag_pattern, and include_prereleases. Cannot be enabled together with auto_deploy |
build.runtime | string | No | Runtime from the GET /v1/git/runtime-catalog catalog |
build.version | string | No | Version accepted by the runtime |
build.start_command, build.pre_build_command, build.build_command | string | No | Up to 256 characters |
build.dockerfile_path | string | No | Up to 256 characters |
build.context_path | string | No | Build 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 with400. - 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
.gitfolder is not included in the build or in the published application. Acontext_pathpointing to.gitis 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 field | Type | Required | Description |
|---|---|---|---|
type_project | string | Yes | postgresql, mariadb, or clickhouse |
version | string | Yes | PostgreSQL: 15, 16, 17, or 18. MariaDB: 10 or 11. ClickHouse: 26.8.6.5 |
instances | number | Yes | PostgreSQL: 1 to 3 (1 on db-free). MariaDB: exactly 3. ClickHouse: as defined by the plan catalog |
storage.persistent | boolean | Yes | Data persistence |
storage.capacity | number | Yes | 1 to 250 GB; exactly 1 on free plans |
envs | array | Yes | Same rules as the HTTP application; use [] |
network_access | object | Yes | Same 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 field | Type | Required | Description |
|---|---|---|---|
type_project | string | Yes | valkey |
profile | string | Yes | key_value, cache, or queue |
version | string | Yes | 9.1.1 |
storage | object | For key_value and queue | { "persistent": true, "capacity": 1-250 }; not allowed for cache |
network_access | object | No | Same 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", andconfig.job.cron. See Scheduled Jobs. - Application from a template: alternative body with
template_idandtemplate_revision. See Templates.
Creation errors
| Code | Situation |
|---|---|
400 | Invalid 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) |
402 | Organization blocked due to a pending payment |
403 | Insufficient scope for the project type, or plan without health check (HEALTHCHECK_NOT_AVAILABLE_FOR_PLAN) or auto-scaling |
409 | Free 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) |
429 | Rate limit exceeded |
503 | Resource 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:
code | Extra fields | Situation |
|---|---|---|
FREE_PLAN_INSTANCE_LIMIT_REACHED | limit: 2 | The organization already uses the 2 instances of the free plan |
FREE_PLAN_INSTANCE_LIMIT_EXCEEDED | limit: 2, remaining | The requested instances exceed what is left on the free plan; remaining tells how many still fit |
DB_FREE_PROJECT_LIMIT_REACHED | limit: 2 | The 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/projectAccepts 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/:idcurl -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.
| Code | Situation |
|---|---|
404 | Project not found in the organization |
409 | Project already deleted, Job runs still being finalized (JOB_RUNS_PENDING), or managed service operation in progress |
503 | Temporary failure releasing the domain; try again |
Next steps
- Read and change the project in Project information.
- Operate databases in Databases through the API.
- Adjust the lifecycle and instances.
- Track costs in Project billing.
Last updated on
Zenifra API
Zenifra REST API reference to create and operate projects with organization API keys, resource-scoped permissions, rate limits, and response codes.
Project information
List and filter projects by type, status, and Valkey profile, read details, and update name, description, and exposure through the Zenifra API.