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:
| Header | Required | Description |
|---|---|---|
x-api-key | Automation operations | Organization API key kept in a secret. |
x-organization-id | With a user token | Active project organization. An organization API key is already linked to its organization. |
Content-Type: application/json | For requests with a body | Indicates 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/v1In 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
| Goal | Method and path |
|---|---|
| Read settings | GET /project/:projectId/preview-environments/settings |
| Update settings | PATCH /project/:projectId/preview-environments/settings |
| Read inherited plan | GET /project/:projectId/preview-environments/plans |
| Read preview costs | GET /project/:projectId/preview-environments/billing |
| List previews | GET /project/:projectId/preview-environments |
| Create or update a preview | PUT /project/:projectId/preview-environments/:previewKey |
| Read a preview | GET /project/:projectId/preview-environments/:previewKey |
| Remove a preview | DELETE /project/:projectId/preview-environments/:previewKey |
| Read an operation | GET /project/:projectId/preview-environments/:previewKey/operations/:operationId |
Project settings
Read settings
GET /project/:projectId/preview-environments/settingsReturns 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/settingsRequires 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/billingRequires 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/plansReturns 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-environmentsLists 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:
| Field | Description |
|---|---|
id | Public preview identifier. |
key | Stable key identifying the environment in the project. |
status | Product state such as accepted, provisioning, available, deleting, deleted, or failed. |
url | Public URL when available. |
plan | Selected plan. |
payment_mode | The preview uses hourly billing. |
exposure | Exposure configured for the environment. |
inherit_envs | Always true: variables from the primary project are inherited. Values are never returned. |
updated_at and expires_at | Most 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/:previewKeyCreates or updates the preview identified by previewKey. An upsert body accepts:
| Field | Type | Required | Description |
|---|---|---|---|
image | string | Yes for upsert | Reference to the ready-to-use image to publish (up to 2,048 characters). |
inherit_envs | boolean | No | Legacy compatibility field; the Preview always inherits user variables inside Zenifra. |
ttl_hours | integer | No | Duration in hours; default 24, minimum 1, maximum 168. |
source | object | No | Source 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/:previewKeyRequires 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/:previewKeyRequests 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/:operationIdRequires 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:
acceptedreservingprovisioningupdatingavailabledeletingdeletedfailed
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:
| Status | Code | When it happens |
|---|---|---|
400 | preview_disabled | Preview Environments are not enabled for the project or organization. Also applies to operations with an organization API Key while the opt-in is off |
400 | preview_plan_not_allowed, preview_ttl_invalid, preview_ttl_exceeds_limit, preview_limit_invalid | Plan, duration, or limit outside what is allowed |
409 | PARENT_CONFIGURATION_UNAVAILABLE | The primary project lacks enough public configuration to apply the preview |
409 | PREVIEW_OPERATION_CONFLICT | An active, incompatible operation already exists for the same identity |
409 | PREVIEW_ORGANIZATION_LIMIT_REACHED, PREVIEW_PROJECT_LIMIT_REACHED | The effective organization or project limit was reached |
502 | PREVIEW_PROVISIONING_FAILED | The preview could not be provisioned |
503 | ORGANIZATION_RUNTIME_UNAVAILABLE | The 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