Templates API

The Templates API lets you browse the public catalog or the active organization catalog, create and revise starting points for HTTP applications, and start a project from a specific revision. Management routes require an authenticated user (with the x-organization-id context) or an organization API key. The official public listing has a separate unauthenticated route.

Authentication and context

Send a user token in the Authorization header with the Bearer scheme or an organization API key (x-api-key or Authorization: Bearer znf_...). With a user token, include x-organization-id; with an organization API key, the organization comes from the key itself. The active organization defines private templates and the destination of a new project.

Authorization: Bearer <token>
x-organization-id: <organization-id>
Content-Type: application/json

GET /v1/public/templates is the only route in this reference that requires neither Authorization nor x-organization-id. It only exposes the official public catalog. Creating projects or changing templates still requires authentication and the corresponding permissions.

Required permissions

Use only the scope for the requested action and the template ID when operating on a specific template:

ActionMinimum scope
List or read a templatetemplate.read on template:<template-id>; public templates follow the public contract
Create a private templatetemplate.create on organization:*
Update a templatetemplate.update on template:<template-id>
Publish or make privatetemplate.publish on template:<template-id>
Remove a templatetemplate.delete on template:<template-id>
Create a project from a templatetemplate.read on template:<template-id> and the corresponding project creation scope

An owner has full access in a user session. assistant, member, and organization API Keys need the specific grants. Creating the application and database together also requires database.create on organization:*. Templates do not create Key-Value, Cache, or Queue.

List the official public catalog

Use this route in websites, showcases, and other integrations that need to display official Templates without opening a user session:

GET /v1/public/templates?sort=most_used&page=1&limit=12

Do not send Authorization or x-organization-id with this request.

ParameterRequiredValues and default
sortNomost_used (default), newest, or updated
pageNoPage starting at 1; default 1
limitNoFrom 1 to 24 items; default 12

The response includes only public, active Templates from active official organizations. most_used sorts by unique consumer organizations, then completed deployments and creation date. newest uses the creation date; updated uses the last update. A deterministic tie-breaker keeps pagination stable.

Each public item contains only id, name, summary, description, tags, official, author.organization_name, requires_database, and updated_at. Application configuration, variables, plan defaults, usage data, and organization identifiers are not part of this contract.

Example response structure:

