Hanzo

Git

Package git is Git hosting for your org: create repos, clone, push, and see what they cost.

Package git is Git hosting for your org: create repos, clone, push, and see what they cost.

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

git

POST /v1/git/{org}/{project}/{repo}/git-receive-pack

Accept a push, and turn it into a build

The pack-transfer phase of a push, and the point at which a push becomes an EVENT. NEVER ANONYMOUS: a push always requires an authenticated org, and the org in the path must equal it.

Once the pack is on disk the repository's storage usage is metered and a build is fired for every branch whose tip actually moved, computed from the before/after branch diff rather than from what the client claimed. That runs on a cancel-immune context, so a client that hangs up the moment its push lands still gets its build, and it runs even when git itself exited non-zero — the refs on disk are the ground truth. Repacking housekeeping is detached and never blocks the response.

A Content-Type other than application/x-git-receive-pack-request is 400. Addressed under the API prefix, with the PROJECT as a middle path segment: project scope otherwise rides a header a git client cannot send, so this path is the only usable remote for a project-scoped repository. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.

ParameterInTypeRequiredDescription
orgpathstringyes
projectpathstringyes
repopathstringyes

POST /v1/git/{org}/{project}/{repo}/git-upload-pack

Serve a clone or fetch

The pack-transfer phase of a clone or fetch: the request and the response are git's binary pack protocol, streamed straight through git itself — request body to git's stdin, git's stdout to the response — so a multi-gigabyte clone never lands in this process's memory.

A PUBLIC repository is fetched anonymously; a private one requires its own org, and a wrong or absent org is 404 rather than a hint that the repository exists. A Content-Type other than application/x-git-upload-pack-request is 400. Addressed under the API prefix, with the PROJECT as a middle path segment: project scope otherwise rides a header a git client cannot send, so this path is the only usable remote for a project-scoped repository. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.

ParameterInTypeRequiredDescription
orgpathstringyes
projectpathstringyes
repopathstringyes

GET /v1/git/{org}/{project}/{repo}/info/refs

Advertise a repository's refs to a git client

The ref-advertisement phase of git's smart-HTTP protocol — the first request a clone, a fetch and a push all make. ?service= selects which: git-upload-pack advertises for a fetch, git-receive-pack for a push, and any other value is 400.

ANONYMOUS ONLY FOR FETCH, AND ONLY ON A PUBLIC REPOSITORY. The push advertisement always requires an authenticated org, and where a path org is present it must equal the authenticated one. A private repository reached without its org is 404, indistinguishable from one that does not exist. Addressed under the API prefix, with the PROJECT as a middle path segment: project scope otherwise rides a header a git client cannot send, so this path is the only usable remote for a project-scoped repository. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.

ParameterInTypeRequiredDescription
orgpathstringyes
projectpathstringyes
repopathstringyes

POST /v1/git/{org}/{repo}/git-receive-pack

Accept a push, and turn it into a build

The pack-transfer phase of a push, and the point at which a push becomes an EVENT. NEVER ANONYMOUS: a push always requires an authenticated org, and the org in the path must equal it.

Once the pack is on disk the repository's storage usage is metered and a build is fired for every branch whose tip actually moved, computed from the before/after branch diff rather than from what the client claimed. That runs on a cancel-immune context, so a client that hangs up the moment its push lands still gets its build, and it runs even when git itself exited non-zero — the refs on disk are the ground truth. Repacking housekeeping is detached and never blocks the response.

A Content-Type other than application/x-git-receive-pack-request is 400. Addressed under the API prefix, so git clone https://<host>/v1/git/<org>/<repo>.git works on any host the binary serves. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.

ParameterInTypeRequiredDescription
orgpathstringyes
repopathstringyes

POST /v1/git/{org}/{repo}/git-upload-pack

Serve a clone or fetch

The pack-transfer phase of a clone or fetch: the request and the response are git's binary pack protocol, streamed straight through git itself — request body to git's stdin, git's stdout to the response — so a multi-gigabyte clone never lands in this process's memory.

