Hanzo

Projects

Package projects is where your sites live: create one, deploy a build, roll back to any release.

Package projects is where your sites live: create one, deploy a build, roll back to any release.

Base URLhttps://api.hanzo.ai
Operations15
AuthAuthorization: Bearer $HANZO_API_KEY

projects

POST /v1/projects/{slug}/deploy

Deploy a build — upload an archive, or trigger a build from the linked repo

Takes a built site live at https://<slug>.hanzo.app. It accepts BOTH shapes on one address and the content type decides which: a zip or tar.gz archive — raw in the body or as a multipart file part — is uploaded and served immediately, answering 200 with the finished deployment; a JSON body instead queues a build from the project's linked repo and answers 202 with a queued deployment and, where one could be minted, a scoped upload grant for CI to write with. The git path needs a linked repo (400 without one) and is finished later by the completion hook.

Billing is fail-closed and fails FIRST: the hosting gate runs before anything is parsed or uploaded, so an unfunded org is 402 and an unreachable commerce is 503 with nothing written. The debit lands only on success — a failed upload is never billed and never flips the live site, and a queued build is billed at completion rather than at queue time. A redeploy returns the SAME URL, because slug and apex are stable.

Scope: a validated principal is required (403 without one) and the project is resolved within that principal's org, so another tenant's slug is a 404. Object storage must be configured, else 503; an archive that does not walk is a 400 and one over the size cap is a 413.

ParameterInTypeRequiredDescription
slugpathstringyes

POST /v1/projects/{slug}/deployments/{id}/complete

CompleteDeployment is the CI completion hook that flips a queued git deployment to live (or error) once CI has synced the built site to S3.

