Preview Environments — API

Use these endpoints to configure and operate Preview Environments nested under a primary HTTP project. A preview has its own identity, URL, capacity, storage, and billing lifecycle, but it can only be accessed through the primary project it belongs to.

The operation is idempotent by organization, primary project, and previewKey. PUT and DELETE may return an asynchronous operation; query the operation endpoint until it reaches a terminal state.

Authentication and security

Include a credential with the corresponding project permissions and, for automation, an organization API key:

HeaderRequiredDescription
x-api-keyAutomation operationsOrganization API key kept in a secret.
x-organization-idWith a user tokenActive project organization. An organization API key is already linked to its organization.
Content-Type: application/jsonFor requests with a bodyIndicates a JSON body.

The owner must enable Preview Environments in the console before an organization API key can operate previews. The key stays limited to the primary project and its configured guardrails: it cannot change settings, entitlements, limits, or disallowed plans.

For the minimum operational lifecycle, grant project.read, project.preview.read, project.preview.deploy, and project.preview.delete on the primary project. project.preview.configure remains reserved for initial configuration by an authorized user. To read costs, also grant project.billing.read.

Public responses never include environment variables, credentials, private image URLs, resource names, or internal operational details. Treat the API Key as a secret and never log it.

Base URL and identifiers

https://api.zenifra.com/v1

In the examples, PROJECT_ID represents the primary HTTP project ID and API_KEY represents an organization API key kept in a secret. Replace only these placeholders with values from your organization.

export PROJECT_ID="your-project-id"
export API_KEY="your-api-key"
export USER_TOKEN="your-user-token"
export ORGANIZATION_ID="your-organization-id"
export BASE_URL="https://api.zenifra.com/v1"

previewKey may contain only letters, numbers, periods, underscores, and hyphens with 1 to 100 characters. Do not silently alter a rejected key.

Endpoints

GoalMethod and path
Read settingsGET /project/:projectId/preview-environments/settings
Update settingsPATCH /project/:projectId/preview-environments/settings
Read inherited planGET /project/:projectId/preview-environments/plans
Read preview costsGET /project/:projectId/preview-environments/billing
List previewsGET /project/:projectId/preview-environments
Create or update a previewPUT /project/:projectId/preview-environments/:previewKey
Read a previewGET /project/:projectId/preview-environments/:previewKey
Remove a previewDELETE /project/:projectId/preview-environments/:previewKey
Read an operationGET /project/:projectId/preview-environments/:previewKey/operations/:operationId

Project settings

Read settings

GET /project/:projectId/preview-environments/settings

Returns the primary project's opt-in and effective guardrails. The public response includes enabled, the project's inherited plan in default_plan/allowed_plans, default_ttl_hours, max_ttl_hours, max_active_environments, active_environments, organization_limits, and the native preview configuration in github and forgejo. A Preview cannot select a different plan from the primary project. Like the update, reading the settings requires a user session (an organization API Key is not accepted) and project.preview.read.

curl -sS "$BASE_URL/project/$PROJECT_ID/preview-environments/settings" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "x-organization-id: $ORGANIZATION_ID"

Example response:

{
  "status": "success",
  "data": {
    "enabled": true,
    "default_plan": "basic",
    "allowed_plans": ["basic"],
    "default_ttl_hours": 24,
    "max_ttl_hours": 168,
    "max_active_environments": 2,
    "active_environments": 1,
    "github": {
      "enabled": true,
      "target_branch": "main"
    },
    "forgejo": {
      "enabled": false
    },
    "organization_limits": {
      "enabled": true,
      "max_active_environments": 10,
      "max_per_project": 2,
      "max_ttl_hours": 168,
      "active_environments": 3
    }
  }
}

Update settings

PATCH /project/:projectId/preview-environments/settings

Requires a user session (an organization API Key is not accepted) and project configuration permission. max_active_environments accepts 0 to 1000. The API does not allow changing the organization entitlement or exceeding the effective limit.

{
  "enabled": true,
  "default_ttl_hours": 24,
  "max_ttl_hours": 168,
  "max_active_environments": 2,
  "github": {
    "enabled": true,
    "target_branch": "main"
  }
}

default_ttl_hours and max_ttl_hours must be between 1 and 168, and the default cannot be greater than the maximum. The hourly price always comes from the primary project's plan.

By default, github.enabled is false. This field enables the native flow for an HTTP project whose source is a GitHub repository. github.target_branch defines the pull request target branch that can create previews and is required when github.enabled is true; when the flow is disabled, it may be omitted. The GitHub configuration is independent from auto-deploy: it does not change the branch or update policy for the primary application. For projects with a Forgejo source, the forgejo object accepts the same fields (enabled and target_branch, required when enabled); see Deploy from Forgejo.