A PUBLIC repository is fetched anonymously; a private one requires its own org, and a wrong or absent org is 404 rather than a hint that the repository exists. A Content-Type other than application/x-git-upload-pack-request is 400. Addressed under the API prefix, so git clone https://<host>/v1/git/<org>/<repo>.git works on any host the binary serves. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.

ParameterInTypeRequiredDescription
orgpathstringyes
repopathstringyes

GET /v1/git/{org}/{repo}/info/refs

Advertise a repository's refs to a git client

The ref-advertisement phase of git's smart-HTTP protocol — the first request a clone, a fetch and a push all make. ?service= selects which: git-upload-pack advertises for a fetch, git-receive-pack for a push, and any other value is 400.

ANONYMOUS ONLY FOR FETCH, AND ONLY ON A PUBLIC REPOSITORY. The push advertisement always requires an authenticated org, and where a path org is present it must equal the authenticated one. A private repository reached without its org is 404, indistinguishable from one that does not exist. Addressed under the API prefix, so git clone https://<host>/v1/git/<org>/<repo>.git works on any host the binary serves. This is git's own wire protocol, not an API call to make by hand: point a git client at the clone URL and it makes this request itself.

ParameterInTypeRequiredDescription
orgpathstringyes
repopathstringyes

DELETE /v1/git/keys/{id}

Removes a registered SSH key, scoped to the caller's org: an org can only delete its own, and a key id it does not own is not found. Answers 204 with no body. Once removed the key no longer authenticates any SSH git access.

ParameterInTypeRequiredDescription
idpathstringyesID is the key's identifier ("gitkey_…"), from the :id path segment.

GET /v1/git/keys

Returns the SSH public keys registered to the caller's org — the keys that authenticate git clone git@<host>:<org>/<repo>.git. Keys are org-scoped on read even though the fingerprint index is global, so one org never sees another's.

POST /v1/git/keys

Registers an SSH public key so it can authenticate git clone git@&lt;host&gt;:&lt;org&gt;/&lt;repo&gt;.git for the caller's org. The key line is parsed and canonicalized before storage, its SHA256 fingerprint becomes the auth lookup handle, and the full public key round-trips (it is public). Answers 201. Fingerprints are globally unique, so a key already registered — to this org or any other — is a 409: one key belongs to exactly one org.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
publicKeystringPublicKey is one OpenSSH authorized-key line ("ssh-ed25519 AAAA… you@host").
titlestringTitle labels the key in the console.

GET /v1/git/repos/{name}/blob

Returns one file's bytes at one revision. Text comes back verbatim, binary comes back base64, and a file past the 1 MiB view cap comes back marked truncated with NO content — the client is expected to clone instead.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo to read, from the :name path segment.
refquerystringRef is a branch, tag or commit; empty means the repo's HEAD.
pathquerystringPath is repo-relative; empty is the tree root.

GET /v1/git/repos/{name}/commits

Walks a ref's history newest first, or one path's history when a path is given. There is no cursor: the page is the newest limit commits.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo to read, from the :name path segment.
refquerystringRef is the branch, tag or commit to walk back from; empty means HEAD.
pathquerystringPath narrows the history to commits touching it; empty walks the whole ref.
limitqueryintegerLimit caps the page.

GET /v1/git/repos/{name}/files

Returns every file a glob selects at one revision, WITH its bytes and the revision they came from. It is the read a delivery generator makes: one call answers "what is the inventory at this commit, and what does it say", where listing and then fetching would be a request per file.

Returning the resolved revision matters as much as the bytes. A generator that lists at main and then reads at main can straddle a push and assemble half its inventory from one commit and half from the next; resolving once makes the whole read consistent by construction.

A file past the read cap comes back Truncated with no content rather than being dropped. A caller building a desired set has to know the difference between "this file is empty" and "this file was not read" — silently omitting it is how a pruning reconcile deletes what the missing file declared.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo to read, from the :name path segment.
refquerystringRef is a branch, tag or commit; empty means the repo's HEAD.
globquerystringGlob selects files, matched segment by segment so * never crosses a /.

