Docs

Create a release

POST
/releases

Assembles a release from existing revisions, given as (kind, revision_id) pairs. To build one from files on disk, use POST /releases/build.

Each revision must exist in the project and is recorded under the handle it declares. The release must be complete and consistent:

  • Every handle its revisions name must be in the release: a flow's user_schema, and the IDP connections named by a flow step's sso_providers or a schema's x-auth-methods.sso.providers. A missing one answers rel.unresolved_reference, naming the resource and the handle, e.g. flow_definition default-login: user_schema human-user not found.
  • Every flow is validated as on save, against the user schema revision in the release. A flow that fails answers rel.invalid, with the reason in details.

Idempotent on the set of revisions: the same revisions with a different message answer 200 with the existing release. A new release answers 201. Creating a release does not deploy it.

Authorization

oauth2 release.write
headerAuthorizationBearer <token>

Scope: release.write

Query Parameters

project_id*string

The unique identifier of the project

Match^[a-zA-Z0-9_-]+$

Request Body

application/json
  1. body

The set of revisions to bundle into a release, plus the metadata to record alongside it.

created_at and created_by are derived server-side from the caller's authentication context and are not accepted here.

pointers*array<>

The revisions to pin. Must not be empty.

A kind usually appears more than once: a project has several user schemas and several flow definitions, and each is pinned on its own. What it cannot do is pin two revisions of the same resource — a second revision of the human-user schema is rejected rather than ordered, since a release describes one state of the project.

The upper bound counts resources, not revisions — one release pins at most one revision of each — so it is a limit on how much a project configures, not on how often it changes. It keeps the server's per-pointer revision lookups bounded by the contract rather than by the request, and is set conservatively: raising it is a spec change, and widening a bound breaks no client that was already inside it.

Items1 <= items <= 50
message?string

A short summary of what the release changes, analogous to a git commit message. Recorded on the release and shown when listing releases.

Lengthlength <= 1024
git_sha?string

The source commit the caller was operating from.

Lengthlength <= 64
git_dirty?boolean

Set when the working tree had uncommitted changes, so the release content does not correspond exactly to git_sha.

Defaultfalse

Response Body

A release pinning this exact set already existed and was returned unchanged. No new release was created.

application/json
  1. response

An immutable, project-scoped snapshot that pins one revision of every resource it includes.

A release owns pointers and metadata, not content — it records which revisions belong together. Callers that need the resource bytes resolve each revision_id through the per-kind read endpoints (GET /schemas/{id}, GET /flow_definitions/{id}, …).

A release exists on its own and is not tied to an origin. Deploying it is a separate call.

Two identifiers: id is the one paths and release_id fields take; content_hash is the digest of the content the release pins, printed by the CLI and pinned by builds, and looked up through fields that name it.

id*release-id

The opaque, immutable resource id, assigned at construction.

Match^rel_[a-zA-Z0-9_-]+$
project_id*project-id

The project this release belongs to.

Match^[a-zA-Z0-9_-]+$
content_hash*ReleaseContentHash

The digest of the pinned content. Identical for two releases with the same content, which is why a project never holds two such releases.

Match^sha256:[0-9a-f]{64}$
Length71 <= length <= 71
metadata*

Who assembled the release, when, and from what source. Set when the release is assembled and never mutated afterwards.

revoked_at*|

When the release was revoked, or null while it is not. A revoked release cannot be deployed and is refused on every path that would serve it, including to a build that pins its digest. No origin serves a revoked release, since a release an origin still serves cannot be revoked; earlier deployments that named it stay in the history.

pointers*array<>

The revisions this release pins, one entry per (kind, handle). Never empty: a release must contain at least one resource, and never longer than the set POST /releases accepts.

Items1 <= items <= 50
Example Request
import { createZitadelClient } from "@zitadel/api/client";const client = createZitadelClient({  baseUrl: process.env.ZITADEL_URL ?? "http://localhost:8080",  token: process.env.ZITADEL_PROJECT_SECRET!, // server-side only});const release = await client.createRelease(  {    pointers: [      {        kind: "schema",        revision_id: "https://example.com/schemas/default-human-user.json",      },      {        kind: "schema",        revision_id: "sch_01KWHF2XY6RN7F0N9WD3P2E4YM",      },      {        kind: "flow_definition",        revision_id: "flowdef_01KWHG09JXA7F0N9WD3P2E4YM5",      },      {        kind: "flow_definition",        revision_id: "flowdef_01KWHG3B72K7M7F0N9WD3P2E4",      },      {        kind: "branding",        revision_id: "brnd_01KWH1P4MYS7F0N9WD3P2E4YM5",      },      {        kind: "idp",        revision_id: "idprev_01KWHF2XY6RN7F0N9WD3P2E4YM",      },    ],    message: "add phone_number to human-user",    git_sha: "4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7",    git_dirty: false,  },  { project_id: "proj_01KWH1P4MYS7F0N9WD3P2E4YM5" },);
Example Response

The pinned set matched an existing release, so that release is returned — note the message it was originally created with, not the one just submitted.

{  "id": "rel_01KX3RG8A7F0N9WD3P2E4YM5C1",  "project_id": "proj_01KWH1P4MYS7F0N9WD3P2E4YM5",  "content_hash": "sha256:9f2c1a7b4e83d05f6c2b19ae7d430f821c6b5de90a4f73829f2c1a7b4e83d05f",  "metadata": {    "message": "add phone_number to human-user",    "git_sha": "4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7",    "git_dirty": false,    "created_at": "2026-07-14T09:11:00Z",    "created_by": "user_01KWH3B72K7M7F0N9WD3P2E4YM",    "created_by_type": "human"  },  "revoked_at": null,  "pointers": [    {      "kind": "schema",      "handle": "human-user",      "revision_id": "sch_01KWHF18816ZQE7F0N9WD3P2E4"    },    {      "kind": "flow_definition",      "handle": "default-login",      "revision_id": "flowdef_01KWHG09JXA7F0N9WD3P2E4YM5"    },    {      "kind": "branding",      "handle": "default",      "revision_id": "brnd_01KWH1P4MYS7F0N9WD3P2E4YM5"    }  ]}

Was this helpful?