Hanzo AI

Heroku

Heroku builds a repo into a slug and runs it as dynos. Here that is /v1/platform — 37 operations from app create through build, promote and rollback — with the add-ons split out into /v1/provisioning (28).

Heroku takes a repo, builds it into a slug, and runs that slug as dynos. /v1/platform (37 operations) is the like-for-like replacement: create an app, deploy it, read the build's logs, promote, roll back, set env, attach a domain. Add-ons are not part of it — they are /v1/provisioning (28), one route per kind.

The structural difference to plan around: a Heroku app is a globally unique name holding several process types, and here an app is one image under org → project → app. The org comes from your key and is never a field, the slug only has to be free inside the project, and a worker process type is a second app rather than a second line in a Procfile.

Start here

With an image already built, one call puts an app on a live HTTPS URL — no project to create, no team, no region.

# 1. mint a key — sk- belongs on a server, pk- is safe in a browser
curl -sS -X POST https://api.hanzo.ai/v1/account/keys \
  -H "Authorization: Bearer $HANZO_SESSION" \
  -H 'Content-Type: application/json' \
  -d '{"type":"secret"}'

# 2. run the image — answers with the app's live URL
curl -sS -X POST https://api.hanzo.ai/v1/platform/run \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"web","image":"oci.hanzo.ai/acme/web:1.4.2","port":8080,"minScale":1}'

# 3. give it a database — the DSN lands in that app's web-addons secret
curl -sS -X POST https://api.hanzo.ai/v1/provisioning/sql \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"web-db","instance":"web"}'

Step 2 answers 202 with url and status: deploying, and what it created is an ordinary app in your org's default project — listable at GET /v1/platform/projects/default/apps, stoppable and redeployable like any other. Step 3 returns connectionString once and never again; together the two are heroku create plus addons:create heroku-postgresql, with no team and no region to name because the org is the claim in the key.

Core capabilities

CapabilityWhat it doesOperations
/v1/platformApp create, build, deploy, promote, rollback, env, domains37
/v1/provisioningThe seven add-on kinds: sql, kv, s3, search, docdb, vector, datastore28
/v1/o11yLogs, live tail and traces, in place of a log drain and an APM add-on381

Nouns

The app, its builds and its releases

HerokuHanzo
Team, Enterprise AccountYour org, taken from the validated key. Never a field in the request
AppPOST /v1/platform/projects/{project}/apps — creating does not deploy; it lands in draft
BuildpackbuildType: pack, the zero-config default that detects the project. dockerfile is the explicit escape hatch
Slug, the build artifactAn OCI image in your org's own registry namespace — /v1/registry (6)
git push heroku mainPOST /v1/git/{org}/{repo}/git-receive-pack, and POST /v1/platform/hook turns the push into a build
BuildPOST .../apps/{app}/deploy, whose build record is listed at GET /v1/platform/builds
ReleaseGET /v1/platform/releases — only deployments that genuinely reached the cluster
releases:rollbackPOST .../apps/{app}/rollback — with no body, the newest earlier deployment that carries a real image
Config varsPUT .../apps/{app}/env — replaces the whole set; secret: true seals into KMS
PipelineGET /v1/platform/pipelines — a projection of an app plus its newest deployment, not a record you create
Staging and production stagesGET /v1/platform/environments — derived from what apps name, so nothing creates or deletes one
pipelines:promotePOST .../apps/{app}/promote — redeploys a built image and never builds
Review appPOST .../apps/{app}/preview — a branch on its own URL, as a first-class app named <app>-<branch>
heroku ps, ps:scale web=0, maintenance modeGET .../apps/{app} reads live phase and health from the cluster; .../apps/{app}/stop and .../apps/{app}/start scale to zero and back, and metering stops with the pods
heroku logs --tailGET .../apps/{app}/deployments/{id}/logs for build then app, GET /v1/o11y/logs/livetail for the stream
Custom domain and ACMPOST .../apps/{app}/domains, then POST .../apps/{app}/domains/{host}/verify

The canonical default host is seeded at create, so an app has a working HTTPS address the moment it first deploys. A custom host is the one thing that cannot be attached at create — it goes through add-domain and DNS verification first.

Add-ons

HerokuHanzo
Heroku PostgresPOST /v1/provisioning/sql — your org's own instance, postgres:// returned once
Heroku Key-Value StorePOST /v1/provisioning/kv for a dedicated instance, /v1/kv (6) for a hosted bucket with history
Heroku Kafka/v1/mq (15) — streams, consumers, and a next that acknowledges on delivery
Object storage add-ons, BucketeerPOST /v1/provisioning/s3, and /v1/s3 (6) to work the buckets
Search add-ons, BonsaiPOST /v1/provisioning/search
MongoDB add-onsPOST /v1/provisioning/docdb
Vector add-ons, PineconePOST /v1/provisioning/vector
Warehouse and analytics add-onsPOST /v1/provisioning/datastore
Heroku Scheduler/v1/tasks (5) — a durable engine, not a dyno that wakes on a clock
Log drains, Papertrail, Logentries/v1/o11y (381) — logs, live tail, pipelines and retention
APM add-ons, New Relic, ScoutGET /v1/o11y/traces with flamegraph and waterfall, plus /v1/metrics (11)
Error trackers, Sentry, Rollbar/v1/event (12)
Flag add-ons, LaunchDarkly, Split/v1/flags (8) — a definition and a deterministic decision
Heroku Managed Inference and Agents/v1/ai (272) — the Zen models themselves, not a broker to somebody else's — plus /v1/agents (37) for the agent runtime
Elements marketplace, Heroku Button/v1/marketplace (6) to install one tool, /v1/blueprint (3) for a whole stack with its monthly cost