POST /v1/git/repos/{name}/gc

Repacks a repo into one bitmapped pack and rewrites its commit-graph, so the next clone reuses the bitmap instead of walking the whole object graph. Idempotent, and safe to interrupt — git swaps both artifacts atomically. It runs under one pack slot with the same memory bounds as a clone, so it can block behind heavy pack traffic rather than compete with it. Storage usage is re-measured afterwards, since a repack reclaims space.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo's org-unique handle, from the :name path segment.

POST /v1/git/repos/{name}/mirror

Imports an external git repository into the caller's repo, provisioning it on first use. Fetch is FORCED and covers every ref, so a first call clones the source and a repeat call re-syncs it — the endpoint is idempotent by mirror semantics. Mirrored bytes are metered exactly like a push, and a push.landed event is emitted for the default branch so the code index picks the repo up.

ParameterInTypeRequiredDescription
namepathstringyesName is the local repo to mirror into, from the :name path segment.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
namestringName is the local repo to mirror into, from the :name path segment.
projectstringProject is the sub-scope to land the repo in; empty uses the caller's own, exactly as a create would.
sourcestringSource is the http(s) git URL to fetch from.

DELETE /v1/git/repos/{name}/mirrors/{id}

Removes one outbound mirror target; later pushes stop being forwarded to it. Answers 204 with no body. Nothing is done to the downstream remote itself — only this repo's intent to push there is dropped.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo, from the :name path segment.
idpathstringyesID is the row to remove, from the :id path segment.

GET /v1/git/repos/{name}/mirrors

Returns a repo's outbound mirror targets — the downstream remotes the mirror reactor pushes to whenever a push lands here.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo's org-unique handle, from the :name path segment.

POST /v1/git/repos/{name}/mirrors

Registers a downstream remote the repo's advanced refs are pushed to whenever a push lands here. Answers 201. The URL must be https to a host on the mirror allowlist (github.com / gitlab.com): the same set the mirror credential may be sent to, so a target can never capture the shared token or point the push at an internal service. Any embedded userinfo is stripped — credentials ride env-only at push time and never enter the stored URL. One mirror per host per repo; a second is a 409.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo whose advanced refs are pushed downstream, from the :name path segment.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
hoststringHost is an optional assertion of the target's hostname.
namestringName is the repo whose advanced refs are pushed downstream, from the :name path segment.
urlstringURL is the downstream https git remote. Must be https to an allowlisted host (github.com / gitlab.com); any embedded…

POST /v1/git/repos/{name}/push

Lands a set of files as one commit without a git client — the hanzo.app builder's push. The repo is CREATED on first push, the files are merged onto the branch tip (unlisted files survive), and the same push-to-deploy hook a real receive-pack fires is fired, so downstream this is indistinguishable from a git push.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo to push into, from the :name path segment.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
branchstringBranch to advance; empty means "main".
filespushFile[]Files are added to or overwritten on the branch tip — files already there and not listed SURVIVE.
messagestringMessage is the commit message; empty gets a generated one.
namestringName is the repo to push into, from the :name path segment.

GET /v1/git/repos/{name}/readme

Returns the README at the tree root as plain text — unrendered, so the caller decides how to present it. A repo with no README is not found.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo to read, from the :name path segment.
refquerystringRef is a branch, tag or commit; empty means the repo's HEAD.

GET /v1/git/repos/{name}/refs

Lists a repo's branches, tags and default branch — what a branch picker needs in one call. Unlike the other read ops it tolerates a repo with no commits: the ref sets come back empty and the default branch is still named.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo's org-unique handle, from the :name path segment.

DELETE /v1/git/repos/{name}/subscriptions/{id}

Removes one Slack subscription from a repo; the notifier stops posting that repo's events to that channel. Answers 204 with no body. An id that is not this repo's subscription is not found.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo, from the :name path segment.
idpathstringyesID is the row to remove, from the :id path segment.