status must be live or error. On a LIVE completion the public host is claimed FIRST, so the deployment reports the URL it actually OWNS — a CI-supplied liveUrl is a hint that can refine that URL but can never assert a subdomain another tenant holds. keys is the manifest CI just uploaded, relative to the deployment prefix: cloud reconciles the prefix against it so a page deleted from the build actually stops serving. Omit keys and nothing is deleted — the prefix only grows. Reconciliation runs only on a live completion (pruning against a failed build's manifest would delete the site the last good build is still serving) and is best-effort, so a stale leftover never turns a successful deploy into a 500. A live completion is also the one billable moment on the git path; an error completion bills nothing.

Scope: a validated principal is required (403 without one). CI authenticates with an org-scoped token through the gateway, so the deployment is resolved within that principal's org and another tenant's slug or deployment id is a 404.

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the project the deployment belongs to, from the path.
idpathstringyesID is the queued deployment to complete, from the path.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
bytesinteger
commitstring
filesinteger
idstringID is the queued deployment to complete, from the path.
keysstring[]Keys is the manifest CI just uploaded, RELATIVE to the deployment prefix.
liveUrlstring
messagestring
slugstringSlug is the project the deployment belongs to, from the path.
statusstringlive | error

GET /v1/projects/{slug}/deployments/{id}

Returns one deployment of a project by id.

It is how a console follows a build: the status (queued, uploading, live, error), the message a failure left, and the URL and prefix it went live at. Like the history, it never replays the upload grant.

Scope: a validated principal is required (403 without one). Both the project and the deployment are resolved within that principal's org, so a deployment of another project — or of another tenant — is a 404.

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the project the deployment belongs to, from the path.
idpathstringyesID is the deployment id, from the path.

GET /v1/projects/{slug}/deployments

Returns a project's deploy history, newest version first.

Every deploy of the project is a row — uploads, generated sites, and git/CI builds alike — carrying its version, status, source, commit, live URL, file count and byte count. The short-lived upload grant a queued git deployment was handed is NOT replayed here: it exists only on the 202 that minted it, so a grant cannot outlive its build by being fetched again.

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

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the project to act on, from the path.

POST /v1/projects/{slug}/domains/{host}/verify

Checks the DNS challenge for a pending custom hostname and, when it passes, promotes the host so it begins routing at the edge.

It answers 200 either way, with the host's honest current state: verified once the TXT record is found, still pending — with the records to publish and the resolver's own explanation in detail — when it is not. A not-yet is not an error: the check ran, DNS simply has not propagated, and the customer retries. An already-verified host is returned unchanged without re-resolving. On a successful promotion the edge cache-tag is flushed, since the host routes as of that moment.

Scope: a validated principal is required (403 without one). Both the site and the claim are resolved within that principal's org, so a host claimed by another tenant is "not claimed by this site".

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the project the host is attached to, from the path.
hostpathstringyesHost is the custom hostname, from the path.

DELETE /v1/projects/{slug}/domains/{host}

Gives a custom hostname back, so the name is free to reuse.

A claim is FIRST-COME and global, so an add-only surface was not ownership but a leak: a customer who mistyped a domain, or claimed one they later moved elsewhere, could neither reuse it nor let anyone else. This is the third writer that closes it. The release is scoped to (host, org, slug), so it can only ever drop THIS tenant's own claim, and it is IDEMPOTENT: releasing a host we do not hold is a clean 204, never a 404 that would let a caller probe which hosts other tenants hold. The edge cache-tag is flushed, since the host stops routing here.

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

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the project the host is attached to, from the path.
hostpathstringyesHost is the custom hostname, from the path.

GET /v1/projects/{slug}/domains

Returns every custom hostname this site holds: the live ones, plus any pending claim with the DNS records it still owes.

domains is the routing answer — the hosts that are verified right now — while claims is the full panel, one row per host, each saying whether it is live or pending and, if pending, exactly what to publish.

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

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the project to act on, from the path.

POST /v1/projects/{slug}/domains

Attaches one or more CUSTOM public hostnames to this org's site.

Binding a host you do not own would let you shadow it at the edge, so which outcome you get depends on whether ownership is already established: a SuperAdmin vouches (the operator manages the customer's DNS, so its bind IS the proof) and binds VERIFIED immediately; every other caller, INCLUDING an admin of the deployment's own brand org, has the host CLAIMED as pending and gets the DNS challenge back in bound[].records. A pending claim HOLDS the name so nobody else can take it, but it does not route until POST .../domains/{host}/verify proves control.

A hostname we operate is refused to a non-vouched caller (those are assigned by the platform, never claimed), a host another site already holds is a 409, and a name the platform holds is a 400 for EVERY caller — a vouch skips the ownership proof, never the host table's own invariant. Claims and binds are idempotent for the same (org, slug), and re-claiming returns the SAME token rather than invalidating a record the customer has already published. The edge cache-tag is flushed afterwards so a newly-verified host serves the current build immediately.

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

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the site the hosts attach to, from the path.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
domainsstring[]Domains are the custom hostnames to attach, in order.
slugstringSlug is the site the hosts attach to, from the path.

POST /v1/projects/{slug}/purge

Flushes the site's edge cache without redeploying anything.

It invalidates the edge cache-tag site-<org>-<slug> and stamps lastPurgeAt (unix seconds), and it NEVER writes or deletes the S3 origin — the live build keeps serving; only stale copies held at the edge drop, so the next request re-fetches the current artifact from origin. Idempotent, and an edge that is unconfigured or failing is not fatal: lastPurgeAt is still stamped and the answer is still the updated project.

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

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the project to act on, from the path.

GET /v1/projects/{slug}

Returns one project of yours by slug — its settings, its live URL and the deployment currently serving it.

Scope: a validated principal is required (403 without one) and the lookup is keyed by (org, slug), so another tenant's slug is a 404 exactly like a nonexistent one.

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the project to act on, from the path.

PATCH /v1/projects/{slug}

Changes a project's settings, and only the settings you send.

Every field is optional and absent means "leave it": name may not be blanked, framework must stay a known build hint, and cacheControl is capped at 256 characters with no newlines (it becomes a response header). visibility flips public/private under the same rule as create — public is free, private needs a funded org. upstream and license are free-text credit for third-party work, and sending "" clears one. Changing anything reconciles the project's canonical git repo, so a visibility change reaches the source and not just the listing.

hidden/hiddenReason are platform MODERATION and are ignored unless the caller is a platform admin; they remove a project from the public catalogue without touching the publisher's own visibility choice, so un-hiding restores exactly what they asked for.

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

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the project to update, from the path.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
cacheControlstring
descriptionstring
frameworkstring
hiddenbooleanHidden is MODERATION, and the only admin-gated field on this body: it pulls a public project out of the catalogue from…
hiddenReasonstring
licensestring
namestring
repoobject
slugstringSlug is the project to update, from the path.
upstreamstringUpstream/License credit the third-party work this app was published from — settable after the fact, because the demos…
visibilitystringVisibility flips an existing project between "public" and "private".

DELETE /v1/projects/{slug}

Deletes a project and takes its site off the internet.

The metadata delete is authoritative and everything after it is best-effort, in this order: the public <slug> subdomain binding is released so the slug is free to reclaim, the release rows are dropped so a reclaimed slug never inherits the previous owner's rollback menu, the S3 origin is purged under BOTH <org>/<slug>/ and the site's sibling release space, and the edge cache-tag is flushed. A failure in any of those is logged and the delete still answers 204 — resurrecting a project because a purge missed would be worse than a leaked prefix.

Scope: a validated principal is required (403 without one) and the project is resolved within that principal's org, so another tenant's slug is a 404 and nothing of theirs is touched.

ParameterInTypeRequiredDescription
slugpathstringyesSlug is the project to act on, from the path.

POST /v1/projects/fork

Creates a project seeded from a PUBLISHED EXAMPLE — either a starter-kit template from the ONE embedded gallery catalog, or any live project on the platform (an example a seeded creator published, or another org's app serving at <slug>.hanzo.app). Answers 201 with the new project.

slug names the PARENT to fork and is required. Templates resolve first, and the caller org's own private templates ahead of the public gallery, so a curated template slug keeps meaning the same thing even if someone later publishes a live project under it; variant picks that template's format/page/theme. If no template matches, the slug resolves to the UNIQUE live project that owns it across all orgs — the same resolution the site edge uses to serve <slug>.hanzo.app, so what you can browse is what you can fork.

name and target override the derived project name and slug; everything else is inherited from the parent. A live parent contributes its REPO, so the child builds from the same source — the parent's deployed bytes are never copied, because releases are per-tenant by design and the fork publishes its own. The parent it actually resolved is stamped on the child as forkedFrom, so attribution is a fact recorded at fork time rather than a claim reconstructed later.

It funnels through the SAME create path POST /v1/projects uses, so slug validation, org scoping, ID minting and the 409 on a slug the caller's own org already uses are identical.

Scope: a validated principal is required (403 without one) and the child is created in THAT principal's org.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
namestringtarget project name (optional; defaults to the parent's title)
slugstringparent slug to fork — catalog template or published project (required)
targetstringTarget overrides the derived project slug (optional; defaults to the parent slug).
variantstringVariant picks a template's format/page/theme (optional; defaults to the template's first shape).

GET /v1/projects

Returns every project your org owns.

Each row carries the slug, name, framework, visibility, status and live URL — the same rows console and the builder render, because there is only one store behind both. It requires a validated principal (403 without one) and is keyed by that principal's org, so it never contains another tenant's project.

POST /v1/projects

Creates a project — the handle a site is deployed and served under — and answers 201 with it in draft.

name is required; slug is derived from the name when omitted and is the identifier that matters — it becomes the S3 key segment, the public host <slug>.hanzo.app, and the handle every later call addresses, so it must match ^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$ and may not be a reserved label such as api or admin. framework is a build hint from a closed set, defaulting to static; it never gates a deploy, it only tells CI how to build a linked repo.

Two defaults are worth knowing: the analytics beacon is ON unless analytics is explicitly false, and visibility is public unless asked otherwise. Publishing publicly is free; PRIVATE is the paid feature, and an unfunded org asking for it is refused rather than quietly published as public. Creation also provisions the project's data space and a canonical git repo, both best-effort — neither can fail the create.

Scope: a validated principal is required (403 without one) and the project is created in THAT principal's org. The slug is unique per org, so a slug already used in the caller's own org is a 409 while the same slug in another org is irrelevant.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
analyticsbooleanAnalytics is the opt-OUT for the wired-by-default analytics beacon: absent (nil) ⇒ ON (the default); explicit false ⇒…
descriptionstring
frameworkstring
licensestring
namestring
repoobject
slugstring
upstreamstringUpstream/License credit the third-party work this project was published from.
visibilitystringVisibility is "public" (the default when absent) or "private".

All Hanzo APIs · Interactive reference

How is this guide?

On this page