Create a release
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'ssso_providersor a schema'sx-auth-methods.sso.providers. A missing one answersrel.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 indetails.
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.
oauth2 release.writeAuthorizationBearer <token>Scope: release.write
project_id*stringThe unique identifier of the project
^[a-zA-Z0-9_-]+$application/json- 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.
1 <= items <= 50message?stringA short summary of what the release changes, analogous to a git commit message. Recorded on the release and shown when listing releases.
length <= 1024git_sha?stringThe source commit the caller was operating from.
length <= 64git_dirty?booleanSet when the working tree had uncommitted changes, so the release content
does not correspond exactly to git_sha.
falseA release pinning this exact set already existed and was returned unchanged. No new release was created.
application/json- 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-idThe opaque, immutable resource id, assigned at construction.
^rel_[a-zA-Z0-9_-]+$project_id*project-idThe project this release belongs to.
^[a-zA-Z0-9_-]+$content_hash*ReleaseContentHashThe digest of the pinned content. Identical for two releases with the same content, which is why a project never holds two such releases.
^sha256:[0-9a-f]{64}$71 <= length <= 71metadata*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.
1 <= items <= 50import { 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" },);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?