GET /v1/git/repos/{name}/subscriptions

Returns a repo's Slack subscriptions — which channels the lifecycle notifier posts this repo's push and deploy events to.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo's org-unique handle, from the :name path segment.

POST /v1/git/repos/{name}/subscriptions

Binds a Slack channel to a repo, so the lifecycle notifier posts that repo's push and deploy events there. Answers 201. The same channel twice on one repo is a 409; a repo outside the caller's scope is a 404, exactly as reading it is.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo to subscribe, from the :name path segment.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
channelstringChannel is the Slack channel the notifier posts to — an id (C…/G…), a #name, or a bare name.
eventsstring[]Events narrows delivery to these lifecycle kinds (push.landed, deploy.live, deploy.failed).
namestringName is the repo to subscribe, from the :name path segment.

GET /v1/git/repos/{name}/tree

Lists the immediate children of one directory at one revision, directories before files. It does not recurse — walk down a level at a time.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo to read, from the :name path segment.
refquerystringRef is a branch, tag or commit; empty means the repo's HEAD.
pathquerystringPath is repo-relative; empty is the tree root.

GET /v1/git/repos/{name}

Returns one repo with its live ref state: every branch name and the resolved HEAD commit. Both are read from the object store on each call, so an empty repo reports no branches and an empty head rather than failing. A repo outside the caller's scope is not found.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo's org-unique handle, from the :name path segment.

PATCH /v1/git/repos/{name}

Flips a repo's public bit, the one mutable repo setting today. Public grants ANONYMOUS fetch only; push and the whole control plane stay org-authed. Returns the updated repo.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo to update, from the :name path segment.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
namestringName is the repo to update, from the :name path segment.
publicbooleanPublic flips anonymous read access.

DELETE /v1/git/repos/{name}

Removes a repo's metadata and purges its storage. Answers 204 with no body. The metadata row is the source of truth for existence, so a storage purge that fails is logged and the delete still succeeds — and a second call is a 404, not a second delete.

ParameterInTypeRequiredDescription
namepathstringyesName is the repo's org-unique handle, from the :name path segment.

GET /v1/git/repos

Returns the repos in the caller's scope, most recently updated first. The scope is the request principal's — the gateway-minted org and its optional project — never anything off the wire, so a caller only ever sees its own. Rows carry no branches or HEAD; read one repo for those.

POST /v1/git/repos

Provisions an empty bare repository in the caller's scope and returns it with its clone URLs. Answers 201. The name must be unique within the scope — a repeat is a 409, never a silent overwrite of an existing repo. The org comes from the validated principal, so a repo is always born owned by the caller's own tenant.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
descriptionstringDescription is a free-form blurb, max 4KiB.
namestringName is the repo's handle, unique within the scope, and the last segment of both clone URLs.
projectstringProject narrows the repo to a sub-scope of the org.
publicbooleanPublic grants ANONYMOUS read (fetch) only; push and the whole control plane stay org-authed.

GET /v1/git/usage

Returns per-repo and total storage bytes for the caller's org — the queryable, per-tenant number commerce and o11y meter on. It spans EVERY project sub-scope, unlike the repo list, so a billing consumer sees the whole tenant footprint in one call. Sizes are last-measured values (create, push, mirror and gc each re-measure), not a live walk of the disk.

POST /v1/git/webhook

Retired — forge pushes build via platform.hanzo.ai

GONE (410). This was the canonical forge's push-to-deploy door, and it never dispatched a build in its life.

It handed each verified push to cloud.OnGitPush, a single-registrant seam whose only registrant lives in apps/platform. cloud runs each app as its own OS process, so in the git process that builder is nil forever — and this handler answered 204 either way. Delivered, signature valid, green on the forge's hook page, and nothing built.

Push-to-deploy now belongs to POST https://platform.hanzo.ai/v1/git-webhook, which owns the build system-of-record and dispatches BuildKit Jobs. git.hanzo.ai delivers there through ONE forge-wide system webhook covering every repository; a repo opts in by committing hanzo.yml, not by owning a hook of its own.