{
  "status": "success",
  "data": {
    "items": [
      {
        "id": "507f1f77bcf86cd799439011",
        "name": "Support API",
        "summary": "Starting point for an HTTP API.",
        "description": "Official template for starting an API.",
        "tags": ["api"],
        "official": true,
        "author": {
          "organization_name": "Zenifra"
        },
        "requires_database": true,
        "updated_at": "2026-09-03T12:00:00.000Z"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 12,
      "total": 1,
      "pages": 1
    }
  }
}

List templates with authentication

GET /v1/templates?scope=organization&sort=updated&page=1&limit=20
ParameterRequiredValues
scopeYesorganization or public
sortYesupdated, newest, or most_used
officialNotrue or false; available with scope=public
pageNoPage starting at 1
limitNoUp to 50 items

organization returns templates accessible in the active organization and public returns published templates. The order follows sort: updated uses the last update, newest the creation date, and most_used ranks by unique consumer organizations and uses completed deployments as a tie-breaker. Use official=true to return only Templates published by Zenifra or official partners.

Each returned Template includes only the public name of the author organization. The organization identifier is not shared. owned_by_current_organization indicates whether the Template belongs to the active organization, while official indicates whether the author is an official publisher. Both fields are informational and do not grant permissions.

{
  "author": {
    "organization_name": "Author organization"
  },
  "owned_by_current_organization": false,
  "official": true
}

Read, create, and update

GET /v1/templates/:id
POST /v1/templates
PATCH /v1/templates/:id

Create and update bodies contain name, summary, description, tags, application, variables, and defaults. The application contains reference, port, and storage_path; defaults contain plan, billing, exposure, instances, auto-scaling, and storage. A template that needs a database can also include an optional database_dependency and, for extra databases, additional_database_dependencies.

Use a full public image reference from an OCI-compatible registry, with domain, path, and tag, such as codeberg.org/forgejo/forgejo:16.0.5. Docker Hub, GHCR, Quay, and GitLab Registry remain supported. Zenifra validates the application before accepting a revision; the registry must be reachable over HTTPS and allow public access to the image. Do not send credentials, tokens, or secret values in a template.

Every update requires expected_revision:

{
  "expected_revision": 2,
  "name": "support-api v2",
  "summary": "HTTP API for new projects",
  "description": "A safe starting point for an API.",
  "tags": ["api", "support"],
  "application": {
    "reference": "ghcr.io/example/support:2.1.0",
    "port": 3000,
    "storage_path": "/app/data"
  },
  "variables": [
    { "name": "APP_ENV", "description": "Environment", "required": true, "secret": false, "default_value": "production" },
    { "name": "APP_SECRET", "description": "Application secret", "required": true, "secret": true }
  ],
  "defaults": {
    "plan": "basic",
    "payment_mode": "monthly",
    "exposure": "public",
    "instances": 1,
    "storage": { "persistent": true, "capacity": 10 }
  }
}

An outdated revision returns 409 Conflict. Secret variables cannot have default_value.

Database dependency

Get the current database catalog before creating or editing a dependency:

GET /v1/project/database/catalog

The response lists engine IDs, plans, billing modes, configuration fields, and connection fields. Use these IDs rather than maintaining a fixed list in your integration. A database_dependency declares compatible engines, one recommendation for each engine, and optional variable bindings:

{
  "database_dependency": {
    "compatible_engine_ids": ["postgresql", "mariadb"],
    "recommended_engine_id": "postgresql",
    "recommendations": [
      { "engine_id": "postgresql", "plan_id": "db-basic", "payment_mode": "monthly", "configuration": { "version": "18", "instances": 1, "storage_capacity_gb": 10 } },
      { "engine_id": "mariadb", "plan_id": "db-basic", "payment_mode": "monthly", "configuration": { "version": "11", "instances": 3, "storage_capacity_gb": 10 } }
    ],
    "environment_bindings": [
      { "variable_name": "DB_PASSWORD", "connection_field_id": "password" },
      { "variable_name": "DB_HOST", "connection_field_id": "host" }
    ]
  }
}

Each binding must reference a declared variable and a connection field supported by every compatible engine. Bindings for sensitive fields require a secret variable and cannot have a default value. Non-sensitive bindings can provide a suggested value for an external database; a combined deployment replaces it with the newly created connection. Omitting database_dependency creates a template without a database dependency. When updating, omit it to preserve the current dependency or send null to remove it.

To create more than one database, include additional_database_dependencies, a list of up to 4 dependencies with the same shape as database_dependency. The list requires database_dependency, and each variable can appear in the bindings of only one database. ClickHouse also offers the https_port and https_url connection fields. When updating, omit the list to preserve the additional databases or send [] to remove them.

If the database connection variables are not valid for the template, the update returns 400 TEMPLATE_DATABASE_DEPENDENCY_INVALID.

Publish and remove

PATCH /v1/templates/:id/visibility
DELETE /v1/templates/:id

The visibility body is { "visibility": "public", "expected_revision": 2 }. Creation starts private. Publishing or making a template private requires template.publish; removal requires template.delete and expected_revision. Removal is logical and does not alter existing projects.

Create a project from a template

Use the alternate payload of POST /v1/project:

{
  "template_id": "507f1f77bcf86cd799439011",
  "template_revision": 2,
  "name": "support-api",
  "description": "Project created from a template",
  "plan": "basic",
  "payment_mode": "monthly",
  "config": {
    "exposure": "public",
    "instances": 1,
    "storage": { "persistent": true, "capacity": 10 },
    "envs": [
      { "name": "APP_ENV", "value": "production" },
      { "name": "APP_SECRET", "value": "provided-at-deploy-time" }
    ],
    "network_access": {
      "ingress_white_list": [{ "cidr": "0.0.0.0/0", "description": "Project access" }],
      "ingress_black_list": []
    }
  }
}

The application, port, HTTP type, and persistent path come from the template revision. The request must contain exactly the declared variables. Deployment requires project.create and, for a private template, template.read. Failed deployments do not count usage or change ranking.

Create an application and database together

For a template with a database dependency, create both resources with:

POST /v1/templates/:id/deployments
Idempotency-Key: <unique-key-with-at-least-16-characters>

Idempotency-Key is required and accepts 16 to 200 characters (A-Z, a-z, 0-9, ., _, and -).

The body includes expected_revision, the regular application configuration, and the selected database:

{
  "expected_revision": 2,
  "application": { "name": "support-api", "plan": "basic", "payment_mode": "monthly", "config": { "exposure": "public", "instances": 1, "storage": { "persistent": true, "capacity": 10 }, "envs": [{ "name": "APP_ENV", "value": "production" }], "network_access": { "ingress_white_list": [], "ingress_black_list": [] } } },
  "database": { "name": "support-db", "engine_id": "postgresql", "plan_id": "db-basic", "payment_mode": "monthly", "configuration": { "version": "18", "instances": 1, "storage_capacity_gb": 10 } }
}

The request requires project.create and database.create. The response contains a deployment ID, status, and project IDs only; it never contains connection credentials. Reuse the same idempotency key and identical body to safely retry. A changed body with the same key returns 409 Conflict.

Zenifra validates the application (plan, plan features such as auto-scaling, minimum storage, capacity and image), the database plan, the organization's pending payments and the plan limits before creating any resource. In these cases the response has no deployment_id and nothing was created. Possible codes: 400 TEMPLATE_DEPLOYMENT_INVALID (including for a template from a deleted organization), 402 ORGANIZATION_BILLING_BLOCKED, 409 FREE_PLAN_INSTANCE_LIMIT_REACHED, 409 FREE_PLAN_INSTANCE_LIMIT_EXCEEDED, 409 DB_FREE_PROJECT_LIMIT_REACHED, 409 INSUFFICIENT_PLAN_CAPACITY, 409 TEMPLATE_REVISION_CONFLICT, 409 IDEMPOTENCY_KEY_CONFLICT (key already used by another template or without read access) and 503 TEMPLATE_DEPLOYMENT_UNAVAILABLE. A platform domain sent as a custom domain is refused with 400 by request validation. When the key was already used with other data on this same template, the response is 409 IDEMPOTENCY_KEY_CONFLICT with the deployment_id of the original deployment. If the request is refused after the database was created (for example, 409 DOMAIN_UNAVAILABLE for a custom domain already used by another project, or a capacity change mid-operation), the response includes deployment_id and Zenifra reverts what was created; follow it with the request below until the failed state. A template from a deleted organization returns 404 TEMPLATE_NOT_FOUND on GET /v1/templates/:id and when creating a project from the template.

GET /v1/template-deployments/:deploymentId

When the template declares additional_database_dependencies, also send additional_databases: a list with one database per additional dependency, in the same order and with the same shape as database. The application and every database need different names. In the ready state, the response includes additional_databases with the project_id and engine_id of each extra database.

Use this endpoint to follow creating, ready, rolling_back, or failed. A successful database and application remain independent projects after creation.

If the creation response is lost, recover the deployment by the organization's idempotency key:

GET /v1/template-deployments/idempotency/:idempotencyKey

The response has the same shape as the lookup by deploymentId; it returns 404 when there is no readable deployment with that key.

Limits

The official public catalog is limited to 120 requests per minute per IP. Authenticated reads are limited to 100 requests per minute. Creation and updates are limited to 10 per minute. Publishing and removal are limited to 5 per minute. POST /v1/templates/:id/deployments is limited to 5 per minute, and GET /v1/template-deployments/* to 60 per minute. Respect 429 responses and wait before retrying.

Next steps

Last updated on

On this page