When the native flow is active, pull requests in the project's own repository targeting the selected branch use the pr-<number> identity on opened, reopened, and synchronize. The edited event reevaluates a target branch change and closed removes the preview. Pull requests from forks do not participate, and pull requests that were already open before activation are not imported automatically; the next relevant event starts evaluation.

The native flow uses the runtime and commands already configured for the project and does not require a GitHub Action, API Key, IMAGE, or repository workflow. The PUT /project/:projectId/preview-environments/:previewKey endpoint remains the path for the Action and other automations that publish a ready-to-use image.

Disabling github.enabled or the global opt-in, changing github.target_branch, or disconnecting/changing the repository removes the affected native previews and invalidates pending publications. The primary application and its auto-deploy configuration remain separate from this cleanup.

Preview costs

GET /project/:projectId/preview-environments/billing

Requires project.billing.read. Returns the accrued cost of active Previews, the current hourly rate, and an estimate until expiration for each environment and for the group.

Monetary values use BRL minor units, matching the existing billing contract. For example, 5.4 means R$ 0.054. accrued_amount includes hourly snapshots calculated through as_of; pending_amount identifies value still awaiting consolidation. estimated_until_expiration is an estimate, not a final charge, and does not include the current hour until it has been consolidated.

{
  "status": "success",
  "data": {
    "currency": "brl",
    "as_of": "2026-08-25T22:00:00.000Z",
    "previews": [
      {
        "preview_id": "preview-id",
        "key": "pr-42",
        "status": "running",
        "plan": "basic",
        "hourly_rate": 5.4,
        "accrued_amount": 10.8,
        "settled_amount": 5.4,
        "pending_amount": 5.4,
        "estimated_until_expiration": 21.6,
        "expires_at": "2026-08-26T02:00:00.000Z",
        "billing_status": "pending"
      }
    ],
    "summary": {
      "active_previews": 1,
      "accrued_amount": 10.8,
      "settled_amount": 5.4,
      "pending_amount": 5.4,
      "hourly_rate": 5.4,
      "estimated_until_expiration": 21.6
    }
  }
}

Plans

List preview plans

GET /project/:projectId/preview-environments/plans

Returns only active plans allowed for previews in this project. Each item provides the plan identifier, public description, hourly price, and currency (BRL). The preview default and common plans appear only when explicitly allowed for this use.

curl -sS "$BASE_URL/project/$PROJECT_ID/preview-environments/plans" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "x-organization-id: $ORGANIZATION_ID"

The catalog price is the source of truth. Do not copy commercial values into the Action or a client application.

List previews

GET /project/:projectId/preview-environments

Lists previews nested under the primary project. The response contains data.items and data.pagination.

curl -sS "$BASE_URL/project/$PROJECT_ID/preview-environments" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "x-organization-id: $ORGANIZATION_ID"

Each item may contain these public fields:

FieldDescription
idPublic preview identifier.
keyStable key identifying the environment in the project.
statusProduct state such as accepted, provisioning, available, deleting, deleted, or failed.
urlPublic URL when available.
planSelected plan.
payment_modeThe preview uses hourly billing.
exposureExposure configured for the environment.
inherit_envsAlways true: variables from the primary project are inherited. Values are never returned.
updated_at and expires_atMost recent update and current expiration.

The list does not return environment variables, data, credentials, hashes, leases, counters, or resource names.

Native GitHub runs

The response from GET /project/:projectId/preview-environments also includes data.github_runs and data.forgejo_runs, with the native pull request runs for each provider (empty lists when there are none). These sections do not change the format of Preview items and share the same format. Reading it uses the same project.preview.read permission on the primary project.

{
  "data": {
    "items": [],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 0
    },
    "github_runs": {
      "items": [
        {
          "preview_key": "pr-42",
          "pull_request_number": 42,
          "status": "building",
          "commit_sha": "abc123...",
          "branch": "feature/example",
          "target_branch": "main",
          "repository": "example-org/example-repository",
          "updated_at": "2026-09-16T18:30:00.000Z"
        }
      ],
      "pagination": {
        "page": 1,
        "limit": 20,
        "total": 1
      }
    }
  }
}

Public run states are queued, building, updating, available, failed, blocked, removing, and removed. error may appear only when a run fails and contains a normalized product error with code and message.

While a run is pending or failed, it does not represent an available Preview: there is no URL, running application, or environment billing value yet. When the run reaches available, query the Preview list or detail to obtain the environment's public data. A removing run represents the shutdown of its associated Preview, and removed keeps the historical record after the environment no longer exists.

If a run fails, read error and send a new commit or produce another relevant pull request event to start a new attempt. The platform does not promise to retry automatically when the result of an attempt cannot be confirmed.

Create or update a preview

PUT /project/:projectId/preview-environments/:previewKey

Creates or updates the preview identified by previewKey. An upsert body accepts:

FieldTypeRequiredDescription
imagestringYes for upsertReference to the ready-to-use image to publish (up to 2,048 characters).
inherit_envsbooleanNoLegacy compatibility field; the Preview always inherits user variables inside Zenifra.
ttl_hoursintegerNoDuration in hours; default 24, minimum 1, maximum 168.
sourceobjectNoSource metadata sent by the official Action: provider: "github_action" and, optionally, event, repository, pull_request_number, branch, base_branch, and commit_sha.

The preview always uses the primary project's plan and synchronizes its port, exposure, and access rules. Storage is new and starts empty for the preview. Data, custom domains, and custom image startup commands are not inherited.

curl -sS -X PUT \
  "$BASE_URL/project/$PROJECT_ID/preview-environments/pr-42" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "x-organization-id: $ORGANIZATION_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "docker.io/library/nginx@sha256:6784fb0834aa7dbbe12e3d7471e69c290df3e6ba810dc38b34ae33d3c1c05f7d",

    "ttl_hours": 24
  }'

The API responds 202 with { "status": "accepted", "data": { "operation": {...}, "preview": {...} } }; operation.operation_id identifies the operation to poll. If an active operation has an incompatible payload, the API returns 409 (PREVIEW_OPERATION_CONFLICT) and you should query the existing operation.

Read a preview

GET /project/:projectId/preview-environments/:previewKey

Requires project.preview.deploy. Returns the current state, URL, plan, hourly BRL price when available, inherited environment state, timestamps, and current operation. It does not return environment variable values.

curl -sS \
  "$BASE_URL/project/$PROJECT_ID/preview-environments/pr-42" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "x-organization-id: $ORGANIZATION_ID"

Remove a preview

DELETE /project/:projectId/preview-environments/:previewKey

Requests immediate removal of the preview and ends its billing when removal is confirmed. The operation is idempotent: repeating the delete does not create another environment or another charge.

curl -sS -X DELETE \
  "$BASE_URL/project/$PROJECT_ID/preview-environments/pr-42" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "x-organization-id: $ORGANIZATION_ID"

The API responds 202 with { "status": "accepted", "data": { "operation": {...}, "preview": {...} } }. When the preview is already removed, a repeat returns the terminal state or an equivalent success response. The Action does not require IMAGE for this path.

Operation polling

Read an operation

GET /project/:projectId/preview-environments/:previewKey/operations/:operationId

Requires project.preview.deploy. Use the operation_id returned in data.operation by PUT or DELETE to follow the operation. Poll at bounded intervals and stop at a terminal state.

Possible states:

  • accepted
  • reserving
  • provisioning
  • updating
  • available
  • deleting
  • deleted
  • failed

An upsert succeeds at available; a delete succeeds at deleted. At failed, read the public code and correct the input before trying again.

curl -sS \
  "$BASE_URL/project/$PROJECT_ID/preview-environments/pr-42/operations/operation-id" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "x-organization-id: $ORGANIZATION_ID"

The operation response contains only state, public identifiers, timestamps, the URL when available, expires_at, and a sanitized public error. It does not contain secrets or internal details.

Public errors

Errors use { "status": "failed", "code": "...", "message": "..." }. Codes stay stable and messages are actionable:

StatusCodeWhen it happens
400preview_disabledPreview Environments are not enabled for the project or organization. Also applies to operations with an organization API Key while the opt-in is off
400preview_plan_not_allowed, preview_ttl_invalid, preview_ttl_exceeds_limit, preview_limit_invalidPlan, duration, or limit outside what is allowed
409PARENT_CONFIGURATION_UNAVAILABLEThe primary project lacks enough public configuration to apply the preview
409PREVIEW_OPERATION_CONFLICTAn active, incompatible operation already exists for the same identity
409PREVIEW_ORGANIZATION_LIMIT_REACHED, PREVIEW_PROJECT_LIMIT_REACHEDThe effective organization or project limit was reached
502PREVIEW_PROVISIONING_FAILEDThe preview could not be provisioned
503ORGANIZATION_RUNTIME_UNAVAILABLEThe organization environment is temporarily unavailable; try again shortly

preview_wait_timeout is reported by the official Action when it reaches its maximum wait time; the API does not return this code. Never expose the full response in public logs when it is part of an automation.

HTTP status codes

  • 200: successful read.
  • 202: upsert or delete accepted for processing, always with { "status": "accepted", "data": { "operation", "preview" } }.
  • 400: invalid body, key, action, or duration, or opt-in turned off.
  • 401: missing or invalid credential.
  • 403: insufficient organization, project, or permission.
  • 404: primary project, preview, or operation not found.
  • 409: conflict with an active operation, incompatible payload, limit reached, or primary project configuration unavailable.
  • 502, 503: the preview could not be provisioned, or the organization environment is temporarily unavailable.

Rate limits (counted per source IP): list reads 50 per minute; reading one preview 100 per minute; PUT and DELETE 10 per minute; operation reads 120 per minute; costs 30 per minute.

Next steps

Last updated on

On this page