The rest of the account

HerokuHanzo
Add-on attachment writing DATABASE_URLinstance on the provision body — the DSN lands in that app's <instance>-addons secret
Container Registry, container:pushsource: image on create, and /v1/registry (6) to list images and mint a pull token
App webhooks/v1/webhook (8) — the signing secret leaves the server on create and on rotate, never on a read
Collaborators, teams, API keys/v1/iam (159)
Invoices and usage/v1/billing (45) and /v1/usage (5)
Private Space, Shield networking/v1/network (4) for the org's overlay; every app already lands in its own tenant-<org> namespace
Buying the domain itself/v1/domain (7)
Routing, TLS and middleware in front of the app/v1/ingress (18)
heroku run bashPOST /v1/sandbox, then POST /v1/sandbox/{id}/exec/v1/sandbox (19)
A function rather than a dyno/v1/functions (11)

The call

Heroku, creating and running an app through the Platform API:

H=(-H "Authorization: Bearer $HEROKU_API_KEY"
   -H 'Accept: application/vnd.heroku+json; version=3'
   -H 'Content-Type: application/json')

curl -sS "${H[@]}" -X POST https://api.heroku.com/apps \
  -d '{"name":"acme-web","team":"acme","region":"us"}'

curl -sS "${H[@]}" -X PATCH https://api.heroku.com/apps/acme-web/config-vars \
  -d '{"STRIPE_KEY":"sk_live_51abc"}'

curl -sS "${H[@]}" -X POST https://api.heroku.com/apps/acme-web/builds \
  -d '{"source_blob":{"url":"'"$SOURCE_URL"'","version":"a1b2c3d"}}'

curl -sS "${H[@]}" -X PATCH https://api.heroku.com/apps/acme-web/formation \
  -d '{"updates":[{"type":"web","quantity":3,"size":"standard-1x"}]}'

Four calls, after a fifth to POST /sources produced $SOURCE_URL, and four identifiers to keep in step: an app name that has to be free across every Heroku customer, a team, a source version, and a formation type that has to match a line in the Procfile.

Hanzo, the same app, in two:

curl -sS -X POST https://api.hanzo.ai/v1/platform/projects/acme/apps \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "web",
    "source": "git",
    "repo": {"url": "https://git.hanzo.ai/acme/web.git", "branch": "main"},
    "replicas": 3,
    "env": [{"key": "STRIPE_KEY", "value": "sk_live_51abc", "secret": true}]
  }'

curl -sS -X POST https://api.hanzo.ai/v1/platform/projects/acme/apps/web/deploy \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"commit":"main"}'

When the image already exists, one call, and it answers with the live URL:

curl -sS -X POST https://api.hanzo.ai/v1/platform/run \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"web","image":"oci.hanzo.ai/acme/web:1.4.2","port":8080,"minScale":3,"maxScale":10}'

There is no team and no region. The org is the claim in the key, so web only has to be free inside the acme project, and no request field exists for a caller to write another tenant down. Env goes in at create because secret: true seals the value into KMS before the app is ever persisted — it reads back as "" from then on, where a Heroku config var is readable again through GET /config-vars. Deploys are ordered by a version that counts up per app and one goes live only if no higher version already has, so a build that finishes late is recorded superseded instead of overwriting the newer release; that is also why promote and rollback redeploy an existing image rather than building a second one.

What does not carry

A Procfile's process types become separate apps. Heroku's app holds web, worker and release and scales each with a formation entry. An app here is one image running one process, so worker is a second app in the same project with its own slug, env and replica count. No release-phase command runs between the build and the release either — put migrations in a scheduled task on /v1/tasks or run them from a sandbox, which is a separate pod with its own filesystem and none of the app's environment.

There is no dyno size to choose. shape is accepted for the client contract and echoed straight back; the operator sizes CPU and memory. The knobs that do bite are counts and capacity: replicas on an app, minScale and maxScale on POST /v1/platform/run, storageGb for a persistent volume — absent means stateless, which a Heroku dyno filesystem always is — and gpu on a run. Each is clamped to the deployment's limit rather than refused, so an oversized ask lands smaller instead of erroring.

Changing env does not restart anything. PATCH /config-vars cuts a new Heroku release and cycles every dyno. PUT .../apps/{app}/env re-declares the secret sync immediately, but running pods keep the environment they started with until their next deploy or restart. It also replaces the whole set: a variable absent from the body is gone.

Review apps are not automatic. Heroku opens one per pull request from app.json and destroys it on merge. POST .../apps/{app}/preview is a call you make, it needs an image that already exists because it does not build, and it carries no environment variables at all — a preview never inherits production's secrets. Re-previewing a branch converges the same target rather than stacking another one.

Backing services are first-party instances, and the credential comes back once. The seven provisioning kinds are the whole catalog — sql, kv, s3, search, docdb, vector and datastore — so there is no third-party vendor plan to subscribe to and no vendor dashboard to be handed off to for a database. POST /v1/provisioning/sql launches a dedicated instance in your own namespace and returns the DSN in that single response; no later read carries it. Name instance and it lands in that app's <instance>-addons secret, which is the attachment; leave it out and you hold the string yourself.

The CI board is not wired. GET /v1/platform/ci answers 501 rather than an empty list, because a deployment with no forge Actions client and a forge with no runs must not look the same. Runs do happen — repos build from .hanzo/workflows on git.hanzo.ai — the API just does not enumerate them yet. Builds triggered by a deploy are a different record and are listed at GET /v1/platform/builds.

How is this guide?