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 URL | https://api.hanzo.ai |
| Operations | 15 |
| Auth | Authorization: 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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes |
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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug is the project the deployment belongs to, from the path. |
id | path | string | yes | ID is the queued deployment to complete, from the path. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
bytes | integer | — | |
commit | string | — | |
files | integer | — | |
id | string | — | ID is the queued deployment to complete, from the path. |
keys | string[] | — | Keys is the manifest CI just uploaded, RELATIVE to the deployment prefix. |
liveUrl | string | — | |
message | string | — | |
slug | string | — | Slug is the project the deployment belongs to, from the path. |
status | string | — | live | 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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug is the project the deployment belongs to, from the path. |
id | path | string | yes | ID 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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug 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".
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug is the project the host is attached to, from the path. |
host | path | string | yes | Host 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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug is the project the host is attached to, from the path. |
host | path | string | yes | Host 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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug 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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug is the site the hosts attach to, from the path. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
domains | string[] | — | Domains are the custom hostnames to attach, in order. |
slug | string | — | Slug 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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug 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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug 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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug is the project to update, from the path. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
cacheControl | string | — | |
description | string | — | |
framework | string | — | |
hidden | boolean | — | Hidden is MODERATION, and the only admin-gated field on this body: it pulls a public project out of the catalogue from… |
hiddenReason | string | — | |
license | string | — | |
name | string | — | |
repo | object | — | |
slug | string | — | Slug is the project to update, from the path. |
upstream | string | — | Upstream/License credit the third-party work this app was published from — settable after the fact, because the demos… |
visibility | string | — | Visibility 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.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes | Slug 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 body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | — | target project name (optional; defaults to the parent's title) |
slug | string | — | parent slug to fork — catalog template or published project (required) |
target | string | — | Target overrides the derived project slug (optional; defaults to the parent slug). |
variant | string | — | Variant 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 body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
analytics | boolean | — | Analytics is the opt-OUT for the wired-by-default analytics beacon: absent (nil) ⇒ ON (the default); explicit false ⇒… |
description | string | — | |
framework | string | — | |
license | string | — | |
name | string | — | |
repo | object | — | |
slug | string | — | |
upstream | string | — | Upstream/License credit the third-party work this project was published from. |
visibility | string | — | Visibility is "public" (the default when absent) or "private". |
How is this guide?
Process-speech-to-text
Package ai is Hanzo AI — the model API on /v1 (/v1/chat/completions, /v1/messages, /v1/models and the rest of hanzoai/ai's surface) — mounted into a cloud binary with the money, ingest and telemetry…
Prompts
Package prompts is your prompt library, versioned, so nothing changes silently.