The route is kept, and answers 410 naming that address, precisely so a misdirected delivery says what is wrong. Deleting it would 404, and a 404 here reads as 'the API is switched off' — the wrong conclusion this estate has already drawn twice.

POST /v1/git/zap/createRepo

Create a repository over the ZAP transport

Creates a repository in the caller's org and project scope and answers with its record. name is required and description is optional; project narrows the scope within the org. A name already taken in that scope is a 409 envelope and an invalid name a 400.

A ZAP PROCEDURE, not a REST resource. It answers the bridge's {status, msg, data} envelope rather than the raw view the /v1 route returns — which is a wire shape a typed op cannot produce, and the reason this stays a raw handler — and it calls the SAME core function the REST route calls, so the two transports cannot diverge in behaviour. Org and project scope come from the request identity and NEVER from the body: the body cannot widen the caller's scope. Without a validated org the answer is a 403 envelope.

Request bodyapplication/json

FieldTypeRequiredDescription
descriptionstring
namestring
projectstring

POST /v1/git/zap/deleteRepo

Delete a repository over the ZAP transport

Deletes the repository named by name and answers with the deleted name. A repository outside the caller's org and project scope is a 404 envelope, so a delete can never reach another tenant's repository.

A ZAP PROCEDURE, not a REST resource. It answers the bridge's {status, msg, data} envelope rather than the raw view the /v1 route returns — which is a wire shape a typed op cannot produce, and the reason this stays a raw handler — and it calls the SAME core function the REST route calls, so the two transports cannot diverge in behaviour. Org and project scope come from the request identity and NEVER from the body: the body cannot widen the caller's scope. Without a validated org the answer is a 403 envelope.

Request bodyapplication/json

FieldTypeRequiredDescription
descriptionstring
namestring
projectstring

POST /v1/git/zap/getRepo

Read one repository over the ZAP transport

Answers a single repository's record, named by name. A repository outside the caller's org and project scope is a 404 envelope, the same answer one that does not exist gets.

A ZAP PROCEDURE, not a REST resource. It answers the bridge's {status, msg, data} envelope rather than the raw view the /v1 route returns — which is a wire shape a typed op cannot produce, and the reason this stays a raw handler — and it calls the SAME core function the REST route calls, so the two transports cannot diverge in behaviour. Org and project scope come from the request identity and NEVER from the body: the body cannot widen the caller's scope. Without a validated org the answer is a 403 envelope.

Request bodyapplication/json

FieldTypeRequiredDescription
descriptionstring
namestring
projectstring

POST /v1/git/zap/listRepos

List your repositories over the ZAP transport

Answers every repository in the caller's org and project scope. It reads NO body — the scope is entirely the caller's identity — so a request with an empty object is correct.

A ZAP PROCEDURE, not a REST resource. It answers the bridge's {status, msg, data} envelope rather than the raw view the /v1 route returns — which is a wire shape a typed op cannot produce, and the reason this stays a raw handler — and it calls the SAME core function the REST route calls, so the two transports cannot diverge in behaviour. Org and project scope come from the request identity and NEVER from the body: the body cannot widen the caller's scope. Without a validated org the answer is a 403 envelope.

POST /v1/git/zap/usage

Report your org's git storage footprint over the ZAP transport

Answers every repository in the caller's org with its size in bytes, plus the org's total — what git storage is actually being used, and by which repository. It reads NO body, and it is scoped to the caller's own org, so it is that org's footprint and never the fleet's.

A ZAP PROCEDURE, not a REST resource. It answers the bridge's {status, msg, data} envelope rather than the raw view the /v1 route returns — which is a wire shape a typed op cannot produce, and the reason this stays a raw handler — and it calls the SAME core function the REST route calls, so the two transports cannot diverge in behaviour. Org and project scope come from the request identity and NEVER from the body: the body cannot widen the caller's scope. Without a validated org the answer is a 403 envelope.


All Hanzo APIs · Interactive reference

How is this guide?

On this page