API operations guide
Authenticate, manage resources, deploy releases, and inspect results through HTTP.
On this page
- Export secret variables
- Curated step and workflow templates
- Base URL
- Project groups
- CLI
- Auth
- Agent fleet (admin only)
- Project templates
- Deploy flow (the common ask)
- Post-deployment verification and rollback
- Gates (know the 422s)
- Scheduling
- Request bodies
- Runbooks
- Inspect variables while authoring steps
- Endpoint cheat sheet
- Getting help
- Passing files between deployment steps
- Generic ZIP packages
Export secret variables
Require an instance administrator with the global admin role.
Project administrator membership does not grant export permission.
Use a global bearer token with administrator permission.
Send POST /api/v1/admin/secret-exports with {"scope":"project","scope_id":123}.
Use "scope":"lifecycle" for shared lifecycle variables.
The response returns 201 with id, approval_url, claim_token, and expires_at.
The request expires after five minutes.
Keep claim_token private.
Open the relative approval_url on the same instance in a browser.
Sign in as the user who owns the requesting token.
Confirm the displayed scope.
Complete fresh password or OAuth/OIDC reauthentication.
For SSO-only accounts, use Use single sign-on without a local password.
Keep the Authentik session active when you confirm another export.
If confirmation fails immediately after an Authentik sign-in, wait two seconds and start a new confirmation.
If local MFA is enabled, complete the required factor check.
Each export requires a new confirmation.
Send POST /api/v1/admin/secret-exports/{id}/download with {"claim_token":"<claim_token>"} and the original bearer token.
Keep the approving browser session active until the download completes.
The response returns a JSON attachment with scope, scope_id, and variables.
Each variable contains id, name, plaintext value, and nullable environment_id.
Project exports contain local project secrets.
Lifecycle exports contain shared lifecycle secrets.
Exports exclude non-secret values, historical snapshots, account passwords, and other stored credentials.
Store the downloaded file in a secure location.
Keep secret values and claim credentials out of URLs, logs, and tickets.
Do not retry a successful download with the same request.
An export permits one download and records an audit event without secret values.
Invalid, expired, canceled, consumed, or unapproved requests return 404.
Insufficient administrator or token permissions return 403.
Curated step and workflow templates
Browse GET /api/v1/template-library with a global token.
Preview GET /api/v1/template-library/{slug} before applying an entry.
The library contains 19 entries across Bash, PowerShell 7, Python 3, and a Terraform workflow.
Append -powershell or -python to an existing Bash slug for its Linux-host variant.
Read the preview’s scripts, configuration, execution target, dependencies, and permissions.
For scoped tokens, use these selected-project routes:
GET /api/v1/projects/{id}/template-library
GET /api/v1/projects/{id}/template-library/{slug}
POST /api/v1/projects/{id}/template-library/{slug}/apply
Supply the preview version, reviewed: true, agent_selectors, and variable_values keyed by the preview’s variable keys.
Supply required values and fresh optional secrets through variable_values.
Do not include credentials in URLs.
Use a unique agent label for a single target host.
Each matching agent runs the selected interpreter’s Linux and Compose steps in host mode.
Prepare the documented tools, paths, services, and permissions before deployment.
{
"version": "1",
"reviewed": true,
"agent_selectors": ["app-host-01"],
"variable_values": {"HEALTH_URL": "https://app.example.test/health"}
}
This request applies http-health-check.
Successful application returns 201 with the project, slug, version, and created-record counts.
It appends independent editable steps and encrypted variable definitions in one transaction.
It starts no deployment and changes no existing release snapshot.
Existing project or inherited lifecycle variable names return 409 without changes.
A stale template version also returns 409.
Refresh the preview before retrying a stale version.
Missing required values, invalid configuration, and unknown agent labels return 422.
Missing entries return 404.
Effective Maintainer permission is required to apply templates.
Viewer and Deployer permission ceilings return 403.
Successful application records apply_library_template without secret values.
Create a release and deploy separately after reviewing the editable copies.
For terraform-plan-apply, omit agent_selectors or supply an empty array.
Supply TF_CONTAINER_IMAGE and TF_CONFIG_DIR through variable_values.
Use a reviewed image containing Bash, Python 3, Terraform, providers, configuration, and .terraform.lock.hcl.
Configure a remote backend with state locking.
Supply fresh optional TF_HTTP_USERNAME and secret TF_HTTP_PASSWORD values for an HTTP backend.
Configure other backend credentials through editable secret variables and step restrictions before deployment.
The copied Terraform steps run in server containers with bridge networking.
They have no host mounts, agent execution, or host fallback.
The plan step pauses for artifact approval before the apply step.
Keep terraform-context absent from managed staging before the plan step.
An existing context fails the step before Terraform runs.
Inspect the exact saved plan before approval.
The apply step uses only that approved plan and fails if remote state has changed.
Copying the workflow does not run Terraform or create infrastructure.
DurpDeploy is a single-binary deployment tool that runs ordered script steps against environments. It exposes three surfaces an agent may use:
- JSON API at
/api/v1/*— bearer-token auth, no CSRF. Prefer this. - Web UI routes (
/login,/projects, …) — session cookie + CSRF token, HTML form semantics, redirect status codes. - CLI subcommands on the
durpdeploybinary (user/token admin).
The endpoint reference is the Swagger UI at /api/swagger/
(no auth) on a running server. Use it alongside this workflow guide.
Swagger requests use the current instance’s host, port, and scheme.
Check API target before you authorize a token.
Base URL
Replace $BASE (e.g. https://durpdeploy.example.com or
http://localhost:8080) throughout. The server listens on :8080 by
default behind a reverse proxy in production. /healthz is public and checks
database connectivity, returning 503 when unavailable. /api/v1/healthz is a
public API liveness probe returning {"ok":true} without a database check.
Call GET /api/v1/version with a bearer token to read the server executable identity.
All authenticated roles can read version, build_number, commit, and modified.
Unknown commits and build numbers use the string unknown.
Open /settings/version to read the same values in the web UI.
CI derives the build number from the source commit count.
Untagged CI builds use a development version with the count and source SHA.
Tagged releases preserve their semantic version separately from OCI image tag normalization.
Administrators can call GET /api/v1/admin/database-security with a bearer token.
The response describes configured database transport policy, not a live TLS session.
It contains backend, policy, and an optional warning without connection details or credentials.
Treat plaintext_allowed, tls_unverified, tls_ca_verified, and tls_pinned as operator-approved exceptions with warnings.
tls_verified requires server verification.
local identifies SQLite or PostgreSQL Unix-socket transport.
Project groups
Project groups organize projects without changing access permissions.
Each project response includes a nullable group_id.
Existing projects remain in Ungrouped until assigned.
Call GET /api/v1/project-groups to list visible groups and their accessible project_count values.
The response contains items, total, limit, and offset.
Only global administrators using global access can see empty groups.
Scoped tokens see only groups containing selected, accessible projects.
Use a global administrator with a global Admin token to manage groups.
Legacy global administrator tokens without a ceiling also work.
Create a group with POST /api/v1/project-groups and {"name":"Team"}.
Rename a group with PUT /api/v1/project-groups/$GROUP_ID and the same body shape.
Delete a group with DELETE /api/v1/project-groups/$GROUP_ID.
Deletion returns 204 and moves projects to Ungrouped.
Names contain 1 to 100 characters after trimming.
Names must be unique without case differences.
Control characters and the name Ungrouped are invalid.
Invalid names return 422.
Duplicate names return 409.
Assign a project with PUT /api/v1/projects/$PID/group and {"group_id":12}.
Move a project to Ungrouped with {"group_id":null}.
Always include group_id in assignment requests.
The global role, project membership, and token ceiling must each permit Maintainer access.
The destination group must be visible to the caller.
Unknown or inaccessible destinations return 404.
Use Manage Groups in the web interface to change project assignments. Manage Groups opens a modal above the Projects list. Use New Group to create a group. Use Cancel to close the modal. Changes save when you apply them. Cancel does not undo changes. Project Maintainers can move authorized projects there. Select projects with checkboxes before you select Move to group. Select all includes only editable projects on the current page. Moves run one project at a time with the existing assignment endpoint. Successful moves remain saved if a later move fails. Only global administrators can create, rename, or delete groups.
Search projects with GET /api/v1/projects?q=application.
Search matches project names and descriptions.
ASCII case does not affect search.
Search treats wildcard characters as literal characters.
Filter with group_id=12, or use group_id=0 for Ungrouped.
Omit group_id to include all accessible groups.
Set sort to newest, name, or name_desc.
The default sort is name, which sorts project names from A to Z.
Creation time and project ID resolve equal names in descending order.
Use limit and offset to paginate matching projects.
The default limit is 100 and the maximum is 1000.
The web list uses the same project filters and pagination. Group headings appear alphabetically, followed by Ungrouped. Sorting controls project order within each displayed group. Pagination selects projects before the web list adds group headings. A group can appear on more than one page. Changing a filter starts from the first page.
CLI
Run on the host with the server’s database (matching DURPDEPLOY_DB):
durpdeploy admin create --email X --password Y # first admin, runs migrations
durpdeploy tokens create --user admin@example.com --name ci # prints token ONCE
durpdeploy tokens create --user admin@example.com --name author --scope scoped --role maintainer --project-id 12
durpdeploy tokens list --user admin@example.com
durpdeploy audit prune --days 180
Auth
API tokens (recommended for agents)
Mint a token on the server host with the CLI above, or in the web UI at
/settings/tokens if scripting: POST the form name=<label>&csrf_token=$CSRF,
then GET the redirect Location (an opaque single-use flash reference,
/settings/tokens?flash=<id> — never the token itself) and take the plaintext
from the one-time banner in that page’s body. The flash is consumed on first
read and expires if abandoned. Then:
TOKEN="ddp_pat_..."
api_get() { curl -s -H "Authorization: Bearer $TOKEN" "$@"; }
api_post() { curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-X POST -d "$1" "$2"; }
api_get "$BASE/api/v1/users/me" # sanity check: who am I
Use POST /api/v1/users/me/password to change your own local password.
Send an unrestricted global bearer token and these JSON fields:
current_password, new_password, and password_confirmation.
Use matching new passwords with at least eight characters.
New passwords must leave room for all password fields, email, and CSRF in the 64 KiB encoded form limit.
Do not include line breaks in new passwords.
The operation supports every user role, including Viewer.
Project-scoped tokens and tokens with a permission ceiling receive 403.
Invalid credentials or password fields receive 422.
Success returns 204 and invalidates all browser sessions and pending MFA challenges.
API tokens and enrolled MFA factors remain valid.
Sign in again with the new password.
For SSO-only accounts, change the password with the identity provider.
Forgotten-password recovery is unavailable.
- No CSRF, no cookies. Errors are JSON envelopes (
400/401/404/422). - Global tokens cover all projects available to the owner.
- Both scopes support a
viewer,deployer,maintainer, oradminpermission ceiling. - Legacy global tokens without a role inherit the owner’s current permissions.
- Scoped tokens also require a selected project and a
viewer,deployer,maintainer, oradminpermission ceiling. - Viewer tokens cannot write, execute deployments, or approve operations.
- Deployer tokens can start existing releases, retry, re-run, and cancel deployments.
- Retries and re-runs recheck lifecycle promotion and approval gates.
- Deployer tokens cannot author content, manage schedules or tokens, run runbooks, request rollback, or approve operations.
- Maintainer tokens permit authoring and runbook execution within existing administrator boundaries.
- Global roles, project membership roles, and token ceilings each limit project operations.
- Scoped
admintokens grant no instance administration. - Project membership and global-administrator approval requirements still apply to the owner.
- Browser MFA protects web logins only — API tokens are single bearer credentials and keep working.
Create a project-scoped token through an authorized Maintainer or administrator session, a legacy global token, or the server CLI.
Select one or more accessible projects and a role in /settings/tokens.
Use this API request for the same operation:
api_post '{"name":"ci","scope":"scoped","role":"deployer","project_ids":[12,34]}' "$BASE/api/v1/tokens"
The creation response returns the bearer value once in id.
Store token_id as its non-secret management identifier.
Token lists show scope, role, and project_ids without the bearer value.
Name-only API requests and legacy web or CLI creation retain global access.
The web form defaults to All accessible projects.
The permission selector defaults to viewer for either scope.
Select Selected projects to set a project selection.
Use {"name":"ci-reader","scope":"global","role":"viewer"} to create an all-project reader through the API.
Explicit scoped requests reject empty selections, invalid roles, and inaccessible projects.
For an otherwise valid scoped request without projects, token creation and scope updates return 422 with select at least one project.
Replace scope with PATCH /api/v1/tokens/{token_id} using scope, role, and project_ids.
Provide an explicit scope on updates.
Use {"scope":"global"} only when you intend to remove restrictions.
Use DELETE /api/v1/tokens/{token_id} to revoke the token.
Scoped tokens and tokens with a permission ceiling cannot access token management APIs.
Use the owner’s browser session or a global token for these operations.
Scoped project lists, deployment lists, activity, and totals exclude unrelated projects. Shared lifecycle reads require assignment to a selected project. Environment reads follow selected projects’ lifecycle eligibility and historical usage. Projects without a lifecycle retain access to all environments. Global templates, shared lifecycle variable administration, and shared-resource writes are unavailable to scoped tokens. Inherited project variables retain existing secret masking.
Owner permission changes, token changes, expiry, and revocation affect new requests and existing log streams.
These changes do not cancel work already accepted by the server.
Creation, scope changes, and revocation record non-secret identifiers in the audit log.
Administrators can filter GET /api/v1/admin/audit with user for an exact current name or email.
ASCII case is ignored.
Combine user with user_id, action, or entity_type to narrow results.
Each entry retains its IDs and adds actor_name, actor_email, and a relative target_url.
An empty target_url means no unambiguous supported target record exists.
Existing scoped database rows without valid grants fail authentication.
Migration rollback is refused because older applications ignore token restrictions.
The four-role upgrade migrates existing Deployer users, memberships, and explicit token ceilings to Maintainer.
Legacy global tokens continue to inherit the owner’s current role.
Configure the OIDC Maintainer group to preserve authoring access on the next OIDC login.
Web session + CSRF (only when driving the UI)
Unauthenticated browser page requests redirect to /login?next=<local-path>.
Fetch that login page with a cookie jar before submitting credentials to preserve the destination.
Successful password, MFA, and SSO login returns to that local page with normal authorization checks.
External and invalid destinations fall back to /.
Failed password login returns 422, retains the email, and leaves the password empty.
Direct /login requests start without a saved destination.
curl -c /tmp/dd-cookies -X POST \
-d "email=you@example.com&password=SECRET" "$BASE/login" # 303 on success
CSRF=$(curl -s -b /tmp/dd-cookies "$BASE/" \
| grep -oP '<meta name="csrf-token" content="\K[^"]+' | head -1)
Token placement rules:
- POST/PUT/PATCH: append
csrf_token=$CSRFto the form body. - DELETE: pass the header
X-CSRF-Token: $CSRF— Go does not parse form bodies on DELETE, so a body-only token 403s. - Status contract:
303= success (id or path inLocation),422= validation failure,403= invalid CSRF. Viewer application writes return200with anHX-Triggertoast for HTMX, or a styled403for native requests.
Agent fleet (admin only)
The dedicated mTLS agent listener defaults to TCP port 23187.
The web listener remains on port 8080.
Pairing advertises DURPDEPLOY_AGENT_PUBLIC_URL to the agent.
Explicit listener and public URL port overrides remain supported.
Existing agents retain their stored server endpoint after an upgrade.
Follow the port migration instructions in docs/agents.md before changing existing endpoints.
All paths below use the /api/v1 prefix and require an admin token.
GET /admin/agentslists agents withadministrative_status,draining,health,current_work,queued_work,last_successful_deployment,last_error,server_version,recommended_agent_version, andcompatibility. The nullableagent_versionidentifies agent-reported software separately fromagent_protocol. A missing version remains unknown until the agent supplies it through polling.GET /admin/agents/$IDalso includes capabilitylabelsandenvironment_labels(environment routing labels).POST /admin/agents/$ID/drainandPOST /admin/agents/$ID/resumereturn204, including repeats;404if missing and409unless active/paired. Drain blocks new claims, retains waiting work, and allows already-issued work to finish. Later remote steps wait for Resume. State survives restart. Waiting on drained targets pauses their timeout; issued work still times out.administrative_statusisdrainingwhile issued work remains, thendrainedonce none remains. Both retaindraining: trueand block claims until Resume. The existingstatusfield keeps the base registration state (e.g.active).POSTorDELETE /admin/agents/$ID/environmentsaccepts{"environment_id":N}and returns204; invalid IDs return400, missing resources return404. Repeated add is harmless.- Before a downgrade without staging support, drain the agent and wait for its jobs to finish.
Revoke the agent through
POST /admin/agents/$ID/revokebefore the downgrade. Re-pair the agent after the downgrade. Revocation and re-pairing clear its previous staging capability. - Capability label changes use
POSTorDELETE /admin/agents/$ID/labelswith{"label":"linux"}. Routing changes and Drain/Resume are audited.
Health is healthy, stale (120 seconds), offline (600 seconds), or
unknown. It is separate from administrative status and uses server time.
Before the first heartbeat, health deadlines use the latest completed pairing
time, so re-pairing starts a new grace period. Draining agents still heartbeat.
Global notifications emit each stale/offline
transition and recovery once; delivery is best effort and does not block claim
maintenance. Alerts stay ordered; a full in-memory queue skips new alerts and
logs a warning without retry. Last error is a stable
outcome code with a deployment link for redacted logs. Compatibility confirms
the observed supported protocol, while agent versions remain unverified.
Project templates
Project templates belong to their source project. Current source-project membership controls access. Saving or applying a template requires effective Maintainer permission on that project. Applying a template also requires project creation permission. Scoped tokens can read and save templates through selected project routes. Scoped tokens cannot create projects or read the global project-template list.
- Save a snapshot with
POST /api/v1/projects/{id}/project-templates. Supply{"name":"Reusable workflow","description":"Project template"}. - Browse accessible templates with
GET /api/v1/project-templates?limit=50&offset=0. UseGET /api/v1/projects/{id}/project-templatesfor one source project. - Preview a snapshot with
GET /api/v1/projects/{id}/project-templates/{templateId}. - Review the ordered steps, copied defaults, and reference hints in
snapshot. - Create a project with
POST /api/v1/projects/{id}/project-templates/{templateId}/projects.
Use this creation body:
{
"name": "New project",
"description": "Independent copy",
"reviewed": true,
"lifecycle_id": 0,
"environment_mappings": {"12": 34},
"variable_values": {"2": "new secret"},
"agent_selectors": {"1": ["production"]},
"package_repository": {
"url_template": "https://packages.example.com/{version}/app.zip",
"auth_type": "bearer",
"username": "",
"credential": "new credential"
}
}
Use variable key values from the preview in variable_values.
Supply a nonempty value for every secret definition.
Non-secret values use their copied defaults unless overridden.
In the web form, select Use replacement value to override a non-secret default.
Leave this checkbox clear to preserve the original default exactly, including line breaks.
If you omit description, creation uses the snapshot description.
Supply an empty description to clear it.
Map every nonzero source environment_id to an existing destination environment.
Environment mappings must match the selected lifecycle.
Only global administrators can assign a lifecycle.
Use 0 for no lifecycle.
Agent selectors remain unchanged unless overridden. Use one-based step positions as selector keys. Use an empty array to clear selectors explicitly. Selected labels must exist when creation runs. Review broad selectors before creation.
If the snapshot includes a package source, supply package_repository with fresh credentials.
Omit this object when the snapshot has no package source.
Templates never contain secret-variable values or package credentials.
Scripts and non-secret defaults remain verbatim.
Review them for embedded credentials before reuse.
Successful save and creation requests return 201.
Creation returns the new project.
Invalid references or missing secrets return 422 without partial creation.
Duplicate names return 409.
Unauthorized operations return 403.
A template under the wrong source project returns 404.
Both creation operations record audit events.
The snapshot excludes runbooks, lifecycle definitions, shared lifecycle values, notifications, schedules, releases, deployments, memberships, and API tokens. Creation starts no deployment. Source edits do not change templates or created projects. Deleting a source project removes its templates. Created projects and release snapshots remain independent.
Deploy flow (the common ask)
Administrators can read delivery history with GET /api/v1/admin/notifications.
Each results field contains a JSON string that maps channels to delivery statuses.
Email status ok: sent without TLS means delivery succeeded over plaintext.
Treat this status as a transport warning, not a delivery failure.
Verified STARTTLS delivery returns ok.
Deployments and runbook executions share a durable FIFO queue per project and
environment pair. Only one item owns that pair, including while it waits for agents or
verification. Later eligible items have status queued; approval waits do not
own a slot. Approval makes work eligible in original creation order without
preempting active work. Different projects in the same environment and the
same project in different environments can execute independently.
Deployment GET/status responses and runbook execution GET responses include
queue_position (1-based for queued work, otherwise 0), plus
active_deployment_id and active_work_url when that work is visible to the
caller. Viewers can read queue state. Authorized writers can cancel queued
work through the existing cancel endpoint; this does not signal the active
deployment. Server restart preserves order and repairs missed launches.
Queued runbook cancellation returns status: "cancelled"; active runbook
cancellation returns status: "cancellation_requested" while stopping work.
Unconfirmed container cleanup, lost agents, or unacknowledged cancellation
keep that project/environment pair blocked: a timeout is not proof that execution stopped.
GET /api/v1/deployments/$ID/status includes waiting_for_agents. It is
true while remote work is queued with no issued claim, and false once
claimed or terminal. The web deployment page shows “Waiting for agents”
while this flag is true; polling updates it automatically.
- Project
POST /api/v1/projects{"name":"my-app"}→ reply hasid. - Environment
POST /api/v1/environments{"name":"prod"}→id. Environments can carry tags; agents can be routed by tag later. - Steps
POST /api/v1/projects/$PID/steps:
{
"name": "build", "script_body": "make build",
"interpreter": "bash",
"sort_order": 1, "timeout_seconds": 300, "max_retries": 0,
"execution_target": "local",
"container_image": "registry.example/build@sha256:<digest>",
"variable_names": ["API_URL"]
}
local means mandatory server-container execution; specify an image with
bash, pwsh, or python3 installed. agent runs on matching remote agents.
Local step containers have no DurpDeploy-imposed RAM ceiling or CPU quota,
including when sharing deployment artifacts. Host/runtime resource policy
still applies; account for workloads consuming more RAM and CPU. PID limits,
temporary-filesystem bounds, and other container isolation remain enforced.
Remote-agent resource limits depend on the installed agent version; the
matching change is tracked in
agent issue #14.
agent_execution_mode defaults to host, which rejects a container image.
For remote containers, set execution_target: "agent",
agent_execution_mode: "container", and a valid container_image.
This requires an agent/3 agent reporting a ready Docker or Podman runtime
and support for the selected container interpreter. API requests may use powershell; DurpDeploy
normalizes it to pwsh. All resolved release variables enter a step by
default; set variable_names only to restrict the step to those names. Local
steps exclude container/SSH client configuration names such as PATH, HOME,
SSH_AUTH_SOCK, or XDG_*; agent-host steps retain their host variable support.
Agent-container steps apply the same reserved-name restrictions as local containers.
Steps, template versions, releases, and runbook versions preserve these fields.
An empty variable_names list passes all resolved variables; a non-empty
list restricts that step, including remote host steps.
The admin agent API reports execution_modes, container_runtimes, and
container_interpreters from the last valid poll.
Upgrade the server before enabling agent containers. Keep old agents on host
steps. Do not downgrade the server while container work is pending or cleanup
is unresolved. Downgrading an agent clears its container capabilities and
fails its incompatible waiting work before claim.
An agent result of cleanup_unconfirmed blocks the environment queue, retry,
and project/environment deletion until that agent reconciles its containers
and reports a ready v3 poll.
The original terminal result remains visible after confirmation.
Re-pairing a revoked agent returns 409 while its remote execution or cleanup
remains unconfirmed. Reconcile its workloads before revocation; a replacement
installation cannot confirm cleanup for the old installation.
Administrators may remove an agent from inventory with
DELETE /api/v1/admin/agents/{id} (empty body or {}). Success is 204; an
unknown/deleted agent returns 404; unresolved execution, cleanup, or buffered
logs return 409 and leave the agent unchanged. Drain and reconcile workloads
and log delivery first.
Deletion removes the server registration, credentials, and configuration.
Deployments, snapshots, logs, and step attempts remain with agent references
cleared; agent-owned remote protocol state is removed. The same identity can
pair through POST /api/v1/admin/agents/pair as a new registration with a new ID
and default configuration; no tombstone blocks it. Stop the reconciled idle
agent, move only state.json out of its
private state directory, preserve its identity keys, and restart to obtain a
fresh code. The bootstrap listener validates that code; an incorrect code does
not restore the deleted registration. The existing
POST /api/v1/admin/agents/{id}/revoke keeps the agent visible for later re-pairing.
Pairing and recovery recheck the registration after the agent’s completion
acknowledgement using a separate bounded context, including after an ACK timeout.
If deletion or revocation wins during that exchange, pairing returns 409
instead of reporting the removed registration as paired.
After a heartbeat or cancellation timeout, the same paired agent can replay
its durable terminal report with the original claim token. This resolves the
remote uncertainty without changing a failed deployment to success. A late
cleanup_unconfirmed report still requires a ready v3 poll before retry.
The embedded agent pulls an image when it is missing. Container steps default
to no network; local steps can opt into network_mode: "bridge". They have no
host mounts. A mutable image tag does not
freeze image contents; prefer a digest. The web/API rejects a new image-less
server step. Old image-less releases remain readable but cannot run, re-run,
or refresh; recreate their steps and create a new release (409 on launch).
- Variables (optional)
POST /api/v1/projects/$PID/variables{"name":"API_URL","value":"...","environment_id":N}— resolved at deploy time for the target environment. Omitenvironment_id(or usenull) for Unscoped. On projects bound to a lifecycle, create and update accept only its stage environments; other IDs return422. Projects without a lifecycle can use any environment. Use 1–255 ASCII letters, digits, or underscores for project and lifecycle variable names. Start each name with a letter or underscore. Unsupported nonempty project names return422on creation and updates. Empty project names retain their400validation response.ARTIFACT_PATH,DURPDEPLOY_STAGE_DIR, andDURPDEPLOY_APPROVED_DIRremain reserved. - Release (snapshot of current steps + variables)
POST /api/v1/projects/$PID/releases{"version":"1.2.0"}→id. Optionally supplyrepository_url,commit_sha,artifact_ref, andci_run_urlas top-level JSON fields. Use HTTPS URLs without credentials, query parameters, or fragments. Use a full hexadecimal SHA-1 or SHA-256 value forcommit_sha. Use a public HTTPS URL or artifact identifier forartifact_ref. OCI references can include an@sha256:digest. Each field accepts at most 2048 bytes without whitespace or control characters. Never include secrets in provenance fields. Recognized credential formats are rejected with422. Provenance stays unchanged during refresh. Release reads include these fields. Deployment detail reads include aprovenanceobject. Release and deployment pages, notifications, and creation audit details also show provenance. A duplicate version returns409with an instruction to enter a different version. Changeversionbefore retrying. With an attached package repository, creation freezes its source, URL and version without downloading. Missing versions and unreachable repositories do not block creation. The artifact endpoint returnspending: true, an emptysha256andsize: 0until the first validated deployment pull. Deployment pull failures stop execution before any steps run. The first successful validated pull locks SHA-256 and size across all copies of that package generation; later deployments reject changed bytes. The package is retained, not omitted. Refresh downloads and validates again, replacing the release checksum and size for future deployments while existing deployment and runbook copies retain their original generation. Refresh also updates steps and variables and refuses active or unconfirmed deployments. Later step edits do NOT affect it;POST /projects/$PID/releases/$RID/refreshre-snapshots the release, including after failed or successful deployments. Active or unconfirmed deployments and buffered agent logs return409; wait for completion and log flushing, then retry. Existing deployment steps and pinned artifacts stay unchanged. New deployments use the refreshed release; re-runs use the original deployment steps and artifact pin, with the release’s current variables. Refresh also cannot upgrade an old image-less release.DELETE /api/v1/projects/$PID/releases/$RIDreturns204when removed,404if absent or in another project, and409if it has active or unconfirmed deployments. It also deletes schedules and terminal deployment history, including logs; this cannot be undone. - Deploy
POST /api/v1/projects/$PID/deployments{"release_id":$RID,"environment_id":$EID}→201with deploymentid. For either creation endpoint, optionally send anIdempotency-Keyheader. Use 1 to 255 visible ASCII characters without spaces. Keep the same key and JSON field values when retrying a request. JSON field order and insignificant JSON whitespace do not affect matching. Matching retries return the original201response withIdempotency-Replayed: true. Reuse with different field values returns409. Keys are scoped to the authenticated user, project, and creation operation. Another token for the same user can retry the request. Each retry must pass current authentication and authorization checks. Keys survive restarts and concurrent requests without an expiry. Failed creation transactions do not consume keys. Deleted results remain replayable while the owning user and project exist. A replay never recreates a resource or starts deployment execution. Use a new key for each intended creation. - Poll until terminal or operator action is required:
for i in {1..150}; do
S=$(api_get "$BASE/api/v1/deployments/$DID/status" | python3 -c 'import sys,json;print(json.load(sys.stdin)["status"])')
[[ "$S" =~ ^(failed|succeeded|cancelled|rejected|expired|cleanup_unconfirmed|pending_approval|awaiting_artifact_approval)$ ]] && break
sleep 0.5
done
- Act on state:
awaiting_artifact_approval→ listGET /api/v1/deployments/$DID/artifact-gates, use the current gate’sstep_indexas$STEP, download/$STEP/artifactunder that gate URL, and independently inspect it with trusted tools. Review counts are unverified step claims. An admin then sends the listed revision and artifact SHA-256 to/$STEP/approveor/$STEP/rejectunder that gate URL. After approval, return to polling; another step can require another review. Follow the artifact-gate details below.rejectedorexpired→ stop polling. Resolve the rejection or expiry, then redeploy to generate a fresh artifact and review.pending_approval→ an adminPOST /api/v1/deployments/$DID/approvewith an empty body or{}unblocks it. The authenticated admin is recorded as the approver. Non-admin tokens get 403.- failure →
GET /api/v1/deployments/$DID/logs(JSON array, secrets are redacted) andGET /.../logs.txt; fix and create a new release, then redeploy withPOST /api/v1/deployments/$DID/redeploy. POST /api/v1/deployments/$DID/cancelcancels queued work or requests cancellation of a running deploy.cleanup_unconfirmedmeans container removal failed. Retry and redeploy return409while cleanup remains unconfirmed. For local execution, a successful startup runtime sweep changes the deployment tofailed. For agent execution, a ready v3 poll confirms cleanup and permits retry or redeploy while preserving the original terminal result.
Post-deployment verification and rollback
GET /api/v1/projects/$PID/deployments returns deployments for that project.
GET /api/v1/deployments returns deployments from accessible projects.
Both lists sort by creation time, newest first.
When creation times match, larger deployment IDs come first.
GET /api/v1/deployments/activity returns 14 UTC calendar days, including
today, in ascending order: [{"date":"2026-10-04","counts":{"succeeded":2}}].
Each counts object groups deployments by their current status; dates with
no deployments have {}. Only accessible projects are counted (all projects
for global admins). Runbook executions are excluded. The home-page charts use
these counts. The API requires a bearer token; the equivalent
GET /dashboard/activity endpoint uses the authenticated web session.
Home charts refresh every 30 seconds.
Deployment panels refresh every five seconds.
Only global admins can configure verification, because environments are
shared across projects. POST /api/v1/environments and
PUT /api/v1/environments/$EID accept:
{
"name": "prod",
"verification_type": "http",
"verification_target": "https://service.example.com/health",
"verification_timeout_seconds": 30
}
Types are "" (disabled), http, and bash. Timeout defaults to 30 seconds
and must be 1–300. An update that omits verification fields preserves them.
Environment responses omit verification_target for non-admin callers.
Targets and frozen verification snapshots are encrypted at rest and included
in secret-key rotation. Bash snapshots retain selected variables without storing a
second plaintext copy of the check script. Explicit HTTP ports must be 1–65535.
The configuration is frozen on deployment creation, including deployments
waiting for approval. HTTP performs one server-side GET; only 2xx succeeds.
Redirects, URL user credentials, loopback, link-local/metadata addresses
(including Alibaba ECS’s 100.100.100.200 and AWS’s fd00:ec2::254),
environment proxies, and DNS rebinding are rejected; private unicast service
addresses are allowed. Use a release secret variable for sensitive Bash values;
arbitrary literals embedded in a script are not automatically identified as secrets.
Bash runs after all steps in a fixed, digest-pinned official Bash 5.2 server
container, using the last step’s selected variables and artifact mount. Project
images and agents never receive the check script, including for older snapshots.
The server container runtime is required even for all-agent deployments. Tools
from project images and remote host/network access are unavailable. Containers
retain the existing no-network restrictions; Bash uses privileged startup mode,
with no retries. Shell startup and loader variables (BASH_ENV, ENV, SHELLOPTS,
BASHOPTS, CDPATH, GLOBIGNORE, PS4, GCONV_PATH, GLIBC_TUNABLES, and LD_*)
are excluded by default and rejected when explicitly selected. Bash verification
requires at least one step.
An empty-step Bash deployment is rejected with 422. A scheduled attempt with
this configuration is disabled with an actionable last_error.
Verification output uses the same secret scrubber as deployment logs.
HTTP output also redacts the frozen URL, path segments, and query names and values,
including their URL-encoded forms. Hostnames and DNS labels are redacted without
case sensitivity, including Unicode and IDNA forms, so an echoed request cannot
expose configuration credentials.
Failure marks the deployment failed, records a verification audit event, and
emits the standard failure notification. Cancellation waits for container
cleanup as normal.
GET /api/v1/deployments/$DID/verification returns safe metadata (type,
timeout, status, timestamps); commands and URLs are omitted. A deployment
without a check returns {"status":"disabled"}. The deployment detail page
also shows verification status and redacted output in its logs.
Rollback is manual:
GET /api/v1/deployments/$DID/rollbackpreviews exact source and target versions, environment, and lifecycle/approval state. It selects the latest earlier successful different release in the same project/environment.- Confirm
target_deployment_idfrom that preview withPOST /api/v1/deployments/$DID/rollbackand{"target_deployment_id":123}. Success returns201with a new deployment. The web confirmation is/deployments/$DID/rollback. - Poll, approve, cancel, and inspect that new deployment as usual.
The source must be the latest terminal deployment and no execution in that
project/environment may still be active or unconfirmed. Missing history,
stale confirmation, or an overlapping submission returns 409; a lifecycle
gate returns 422. Rollback cannot force a gate. Viewers cannot submit it;
project authorization and admin-only approval still apply. The new deployment
reuses the prior successful deployment’s frozen steps and package pin and the
release’s current snapshot of variables, then runs the environment’s current
verification. An explicit release refresh replaces those variables for future
deployments, re-runs, and rollbacks; historical variable values cannot be
reconstructed. It records rollback provenance and an audit entry.
Mutable container image tags
also remain mutable; use digests when exact image contents matter.
Gates (know the 422s)
Enforced identically on web and API (source: internal/gate/gate.go):
- Lifecycle order: projects bound to a lifecycle deploy along its stage
order. Skipping the previous stage →
422(bypassable withforce=trueon the deploy request). Deploying to an environment outside the lifecycle →422and force does not help. - Approval: stages with
requires_approvalpark atpending_approvalinstead of running. Approve is admin-only. - Membership: non-admin tokens need to be in the project; unknown or
forbidden projects return
404/403. - Lifecycle assignment: only a global admin can attach a lifecycle on
project creation or change its assignment. Other project admins can retain
or remove an existing assignment. Assignment grants execution access to
the lifecycle’s shared variables, including secrets; unauthorized attempts
return
403before project creation or edits. - Parent deletion: deleting environments or lifecycles is global-admin
only because deletion also removes scoped/shared variables. Lifecycle
deletion is
DELETE /api/v1/lifecycles/$IDand returns204; projects using it become unassigned while existing snapshots remain intact. - Lifecycle stages: adding, updating, reordering, or removing stages is
global-admin only (
403otherwise). Restoring a removed stage reactivates its retained scoped shared values for assigned projects. Create a stage withPOST /api/v1/lifecycles/{id}/stagesandenvironment_id. A positivesort_orderselects an unused position and can leave gaps. Omitted, zero, and negativesort_ordervalues append after the highest position. An occupied position or duplicate environment returns409without a stage change. A conflicting stage update also returns409and identifiesenvironment_idorsort_order. UsePOST /api/v1/lifecycles/{id}/stages/reorderwith every stage ID instage_ids. Reordering assigns consecutive positions from one in the requested sequence. - Cross-project release: deploying a release id under the wrong
project →
400.
Scheduling
POST /api/v1/projects/$PID/schedules with release_id, environment_id,
5-field cron (e.g. * * * * *), optional note, enabled=true. The
scheduler ticks once per minute — never test sub-minute cron expectations.
Schedule creation and updates return 404 if the selected release is deleted
before the write commits.
An old image-less server release cannot be scheduled. Existing schedules
pointing at one disable on their due run and expose an actionable last_error.
Request bodies
API JSON bodies and /api/lint bodies are limited to 4 MiB (4,194,304
encoded bytes), including all script text, escaping, and JSON overhead.
Send exactly one JSON object matching the documented request schema. Unknown
fields (including nested fields), top-level null, additional documents, and
trailing non-whitespace data return 400 {"error":"Invalid JSON body"}.
Oversized input returns 413 {"error":"Request body too large"} before any
mutation, including chunked requests. Whitespace after the object is allowed
and counts toward the ceiling. Control actions with no fields accept an empty
body or {}; additional fields are rejected.
General web request bodies, including multipart bodies, have a 16 MiB encoded byte ceiling and return 413 when exceeded. Package repository forms have a 64 KiB ceiling. Login and password reauthentication retain their separate 64 KiB limits and existing 400/422 error responses. Scripts must fit within the applicable ceiling after JSON or form encoding; runbooks share one JSON ceiling across all steps. These are server limits; proxies may impose lower limits.
Runbooks
Runbooks save immutable versions of ordered steps. Create one with
POST /api/v1/projects/$PID/runbooks and a JSON body containing name and
steps (name, script_body, optional interpreter, timeout_seconds,
max_retries, execution_target, agent_selectors, container_image, and
variable_names). Local runbook steps require an image. Save the next
version with PUT /api/v1/projects/$PID/runbooks/$BID and steps.
POST /api/v1/projects/$PID/runbooks/$BID/executions takes
environment_id and optional version_id; omit the version or pass 0
for the latest. Its response includes the runbook execution id and the
underlying deployment_id. Read the execution at
/api/v1/projects/$PID/runbook-executions/$XID, logs at /logs, and live
logs at /logs/stream (SSE by default, ?format=ndjson for NDJSON).
Delete a runbook with DELETE /api/v1/projects/$PID/runbooks/$BID.
Send an empty body.
Successful deletion returns 204 without a response body.
Missing, deleted, or inaccessible runbooks return 404.
Viewers receive 403 for API writes.
Unfinished executions or unconfirmed cancellation and cleanup return 409 with an explanation.
Finish or cancel executions before deletion.
Confirm cleanup before you repeat deletion.
Deletion removes the runbook from active lists and disables all its schedules. Deletion prevents new executions, retries, saved versions, and schedules. Saved versions, backing releases, artifact pins, completed executions, and logs remain for history. Deleted runbook names remain reserved within the project. Deletion does not provide restoration or permanent history removal. Existing project and environment deletion policies still apply to retained history.
Read retained executions and logs through the existing runbook execution endpoints.
The runbook_deleted_at field uses the existing nullable integer format.
Its Valid field is true when the runbook is deleted.
Runbook definition and version endpoints return 404 after deletion.
Use GET /api/v1/deployments/$DID/logs/page for bounded history.
Runbooks support GET /api/v1/projects/$PID/runbook-executions/$XID/logs/page.
Responses contain items, next_before, last_id, limit, before, byte_limit, and line_byte_limit.
Each page contains at most 500 entries, ordered by increasing persisted ID.
The JSON response also has a 1 MiB byte budget, including its envelope.
Display text fields have a 16 KiB UTF-8 budget, including truncation markers.
Shortened entries include truncated: true.
Byte limits can reduce the number of entries on a page.
Complete persisted output remains available through full streams and exports.
Omit before for the newest page.
Pass a nonzero next_before as ?before=ID to retrieve the preceding page.
A zero next_before means no older page remains.
Explicit zero, negative, malformed, or overflowing cursors return 400.
Resume structured streams after last_id.
The existing /logs endpoints retain their complete JSON arrays.
Runbooks also support /logs.txt for complete text export.
Deployment and runbook pages retain at most 500 displayed entries and 1 MiB of previews. Use Older logs to view earlier output with live updates paused. Use Latest logs to resume live updates. Use Export all logs to download complete history. Deployment step states remain available when their output leaves the displayed window. Older output pages show current step states.
HTTP headers must arrive within 5 seconds, and the complete request body within 30 seconds from the start of the request read. Ordinary responses have a 60-second write budget. Deployment/runbook SSE and NDJSON streams have no total lifetime limit, but each event write/flush must finish within 60 seconds. Consume streams continuously and reconnect after disconnects. An operator’s reverse proxy may impose additional limits.
Deployment logs also support structured SSE at
GET /api/v1/deployments/$DID/logs/stream?format=structured. Each log
event has an SSE id and JSON fields id, line, step, step_index
(nullable for legacy/unassigned output), created_at, display_timestamp,
and optional state
(waiting, running, succeeded, failed, or cancelled). Step indices
are zero-based snapshot positions; duplicate names remain separate. State
events are trusted lifecycle records, not parsed script output. created_at
is the persisted Unix time in seconds. display_timestamp formats that time
as YYYY-MM-DD HH:mm:ss in the server’s local timezone, matching timestamps
on the deployment page before and after refresh. Resume with
Last-Event-ID or an initial after query parameter. A terminal deployment
drains its logs, sends event: complete with JSON status, then closes;
clients should close their EventSource on that event. Default SSE/NDJSON and
plain-text export retain their formats.
Queued cancellation saves a cancelled lifecycle log for each frozen step.
Each log explains Not run: cancelled while queued and why no output exists.
The web page retains this explanation after reload.
Cancellation after execution starts retains its existing step states and output.
Add preview=true to a structured deployment or runbook stream to receive bounded display text.
Shortened events include truncated: true and a visible truncation marker.
Omit preview, or use preview=false, to receive complete text.
Invalid preview values return 400 before the stream starts.
Preview streams preserve persisted IDs, lifecycle states, replay cursors, and the final completion event.
Execution location entries appear in the existing line field before script output.
Server container attempts identify the physical host, runtime, container name, image reference, interpreter, and attempt number.
Remote steps also record the selected agent name and ID.
Remote log entries include the authenticated agent_id to distinguish interleaved fan-out output.
Embedded line separators appear as escaped text.
Agent attempts distinguish container from host/shell and record each retry and fan-out target.
Unavailable physical hostnames appear as unknown.
Containerized host agents without runtime information can set DURPDEPLOY_AGENT_HOSTNAME to the physical machine hostname.
Historical logs retain their original information.
Default text SSE also emits complete after terminal log replay.
Close its EventSource on that event.
Initial text replay preserves remote sequence order and checkpoints its cursor after replay.
Subsequent text events use persisted log IDs.
Resume text SSE with Last-Event-ID or after.
An interrupted initial replay repeats history instead of skipping out-of-order rows.
NDJSON retains line and step fields and closes after terminal replay.
Live NDJSON keeps its existing empty step field.
All stream formats recover missed broker notifications from persisted logs.
Open streams recheck credentials, current roles, and project membership each second.
Revocation ends responsive streams within approximately two seconds.
A blocked consumer can delay closure up to 62 seconds because event writes retain their 60-second deadline.
Authorization failures and database errors close the stream.
Runbook streams use the execution’s project membership.
Errors before a structured stream starts use the usual JSON error envelope,
including invalid cursors, missing deployments, and startup failures.
Project and global notification credentials are write-only, including for administrators.
GET and PUT responses return empty slack_webhook_url, gotify_token, and discord_webhook_url fields.
The corresponding *_configured fields report whether stored credentials exist.
Blank or omitted credential fields preserve stored values in PUT requests.
Set clear_slack_webhook_url, clear_gotify_token, or clear_discord_webhook_url to true to clear a credential.
Clear flags take precedence over replacements.
The server retains credentials for delivery.
Existing write permissions still apply.
Email recipients and Gotify server URLs remain readable.
Notification history omits provider error details that can contain credentials.
Execution actions are POST .../$XID/cancel, /approve (admin only),
and /retry (after a terminal status). Retry returns 409 while the source
execution has a lost or unconfirmed remote outcome; inspect the agent before
retrying. Retry also returns 409 while container cleanup remains unconfirmed.
Local cleanup requires a successful startup runtime sweep; agent cleanup
requires a ready v3 poll. Confirmed agent cleanup permits retry without
changing the historical cleanup_unconfirmed result.
GET /api/v1/projects/$PID/runbook-executions?limit=100&offset=0
returns {items, total, limit, offset}. The default page has 100 items;
the maximum is 1000. POST /api/v1/projects/$PID/runbooks/$BID/schedules
takes environment_id, five-field cron, and optional version_id.
Omit the version or pass 0 to follow the latest saved version. Disable a
schedule with POST .../schedules/$SID/disable.
The web UI starts at /projects/$PID/runbooks. It supports editing,
execution history, live logs, approval, cancellation, retry, and schedules.
Deleting a project with a pending, running, approval-gated, or unconfirmed
remote runbook returns 409. Approve an approval-gated execution, then wait
for a confirmed terminal status (or cancel it after it starts) before deleting
the project. An unconfirmed remote outcome needs operator inspection.
Deleting an environment with any active or unconfirmed remote deployment
also returns 409.
Once deployments are terminal, environment deletion removes their history.
Inspect variables while authoring steps
Call GET /api/v1/projects/{id}/variables/available to inspect current variable definitions.
The endpoint requires project access and honors scoped-token grants.
It returns configured, predefined, environments, context, and warnings.
Environment choices contain only IDs and names.
Secret values are null and are never decrypted for this view.
Non-secret values follow existing project inspection permissions.
Set environment_id to select an eligible environment.
Without it, the response shows scope alternatives without an effective environment.
Set step_id to load a saved project step’s metadata.
The optional draft fields override that metadata:
interpreter:bash,pwsh,powershell, orpython3.execution_target:localoragent.agent_execution_mode:hostorcontainer. Local steps requirehostin this field.variable_names: comma-separated names. An explicit empty value removes the saved restriction for this preview.
Each definition includes its source, scope, secret flag, effective flag, status, reason, and copyable reference.
overridden_by identifies the winning definition by its key.
Configured statuses include available, excluded, overridden, out_of_scope, requires_environment, conditional, and unsupported.
Python references require import os.
References contain variable names, never values.
Predefined paths remain conditional because authoring has no deployment package or approval context.
ARTIFACT_PATH requires a package.
DURPDEPLOY_STAGE_DIR requires managed deployment staging.
DURPDEPLOY_APPROVED_DIR requires an approved artifact volume and is unsupported on the current remote agent.
Managed paths bypass configured-variable restrictions when injected.
Their values are assigned at execution.
Host process environments and server configuration are excluded.
This endpoint does not change snapshots, precedence, injection, or permissions.
Invalid context returns 400.
A missing or foreign step returns 404.
For lifecycle projects, an environment outside the lifecycle returns 422.
For projects without a lifecycle, a missing environment returns 404.
Responses use Cache-Control: no-store.
Endpoint cheat sheet
Lifecycle variables are shared by every project assigned to that lifecycle.
Global admins manage them with GET/POST /api/v1/lifecycles/{id}/variables
and GET/PUT/DELETE /api/v1/lifecycles/{id}/variables/{varId}. The write body
uses the same name, value, optional environment_id, and boolean secret
fields as project variables. Names must be shell identifiers of at most 255
characters. A scoped environment must be a current lifecycle stage. Secret
values are masked in responses; a blank value on an existing secret preserves
it. Reserved artifact and container variable names are rejected.
Project members can read GET /api/v1/projects/{id}/variables/inherited for
the lifecycle source and any same-scope project override. The existing project
variables endpoint continues to return only project-owned rows. To override,
create a project variable with the same name and environment scope. Delete that
project variable with DELETE /api/v1/projects/{id}/variables/{varId}?reset=inherit
to reset inheritance. This removes all matching local rows in that name/scope,
including legacy unscoped duplicates, while retaining other scopes. Ordinary
DELETE without the query parameter removes only the selected row.
From highest to lowest priority:
project environment, lifecycle environment, project unscoped, lifecycle
unscoped. Removing a lifecycle stage excludes its scoped shared values from
future deployments. Releases and runbook versions snapshot only project-owned
variables. Each new deployment, re-run, rollback and runbook execution resolves
the project’s current lifecycle variables and scope, then captures encrypted
values for that execution. No release refresh is needed after a shared edit.
Queued, approval-waiting and active executions retain their captured values,
including secrets for consistent log redaction. Historical releases that already
contain merged values are not migrated; recreate them when upgrading.
| Resource | Endpoints |
|---|---|
| Projects | GET/POST /api/v1/projects, GET/PUT/DELETE /projects/{id} |
| Environments | same shape under /environments |
| Steps | /api/v1/projects/{id}/steps[/{stepId}] (POST/GET/PUT/DELETE, PATCH /steps/reorder) |
| Variables | /api/v1/projects/{id}/variables[/{varId}] |
| Inherited variables | GET /api/v1/projects/{id}/variables/inherited |
| Available authoring variables | GET /api/v1/projects/{id}/variables/available |
| Shared lifecycle variables | /api/v1/lifecycles/{id}/variables[/{varId}] (global admin) |
| Releases | /api/v1/projects/{id}/releases[/{relId}] (GET/POST/DELETE), POST .../refresh |
| Deployments | POST /api/v1/projects/{id}/deployments, GET /api/v1/deployments (member projects; global admins see all; optional positive project_id filter) |
| Deployment detail | GET /deployments/{id}, /status, /logs, /logs/{logId} |
| Actions | POST /deployments/{id}/cancel|redeploy; admin-only POST .../approve |
| Runbooks | /api/v1/projects/{id}/runbooks[/{runbookId}], /runbook-executions[/{executionId}] |
| Users/tokens | admin under /api/v1/admin/...; /api/v1/users/me; POST /api/v1/tokens ({"name":"..."}) mints another token for yourself — 201, plaintext is in the id field of the reply |
Getting help
- Read the API reference for request and response shapes.
- Browse the reference library for server documentation.
Passing files between deployment steps
Deployment steps receive DURPDEPLOY_STAGE_DIR, even with a variable_names restriction.
Write or copy files into this directory to pass them to later steps in the same deployment.
Server containers and agent containers use /stage.
Agent host steps use a private directory on that agent.
For example, run cp build.tar "$DURPDEPLOY_STAGE_DIR/build.tar" in a producing step.
Read the file through the same variable in a later step.
The server automatically transfers all staging files between jobs, including jobs on different agents.
Upgrade each participating agent to a build that reports X-DurpDeploy-Staging: staging/1.
Older agents cannot receive these deployment jobs.
Active staging requests do not consume the assigned run’s step timeout.
Each agent in a parallel step receives the same input snapshot.
The server combines independent changes and deletions after every agent succeeds.
Identical changes are accepted, but conflicting changes fail the deployment.
Only regular files and directories transfer.
Each snapshot permits 512 MiB of file content and 20,000 file and directory entries.
File modes and ownership are normalized during transfer.
The server encrypts transfer snapshots at rest and restricts downloads to the assigned agent’s live claim.
Use GET /api/v1/deployments/{id}/staging to list active transfer metadata without file contents.
Invalid deployment IDs return HTTP 400.
This endpoint returns an empty array after cleanup.
Use GET /api/v1/admin/audit?action=publish_staging to identify staging publishers after cleanup.
Each successful upload audit records the authenticated agent ID in its details JSON field agent_id.
Each deployment starts with an empty directory. Files survive step completion
and retries; failed attempts can leave partial files. Publish completed outputs
with a temporary file and atomic rename when a retry must not consume partial
data. Ordinary step /tmp files remain private to each attempt.
The runtime also needs the digest-pinned Alpine staging helper documented in
docs/deploy.md; an offline installation must preload it alongside step
images before launching deployments.
The staging and approved paths are reserved: variable create/update requests
return 422 for DURPDEPLOY_STAGE_DIR and DURPDEPLOY_APPROVED_DIR, including
blank-secret updates. Step variable selections cannot include them. Staging
is writable, noexec, nosuid, and nodev, backed by a bounded temporary volume
(512 MiB plus 20,000 host pages, 40,002 inode units). These filesystem bounds do
not impose a RAM ceiling or CPU quota on local step containers. The staging
helper retains its own resource limits. The volume is removed on success,
failure, cancellation, or shutdown; startup reclaims orphaned volumes within
the configured runtime namespace. Unconfirmed removal yields
cleanup_unconfirmed and blocks retry until a successful startup runtime sweep
changes the deployment to failed, even if a later cleanup attempt removed
the remaining resources. This preserves the existing conservative recovery
contract for local execution.
The server purges transfer snapshots when the deployment succeeds, fails, or is canceled. Agents remove their private staging files before reporting completion or cancellation. Host jobs stop all remaining descendants before transfer, retry, or cleanup, including detached processes. If process cleanup cannot be confirmed, the agent retains staging and exits for service-wide cleanup.
Staging is not shared between deployments or runbooks. Existing local staging remains available within a runbook execution. Staging is not durable storage for an approval pause or restart.
Pinned release packages use ARTIFACT_PATH outside writable staging.
Container jobs mount this package at /artifacts with read-only access.
Host jobs receive a private package directory with read-only file permissions.
If an agent withdraws artifact/1, incompatible waiting package jobs fail before dispatch.
Bash post-deployment verification receives the package in a server container, including releases with only agent steps.
If Bash verification is configured, keep server container execution enabled.
Local steps accept network_mode (none by default, or bridge). Artifact
gates use approval_artifact_path, approval_review_path, and
approval_review_format (summary or terraform), relative to
DURPDEPLOY_STAGE_DIR. A gated deployment pauses before the next step and
reserves its project/environment slot; later deployments and runbooks for that
same project and environment queue until it completes, is
rejected, is cancelled, or expires. Artifact approval retains its queue slot.
Other projects can deploy to that environment concurrently. The same project
can also deploy to other environments concurrently. Queue positions and active
work links refer only to the deployment’s project/environment pair.
Gate producers must run locally in deployment releases.
Gated deployments can include agent steps, but cannot use automatic retries.
Runbooks and agent steps cannot produce approval gates.
Local steps receive approved artifacts through the read-only DURPDEPLOY_APPROVED_DIR directory.
If agent delivery needs apply results, write those files to DURPDEPLOY_STAGE_DIR in the local apply step.
Both approval paths and
an explicit review format are required together; an empty format is rejected.
Approval bundles and review JSON have no application byte limits.
API and browser previews show at most 100 resources and 1 MiB of resource text.
Oversized values or sensitivity metadata omit the affected side in full.
The API reports preview_truncated, resources_omitted, and per-resource omitted_fields.
Preview bounds do not change approval counts, stored context, or artifact downloads.
Sparse staging files are rejected before export.
Runtime staging capacity, scratch capacity, disk space, and memory still constrain execution.
If approval publication fails, the generation step reports failed.
Read the Artifact approval failed: diagnostic in GET /api/v1/deployments/{id}/logs.txt.
GET /api/v1/deployments/{id}/artifact-gates returns counts, checksums,
revision, expiry, status, and approver metadata, including
decision_ready (true only while the deployment is waiting for a decision
and this gate is awaiting and unexpired),
review_format (terraform or summary), review_source: "step_output"
and review_verified: false: counts are
unverified producer claims, not an independent analysis of the artifact.
Use trusted tools to inspect the exact downloaded artifact before approving.
Checksums establish byte identity, not review accuracy. Generation and apply
scripts remain trusted; a gate does not sandbox them to plan/apply semantics.
Write-capable project members can download /{stepIndex}/artifact; viewers
cannot. They can also inspect /{stepIndex}/review, which returns resources
with resource addresses, ordered actions, and formatted before/after JSON
strings. Terraform sensitivity masks and known secret release variables are
redacted; unknown values are labelled (known after apply). Configuration,
variables, and outputs are omitted. This producer-supplied view retains
review_source: "step_output" and review_verified: false; it is not an
independent decoding of the saved binary plan. Summary-format gates return no
resources. Expired or unavailable reviews return 409. The deployment UI’s
View resource changes disclosure loads this view on demand.
Administrator-only
/{stepIndex}/approve and /{stepIndex}/reject accept
{"revision":1,"sha256":"..."}. Stale or duplicate decisions return 409.
Wait for decision_ready: true before submitting a decision. A gate can be
published while container cleanup is still finishing; its status alone does
not imply readiness. Readiness is advisory; decisions still validate identity,
expiry, deployment state, and artifact integrity at submission time.
Approved context is read-only at DURPDEPLOY_APPROVED_DIR; apply the saved
artifact exactly, never regenerate it. Gated scripts and logs remain visible
under normal permissions and secret-variable log redaction. Keep Terraform
JSON in the review file; printing it can expose plaintext sensitive values. Gates
expire after 24 hours; the minute worker records expiry and releases queues.
Polling is read-only, and expired downloads and decisions return 409 immediately.
Stopped container references retain pinned image IDs through waits and restarts;
maintenance removes them after terminal decisions. Encrypted terminal bundles
are removed after seven days. See docs/artifact-approval.md for Terraform setup.
Generic ZIP packages
Each project has one active HTTPS ZIP repository. Saving it automatically enables packages for future release snapshots; no separate selection is needed.
GET/PUT/DELETE /api/v1/projects/{id}/package-repositoryPOST /api/v1/projects/{id}/package-repository/testwith{"version":"1.6.0"}GET /api/v1/projects/{id}/releases/{relId}/artifact(pin ornull)
Repository bodies contain url_template, auth_type (noauth,
bearer, or basic), username (basic only), and credential (token or
password). Use exactly one {version} in the HTTPS URL path, for example
https://repo.example/app/{version}/package.zip. URL credentials, query
strings, and cross-origin redirects are rejected. Private HTTPS destinations
are allowed; loopback and link-local destinations are blocked. Configure normal
system trust for a private repository’s TLS certificate; TLS verification is
never disabled.
Artifact versions cannot be dot segments (. or ..) or contain path
separators. ZIP paths must fit Linux staging filesystems: at most 255 bytes per
component and 4,000 bytes per relative path.
GET returns the active configuration or null. PUT configures or replaces it
and returns 200. DELETE disables packages for future snapshots, retaining source
records and credentials required by existing pins. Replacing the URL template,
auth type, or username creates a new source record rather than altering old pins.
Credentials are encrypted and omitted from all responses. A blank credential
preserves the current secret only when those source fields are unchanged.
Supply new credentials when replacing an authenticated source.
To rotate a retained historical source, save its exact URL template, auth type,
and username again with an explicit credential. This reactivates that source
record and updates the credential used by existing pins without changing them.
The test endpoint downloads and validates the requested ZIP without creating a
release. Success returns exists: true, the resolved url, version, sha256,
and compressed ZIP size. Invalid versions/ZIPs return 422; missing configuration
returns 404; upstream fetch failures return 502 and do not claim the package is
absent. Generic templates do not provide version discovery or listing.
Release creation captures the repository, URL and version without a pull. Deployment creation copies that package generation; the first successful ZIP pull validates and atomically records SHA-256/size on every pending copy before any step runs. Concurrent pulls with different bytes produce one winning pin and a checksum failure. Re-runs retain the original generation even after release refresh. Refresh explicitly downloads and validates the current package and replaces only the release pin. Deployments use current credentials for their captured repository and reject content that no longer matches their pin.
Runbook create/save accepts artifact_release_id, selecting an existing release
in the same project that has a package attachment, including a pending pull.
It is required while a project repository is selected. The runbook version
copies that package generation; later refreshes of
the source release do not change it. Runbook version responses include
artifact (the copied pin or null).
Deleting the source project release removes its completed deployment history and
its own artifact pin, but retains copies held by saved runbook versions. Those
copies clear source_release_id and remain executable with their pinned bytes.
When editing a saved runbook, keep_artifact_pin: true copies its latest pin
directly, including when its source release has been deleted. This cannot be
combined with artifact_release_id and is not valid for creating a new runbook.
The web edit form defaults to “Keep current pinned package” for deleted sources.
Every package-bearing step receives ARTIFACT_PATH, including steps with a variable allowlist.
Container steps use /artifacts with a read-only, noexec mount.
Host steps use a private directory with read-only file permissions.
The server owner remains responsible for trusted host scripts and their effects.
Copy package files into DURPDEPLOY_STAGE_DIR to share changed outputs with later jobs.
For host copies, add owner write permission to copied files before changing them.
The agent excludes the original package directory from staging uploads.
This variable is reserved. Interpreters can read package files.
ZIP paths, links, and
special files are rejected. Limits are 300 MiB downloaded, 512 MiB extracted,
10,000 ZIP entries, 19,999 total distinct files/directories (including implicit
parents), a fixed 16 MiB ZIP metadata-read budget, and a five-minute
download timeout. The metadata budget includes footer discovery and repeated
parser reads, not payload contents; unusually metadata-heavy ZIPs are rejected.
It is not an exact memory limit and does not scale with server RAM. Temporary server files
and runtime staging volumes are removed on completion, failure, or cancellation.
Cleanup uncertainty blocks retries until runtime reconciliation succeeds.
The container image uses native TMPDIR=/data/tmp on its writable data volume,
not Compose’s 64 MiB /tmp mount. Reserve about 1.3 GiB of workspace per
concurrent maximum-size deployment for the ZIP, extracted files, and transport
tar. Direct installations can select a suitable directory with TMPDIR.
The server holds an exclusive lease on a private durpdeploy-artifacts
subdirectory and reclaims stale ZIP, extraction, and tar files at startup.
Concurrent server instances must use separate TMPDIR directories.
Package-bearing deployments and runbooks support server containers, agent containers, and agent host jobs.
Agents must report X-DurpDeploy-Artifact: artifact/1 and X-DurpDeploy-Staging: staging/1 when polling.
The encrypted job payload contains the pinned package checksum and size.
The agent downloads each package through the existing contract:
POST /agent/v1/deployments/{id}/artifact with JSON {"claim_token":"..."}
on the separate mTLS listener. It requires the assigned agent’s live step claim,
returns verified ZIP bytes with X-Artifact-SHA256 and X-Artifact-Size, and
never sends repository credentials. Its request body is limited to 4 KiB and
does not accept a protocol field.
Invalid packages or changed pinned bytes return 422.
Package transfers keep heartbeats active and pause the remote step timeout during authenticated HTTP transfer.
Transfers stop after five minutes without progress.
Managed package files and volumes follow the same cleanup barrier as writable staging.