Create apps — POST /v1/platform/apps
Builds a git repository into an image and writes the declaration that names it — a values file in hanzoai/universe under charts/app/values/<namespace>/<name>.yaml, which the fleet ApplicationSet renders as one Application.
POST /v1/platform/apps
| Address | https://api.hanzo.ai/v1/platform/apps |
| Method | POST |
| Operation | post_platform_apps |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Builds a git repository into an image and writes the declaration that names it — a values file in hanzoai/universe under charts/app/values/<namespace>/<name>.yaml, which the fleet ApplicationSet renders as one Application. That file IS the deployment: nothing else has to be applied.
repo is a GitHub repository — owner/name or https://github.com/owner/name — or another https clone URL. A GitHub repository must be one the caller may see: their own connected GitHub, or the installations their org holds for an org admin (the same set GET /v1/provider/github/repos lists); anything else is 404. It is built at the name GitHub gives it now, from ref, which defaults to its default branch. Any other URL is an org admin's to build.
partOf names the project the app belongs to, and defaults to the app's own name.
Every member of the org may deploy; WHERE it lands is the difference. An org admin commits to universe main and CD applies it — in one call, naming the tag its own build will push, which CD pulls once the build lands. Anyone else opens a review: the declaration is pushed to deploy/<namespace>/<name>/<tag>, declaration.review is the link that opens the pull request, and nothing deploys until it is merged. mode may say branch to make an admin's deploy a review too; a non-admin naming commit is refused, never downgraded.
A member's deploy is bounded by who asked: one build at a time and never the org's last build slot, which is kept for its admins; ten deploys an hour; five open review branches. Past a bound it answers 429. A review branch is deleted once main declares its tag, or after seven days unmerged; notice says so, and that the public env values the branch carries are cleartext in universe, where deleting the branch does not unpublish them.
A release of an image an earlier call built (tag given) is proven pullable before main is pointed at it — a declaration naming an image the registry cannot serve is an ImagePullBackOff with no rollback path.
Omit tag to build; give it to declare an image an earlier call already built, which is how a green build is released without rebuilding it. That release is an org admin's: a member's deploy always builds, and carries public env only — a sealed value would write the running app's secrets before the review is merged, so a member naming one is refused.
An org is its name: the values DIRECTORY, the destination NAMESPACE and the AppProject FENCE are all <org>, and the image is <registry>/<org>/<app>. None of them is a request field — the directory decides what CD admits the sync under and the repository decides what the cluster pulls, so a caller who could name either could reach outside its own org.
org is an ACT-AS, not a placement field: it defaults to the caller's own, and naming another requires SuperAdmin. So does naming a RESERVED org — the platform's own namespace family (the brands and their environments, the control and delivery planes, admin) — even when it is the caller's own, because an IAM org named kube-system does not own Kubernetes. Both refuse rather than downgrade, so an escape attempt is never indistinguishable from a normal request.
A host outside the caller's org subtree is refused: claim and verify a custom domain first.
Request
16 fields, body application/json.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
build | body | boolean | — | |
dockerfile | body | string | — | |
env | body | declareEnv[] | — | |
env[].name | body | string | — | |
env[].public | body | boolean | — | |
env[].value | body | string | — | |
host | body | string | — | |
mode | body | string | — | |
name | body | string | — | |
org | body | string | — | |
partOf | body | string | — | |
port | body | integer | — | |
ref | body | string | — | |
replicas | body | integer | — | |
repo | body | string | — | |
tag | body | string | — |
Response
| Status | Body | Meaning |
|---|---|---|
2XX | declareResp | Success |
2XX body — 62 fields.
| Field | In | Type | Always | Description |
|---|---|---|---|---|
app | body | Declaration | — | |
app.application | body | string | — | |
app.automated | body | boolean | — | |
app.component | body | string | — | |
app.digest | body | string | — | |
app.env | body | declareEnv[] | — | |
app.env[].name | body | string | — | |
app.env[].public | body | boolean | — | |
app.env[].value | body | string | — | |
app.hosts | body | string[] | — | |
app.name | body | string | — | |
app.org | body | string | — | |
app.partOf | body | string | — | |
app.path | body | string | — | |
app.project | body | string | — | |
app.replicas | body | integer | — | |
app.repository | body | string | — | |
app.secrets | body | SecretRef[] | — | |
app.secrets[].env | body | string | — | |
app.secrets[].keys | body | string[] | — | |
app.secrets[].path | body | string | — | |
app.secrets[].project | body | string | — | |
app.tag | body | string | — | |
build | body | buildRef | — | |
build.id | body | string | — | |
build.image | body | string | — | |
build.job | body | string | — | |
build.ref | body | string | — | |
build.repo | body | string | — | |
build.status | body | string | — | |
declaration | body | declareResult | — | |
declaration.changed | body | boolean | — | |
declaration.commit | body | string | — | |
declaration.created | body | boolean | — | |
declaration.declaration | body | Declaration | — | |
declaration.declaration.application | body | string | — | |
declaration.declaration.automated | body | boolean | — | |
declaration.declaration.component | body | string | — | |
declaration.declaration.digest | body | string | — | |
declaration.declaration.env | body | declareEnv[] | — | |
declaration.declaration.env[].name | body | string | — | |
declaration.declaration.env[].public | body | boolean | — | |
declaration.declaration.env[].value | body | string | — | |
declaration.declaration.hosts | body | string[] | — | |
declaration.declaration.name | body | string | — | |
declaration.declaration.org | body | string | — | |
declaration.declaration.partOf | body | string | — | |
declaration.declaration.path | body | string | — | |
declaration.declaration.project | body | string | — | |
declaration.declaration.replicas | body | integer | — | |
declaration.declaration.repository | body | string | — | |
declaration.declaration.secrets | body | SecretRef[] | — | |
declaration.declaration.secrets[].env | body | string | — | |
declaration.declaration.secrets[].keys | body | string[] | — | |
declaration.declaration.secrets[].path | body | string | — | |
declaration.declaration.secrets[].project | body | string | — | |
declaration.declaration.tag | body | string | — | |
declaration.live | body | boolean | — | |
declaration.mode | body | string | — | |
declaration.ref | body | string | — | |
declaration.review | body | string | — | |
notice | body | string | — |
Failure carries the platform error shape — see Errors.
Examples
hanzo platform apps createimport { Configuration, PlatformApi } from 'hanzoai';
const api = new PlatformApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postPlatformApps({ build: false, dockerfile: "<dockerfile>" });from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import PlatformApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = PlatformApi(client).post_platform_apps(build=False, dockerfile="<dockerfile>")cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)
resp, _, err := client.PlatformAPI.PostPlatformApps(context.Background()).Execute()
if err != nil {
return err
}use hanzo_client::apis::{configuration::Configuration, platform_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = platform_api::post_platform_apps(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.PlatformApi;
ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));
var result = new PlatformApi(client).postPlatformApps();curl -X POST https://api.hanzo.ai/v1/platform/apps \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"build": false,
"dockerfile": "<dockerfile>"
}'MCP reaches platform through the platform tool, which names its 38 operations with its own verbs — this one among them, under a name only MCP declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "get_build"
}
}
}'