Create deployments — POST /v1/projects/{slug}/deployments

Opens a deployment and hands back a short-lived, prefix-scoped grant to write its bytes straight to object storage.

POST /v1/projects/{slug}/deployments

Addresshttps://api.hanzo.ai/v1/projects/{slug}/deployments
MethodPOST
Operationpost_projects_by_slug_deployments
AuthAuthorization: Bearer $HANZO_API_KEY

Opens a deployment and hands back a short-lived, prefix-scoped grant to write its bytes straight to object storage. Answers 202.

This is the path for a site too large to send as one archive: a real export is hundreds of megabytes against a 16 MiB body limit, so the bytes deliberately do NOT pass through the API. The answer carries bucket, prefix and upload — a presigned POST policy that S3 itself confines to this site's prefix (starts-with <org>/<slug>/), expires in 30 minutes and bounds each object. So a build writes its own files and holds no standing bucket credential; there is nothing to rotate and nothing that leaks between tenants. Never guess the prefix — it is server-derived, and a guessed one lands where nothing is served.

The deployment is queued until POST .../deployments/{id}/complete flips it live (or error). That completion is also where DELETION happens: the grant authorizes writes only, so a build cannot remove a file, and cloud reconciles the prefix against the keys manifest the completion carries. A build that dies before completing leaves the deployment queued rather than a half-live site.

The grant is on the 202 and NOWHERE else — it is never stored and never replayed on a later read, so it cannot outlive the build it was minted for. A deployment whose grant could not be minted is still created and still completable; it simply carries no upload, and a caller with no other way to write should treat that as the failure it is.

Billing: the deployment is free. The one gate is storage, and it runs BEFORE anything is created: an org past the free storage tier that cannot pay a month of what it holds is 402, and 503 when commerce is unreachable. What the build uploads is billed as storage from the hourly sweep that finds it, never as a deploy.

Scope: a validated principal is required (401 without one) and the site is resolved within that principal's org, so another tenant's slug is a 404.

Request

3 fields, body application/json (required).

FieldInTypeRequiredDescription
slugpathstringyesSlug is the site to deploy, from the path.
commitbodystring—Commit is the git sha this build was produced from, recorded on the deployment so a released site can be traced back to its source.
slugbodystring—Slug is the site to deploy, from the path.

Response

StatusBodyMeaning
202project.projectsDeploymentaccepted
defaultproblem-detailsrefused

202 body — 22 fields.

FieldInTypeAlwaysDescription
bucketbodystring—Bucket is the object-store bucket its files were written to.
bytesbodyinteger (int64)—Bytes is their total size in bytes.
commitbodystring—Commit is the revision that was built, for a deployment that came from a repository.
createdAtbodyinteger (int64)—CreatedAt is when the deployment was queued, as Unix seconds.
filesbodyinteger (int64)—Files is how many objects the deployment published.
idbodystring—ID identifies this one deployment attempt, and is what CI quotes back to complete it.
liveUrlbodystring—LiveURL is where this deployment serves, once it is live.
messagebodystring—Message is what happened, in words — the build's own note, or on a failure why it failed.
prefixbodystring—Prefix is the key prefix within that bucket holding EXACTLY this deployment's objects — the unit an upload grant is scoped to, so a grant for one deployment cannot write over another.
projectIdbodystring—ProjectID is the project this deployment belongs to.
sourcebodystring—Source is what caused the deployment — a git push, an uploaded artifact, a generated site.
statusbodystring—Status is where the attempt got to — queued, live, or failed.
updatedAtbodyinteger (int64)—UpdatedAt is when it last changed state, as Unix seconds — so the gap between the two is how long the build took.
uploadbodyproject.projectsUploadGrant—
upload.copybodyboolean—Copy is true when the completion of this deployment accepts copyFrom and copy.
upload.expiresAtbodyinteger (int64)—ExpiresAt is when the grant stops being accepted, as Unix seconds.
upload.fieldsbodyobject—Fields are form values every POST must carry VERBATIM, alongside key and file.
upload.fields.*bodystring—
upload.maxBytesbodyinteger (int64)—MaxBytes bounds ONE object, not the upload as a whole.
upload.prefixbodystring—Prefix is the only place this grant can write: the deployment's own key prefix.
upload.urlbodystring—URL is the address to POST each object to.
versionbodyinteger (int64)—Version counts deployments of this project from 1, so the history reads as an ordered sequence rather than by timestamp.

Failure carries the platform error shape — see Errors.

Examples

hanzo projects deployments create <slug>
import { Configuration, ProjectApi } from 'hanzoai';

const api = new ProjectApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postProjectsBySlugDeployments({ slug: 'slug', commit: "<commit>", slug: "<slug>" });
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import ProjectApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = ProjectApi(client).post_projects_by_slug_deployments(slug='slug', commit="<commit>", slug="<slug>")
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.ProjectAPI.PostProjectsBySlugDeployments(context.Background()).Execute()
if err != nil {
	return err
}
use hanzo_client::apis::{configuration::Configuration, project_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = project_api::post_projects_by_slug_deployments(&cfg, Default::default()).await?;
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.ProjectApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new ProjectApi(client).postProjectsBySlugDeployments();

The method above is the one at the current release of the document. hanzoai@8.5.914 (npm) and hanzoai@8.5.914 (PyPI) were generated from an earlier release, where this operation carried a different id, so it spells the method differently — regenerating the clients is what makes the two agree. SDKs →

curl -X POST https://api.hanzo.ai/v1/projects/<slug>/deployments \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "commit": "<commit>",
       "slug": "<slug>"
     }'

Tool project, op post_projects_by_slug_deployments — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.

curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "project",
         "arguments": {
           "op": "post_projects_by_slug_deployments",
           "input": {
             "slug": "<slug>",
             "commit": "<commit>"
           }
         }
       }
     }'

Project API · All Hanzo APIs · Interactive reference

Was this page useful?