Skip to main content

Command line

hostingctl answers the same questions the panel does, from a terminal, over the same public API and with the same permissions. It is useful for scripting, and for checking what the platform actually reports rather than what a screen last rendered.

Every command takes --json, which prints the API's own field names — the names you would find in the API reference — rather than a second naming of the same facts. Its exit status is 0 when the answer came back, 1 when the API refused, and 2 when the command could not be used at all, so a script can tell a permission problem from a syntax mistake.

Getting it​

hostingctl is distributed as an npm package, @haakco/hostingctl.

The first version has not been published yet. The package is prepared and the release that publishes it is wired, but until a version is published the commands below answer that the package does not exist. The team will say so when it lands.

To try it without installing anything:

npx @haakco/hostingctl --version # no install at all

To keep it on your PATH, install it once:

npm i -g @haakco/hostingctl # install, once

Either way it needs Node 22 or newer on the machine that runs it, and nothing else — no checkout, no database, no cluster credentials. The examples below are therefore ordinary hostingctl … commands.

A machine with no Node can still run the CLI. The package carries a self-contained executable for each platform, and that platform package's bin is the native binary, so it needs no runtime at all. Install the one that matches the machine — @haakco/hostingctl-linux-x64 and its four siblings, @haakco/hostingctl-linux-arm64, @haakco/hostingctl-darwin-arm64, @haakco/hostingctl-darwin-x64 and @haakco/hostingctl-windows-x64:

npm i -g @haakco/hostingctl-linux-x64

Node is the delivery truck here, not the runtime the CLI needs: the parent package's launcher is a small JavaScript file, so npm i -g @haakco/hostingctl needs Node too, while the platform package does not.

Staying current​

hostingctl update brings a copy to the published version. It works out how your copy was installed, prints the command it will run, runs it, and then reads the installed version again to confirm the change:

hostingctl update # update this copy
hostingctl update --check # report the published version and the command, changing nothing

If you would rather run your package manager yourself, then for a global npm install the command is:

npm i -g @haakco/hostingctl@latest

hostingctl --version reports the version of the copy you are running; a development build reports 0.0.0-dev.

Signing in​

At a terminal, login opens a browser and catches the redirect back:

hostingctl login --api-url https://api.example.com/api/v1 --client-id <client-id>

It keeps the session in a credential file at ~/.config/hostingctl/credentials.json — a 0700 directory holding one 0600 file, readable only by your account. It is a file rather than an operating-system keychain, so backup and sync tools will copy it: exclude that path. What bounds the exposure is that the refresh token is single-use, the exchange that uses it invalidates it, and it expires; the CLI renews the short-lived access token silently until it does.

The sign-in listens on a loopback callback (http://127.0.0.1:8477/callback, the redirect the platform has registered for the tool) and accepts exactly one matching redirect, so nothing is exchanged unless the response carries the request's own state. On a machine with no browser or opener, login prints the authorization URL to stderr as soon as it starts waiting, so you can open it from another browser and finish the sign-in there; with --json, stdout stays parseable and the URL is on stderr.

For scripts and CI, pass a token instead and skip the browser entirely:

export HOSTINGCTL_API_URL=https://api.example.com/api/v1
export HOSTINGCTL_TOKEN=<token>

The commands you will use most​

CommandAnswers
hostingctl whoamiWhich account the token belongs to, and what it may do
hostingctl environments --company acmeEvery environment in a company, across all pages
hostingctl environment show <uuid> --company acmeOne environment, with the status and the generation it describes
hostingctl environment profile <uuid> --company acmeWhether the shared catalog covers that environment's profile, and which components are short if it is not
hostingctl access users <uuid> --company acmeWho may reach an environment, their state and their key counts
hostingctl backups <uuid> --company acmeEach backup track's state and the newest restore point
hostingctl mailbox setup <domain-uuid> <mailbox-uuid> --company acmeOne mailbox's mail-client settings
hostingctl logs environment <uuid> --company acmeBounded retained web-container history for one environment
hostingctl logs dns <uuid> --company acmeBounded retained zone-transfer events for one owned DNS domain
hostingctl logs mail <uuid> --company acmeBounded retained delivery attempts for one owned mail domain
hostingctl environment create --company acme --name "Acme Shop"A new environment from a platform package, placed by the platform
hostingctl environment databases <uuid> --company acmeEvery database identity in an environment, its engine and its allowed consumers
hostingctl environment portable export <environment-uuid> --company acmeOne environment's supported setup as portable YAML

Every command that names an environment also needs --company, because the API scopes environments to a company and the CLI asks rather than guessing.

These are the everyday ones rather than the whole surface. hostingctl --help lists every command, and hostingctl <command> --help prints that command's own usage — its groups and the flags that group takes — and hostingctl help <topic> does the same for one group. The command reference is the complete list: every command, every flag it accepts and what each one does, generated from the CLI's own metadata so it cannot describe a command the CLI does not have.

Reading retained log history​

logs reads the platform's bounded, sanitized history for one of your resources — the same data the panel's Logs, Log history and Delivery log history sections show:

hostingctl logs environment <environment-uuid> --company acme
hostingctl logs dns <domain-uuid> --company acme --json
hostingctl logs mail <email-domain-uuid> --company acme
hostingctl logs environment <environment-uuid> --company acme \
--start 2026-10-14T11:00:00Z --end 2026-10-14T12:00:00Z --limit 100

Each subcommand takes the uuid of the environment, DNS domain or mail domain. The API bounds every read: at most a six-hour window within the last 24 hours, defaulting to the most recent hour, and 1–200 entries, defaulting to 100. --start/--end are ISO instants; the server enforces the window and the CLI refuses a malformed --limit before asking.

A source of unavailable means the platform could not read the retained history at all — a missing reader, an active ownership interval it cannot prove, or a boundary failure for an authorized resource. An out-of-company UUID is refused before reading history. Unavailable is not a successful read with no matches; the human output distinguishes these states. The three families are deliberately narrow: environments return dedicated web-container output only; DNS reports zone-transfer initiation rather than full query history; and mail reports a Postfix delivery attempt, which is transport acceptance toward the next hop rather than proof of mailbox receipt.

Creating an environment​

Create an environment from a package the platform sells. The package catalog comes from the API, so the CLI offers exactly the packages the platform offers rather than a key of its own:

hostingctl environment create --company acme --name "Acme Shop" --dry-run
hostingctl environment create --company acme --name "Acme Shop" --slug acme-shop \
--package small_sites --confirm

--name is what the environment is called. --slug is its permanent identifier: omit it and the CLI derives one from the name — lowercase, each run of other characters folded to a single hyphen, trimmed and capped at 63 characters — and refuses a slug shorter than three characters or outside lowercase letters, digits and single hyphens that start and end with a letter or digit, before any request. It is reserved permanently because it addresses the environment's secret folder, so it is never reused. --package names one of the catalog's keys and defaults to its first; a key the catalog does not offer is refused before anything is sent.

Placement is the platform's. The create request carries no cluster and the API refuses one, so nothing here chooses a cluster: the installation's own configuration decides placement. The package supplies the purchase and the environment's starting composition — its applications, database and volumes — and the namespace derives from the environment's own identifier. --dry-run describes the change, reads the catalog and sends nothing; the create itself needs --confirm. Creation records desired state and applies it in the background, so check that the environment's generation is applied before treating it as ready, and --json carries the API's own created environment object.

Environment databases​

An environment can hold SQL identities of its own, separate from any single application. These commands use the company-scoped public API to read, declare, bind or retire them; the CLI never opens a SQL connection, prints a password or issues credentials.

hostingctl environment databases <environment-uuid> --company acme
hostingctl environment database create <environment-uuid> --company acme \
--key shop --engine mariadb --dry-run
hostingctl environment database create <environment-uuid> --company acme \
--key shop --engine mariadb --confirm
hostingctl environment database bind <environment-uuid> <database-uuid> --company acme \
--bind <workload-uuid> --confirm
hostingctl environment database bind <environment-uuid> <database-uuid> --company acme \
--clear-bindings --confirm

databases lists every database the environment holds — including each application's own, shown with its owning application — with its engine, its configuration key and the applications allowed to consume it.

create declares one independent, environment-owned database. --key is the stable declaration key — lowercase letters, digits and underscores, starting with a letter — and --engine is mariadb, postgres or mysql. --bind optionally grants the new database to consuming workloads as it is made; without it the database is created with no consumer, which is a normal state. Ownership cannot be handed to an application here, so an application's own database is never replaced by this command.

bind replaces which applications may consume one database. Give --bind <uuid>[,<uuid>…] to name the new list, or --clear-bindings to detach every application; one of the two is required and they cannot be combined. Detaching stops future credential delivery only: a password an application already holds is not revoked, and the platform refuses to detach a consumer until that workload is stopped and the environment has observed the stop. Nothing here deletes a database or its data.

create and bind write, so they need --confirm. Their --dry-run may read current environment metadata but sends no mutation. Both read the environment's current generation first, so a change reviewed against state that has since moved on is refused by the API rather than applied.

Retiring an identity without erasing its data​

hostingctl environment database review <environment-uuid> <database-uuid> --company acme
hostingctl environment database retire <environment-uuid> <database-uuid> --company acme --dry-run
hostingctl environment database retire <environment-uuid> <database-uuid> --company acme \
--digest <review-digest> --generation <review-generation> --confirm

review reads the identity, consumers, current generation and exact retirement effect. Retirement is refused while an application still consumes the identity; ownership release and stopping/unbinding consumers use their existing reviewed product operations. Do not treat unbinding as credential revocation.

retire --confirm requires --digest and --generation from the review you inspected; it does not fetch and approve a new inventory silently. A changed review is refused by the API. --dry-run sends nothing, including no review request. The operation prevents new SQL logins while retaining database data and existing backup/retention obligations; it deliberately does not terminate existing SQL sessions.

An accepted request is pending, not proof of disabled SQL access. The database list and review show pending, failed or applied state; only applied means the server was observed refusing new logins. A failed request can be retried with another fresh reviewed retire --confirm. Recorded retirement prevents the identity from being rebound or presented as an active credential. Erasing retained data is a separate operation, not a side effect of retirement.

Environment configuration and secrets​

An environment's desired state is a YAML document. These commands read it, review a document against it, record a draft or apply it, and set the declared secret values it needs — all through the same routes the panel uses.

hostingctl environment configuration show <environment-uuid> --company acme
hostingctl environment configuration preview <environment-uuid> --company acme --file acme.yaml
hostingctl environment configuration preview <environment-uuid> --company acme --file acme.yaml --apply
hostingctl environment configuration save <environment-uuid> --company acme --file acme.yaml --confirm
hostingctl environment configuration apply <environment-uuid> --company acme --file acme.yaml --confirm
# Adopt an existing application instead of creating another identity:
hostingctl environment configuration apply <environment-uuid> --company acme --file acme.yaml \
--application-uuid <application-uuid> --confirm

show names the current revision, the active revision and the last known-good revision, with each revision's declared secrets and whether they hold a value. preview classifies the document against the environment and writes nothing; add --apply to review it as an apply, so the verdict is the one the apply relies on. save records a draft revision that is not applied; apply records the same document and queues normal reconciliation. Both re-run the review to obtain the hash and generation the API requires, so a document that changed since review is refused rather than applied against state nobody saw. save and apply need --confirm; --dry-run reads the review and sends nothing.

A secret value never comes from an argument. Set one declared slot from a private file, stdin, or a generated Laravel application key:

hostingctl environment secret write <environment-uuid> --company acme \
--key app_key --value-source file --value-file ~/app-key --confirm
hostingctl environment secret write <environment-uuid> --company acme \
--key app_key --value-source generate --confirm
hostingctl environment secret import <environment-uuid> --company acme --file secrets.env --confirm

--value-source names where the value comes from. file and stdin read it from a private file (mode 600) or --value-stdin, never from the command line. generate is only for the declared secret a Laravel application binds to its APP_KEY environment variable — the fixture's app_key, or a member-scoped <memberKey>_app_key — 32 random bytes, base64-encoded and base64: prefixed. The secret's own name never decides this: the binding in the reviewed document does, so generation refuses a declared secret that is not bound to APP_KEY. It also refuses to replace a value already in use unless --replace says to rotate it, because rotating it invalidates data encrypted with the old key. import sets several declared secrets from a KEY=VALUE file (mode 600) and is the way to bring an existing key over unchanged. The value is read only when the change is sent: a dry run reads nothing, and no value is ever printed, logged or placed in a dry-run description.

Applications​

One application's build, deployment, desired runtime state, process runs, rename and removal:

hostingctl application list <environment-uuid> --company acme
hostingctl application show <environment-uuid> <workload-uuid> --company acme
hostingctl application deployment <environment-uuid> <workload-uuid> --company acme
hostingctl application build <environment-uuid> <workload-uuid> --company acme \
--source <selection-uuid> --confirm
hostingctl application deploy <environment-uuid> <workload-uuid> <release-uuid> --company acme --confirm
hostingctl application runtime <environment-uuid> <workload-uuid> --company acme --mode stop --confirm
hostingctl application runs <environment-uuid> <workload-uuid> --company acme
hostingctl application run-log <environment-uuid> <workload-uuid> <run-name> --company acme
hostingctl application schedule-run <environment-uuid> <workload-uuid> <schedule-key> --company acme --confirm
hostingctl application update <environment-uuid> <workload-uuid> --company acme --label "Shop" --confirm
hostingctl application removal <environment-uuid> <workload-uuid> --company acme
hostingctl application remove <environment-uuid> <workload-uuid> --company acme --confirm

deployment reports the running, requested and interrupted builds and releases, the bound source and whether a push would deploy; build queues a build of the application's applied configuration from --source (or the bound selection), resolved to an exact commit by the source service rather than named here. deploy makes an existing release the one the application runs. runs and run-log read the process run history and one run's own output, which may report unavailable when the cluster cannot answer rather than an empty body. schedule-run runs a declared schedule once, now, beside the schedule's own runs; it does not execute an arbitrary command, for which the API has no route. update renames the label only, never the immutable name.

runtime --mode stop|start|restart uses the application's existing desired-state endpoint. Start and restart both record a fresh runtime revision, even when it is already running. An accepted stop is not proof that the Pod has stopped: wait for the environment to observe the stop generation before unbinding database delivery. Its dry-run sends no request, including no environment read.

removal reviews what removing one application would affect; remove re-reads that review and sends its own inventoryDigest, so a confirmation approves an exact inventory rather than a label, and the environment generation makes a stale review a conflict. Removal withdraws the runtime through normal reconciliation and deletes no retained data. Database retirement disables new logins while retaining data; erasure is separate. Every write (build, deploy, runtime, schedule-run, update, remove) needs --confirm; --dry-run describes the change and sends no mutation. Existing metadata-preview reads may still occur for other commands.

Asking a system what it sees​

hostingctl system orphans asks the system that manages a cluster what is left on it — folders and namespaces nothing claims, plus whether that cluster may be cleaned at all.

The command exists because a checkout cannot answer this. Listing a store from a workstation is easy, but the comparison that makes a folder an "orphan" is against the database that owns its rows. A developer checkout claims one environment, so every folder in every cluster reads as unclaimed: on 2026-10-19 that produced a confident report of 15 and 16 orphans on two clusters, all of it an artefact of the wrong database. The managing system owns both the rows and the credentials, so its answer is the only one.

state and cleanup are the same rule the scheduled cleanup uses: free may be cleaned, held means a run holds the cluster and only its own consent may retire its leftovers, unknown means the state manager could not name a holder and nothing may be touched. Each deployment answers only for the cluster it manages, named by HOSTING_SYSTEM_CLUSTER (falling back to the state manager's target cluster, then to placement's default). A deployment that names nothing refuses with the setting to change rather than describing someone else's cluster, and a cluster name that is not configured is ignored rather than trusted.

What each environment was sold for​

hostingctl purchases --company <slug> --from <2026-10-01T00:00:00Z> --to <2026-11-01T00:00:00Z> reads what each of your environments was sold at, and for how long it was held that way. The window is half-open — --from is included and --to is not — and it is required, because a default window would be an invented billing period.

The record holds facts and no amounts. A price is a price list times a period, and the price list is not ours to state, so what the record carries is the package, the size, and the interval. If an environment's package changes, that is two periods rather than one edited one: the first keeps what it was sold at, and the second carries the new size, and they share the instant the change happened. A cancelled environment stops being held at the agreed service end, and the record says so.

Cancelling hosting, and deletion locks​

The deletion decision is the client's: the confirmation is the approval, and the platform schedules the teardown itself. The same operations are on the terminal, which is what lets a cancellation be driven from a pipeline.

hostingctl cancellation show <uuid> --company acme
hostingctl cancellation request <uuid> --company acme --timing at-service-end \
--effective-at 2026-11-01 --reason "Contract ended" --generation 7 --dry-run
hostingctl cancellation request <uuid> --company acme --timing now \
--reason "Launched by mistake" --generation 7 --confirm
hostingctl cancellation withdraw <uuid> <cancellation-uuid> --company acme --confirm
hostingctl cancellation delete-now <uuid> <cancellation-uuid> --company acme --generation 7 --confirm
hostingctl cancellation purge <uuid> <cancellation-uuid> --company acme --generation 7 --confirm
hostingctl cancellation reconfirm <uuid> <cancellation-uuid> --company acme \
--generation 7 --digest <inventory-digest> --confirm

show is a read: it returns the episode, the review, any recorded refusal, and which operations the current state allows. delete-now destroys the environment and keeps the backups to the retention date; purge destroys those backups too, so nothing is left to restore from. --timing is required rather than defaulted, because a request without one is the legacy shape that records no choice at all. --effective-at applies only to at-service-end; now ends service at the confirmation and takes no date.

--generation is the environment's current generation, from environment show — not the number cancellation show prints, which is the episode's recorded generation. Service end advances the environment's generation, so using the recorded one is refused as a stale confirmation rather than deleting the wrong thing. reconfirm also needs --digest from the review, because the generation alone cannot witness the scope.

Two kinds of lock stop a deletion, and either can be cleared:

hostingctl deletion-lock environment <uuid> --company acme --generation 7 --confirm
hostingctl deletion-lock environment <uuid> --company acme --unlock --generation 7 --confirm
hostingctl deletion-lock service <uuid> <workload-uuid> --company acme --unlock --generation 7 --confirm

An environment lock stops every delete inside it, including an application that carries its own; a service lock stops just that application. There is no separate read command on purpose: cancellation show already lists each blocker with the control that clears it.

Making a change​

The change commands are environment create, environment scale, environment reconcile, environment database create/bind/retire, access recover, access user add/remove, cancellation request/withdraw/delete-now/reconfirm and deletion-lock. They are not a shortcut around anything: they ask the same questions the panel does, and every one of them needs the same permission the panel would.

Each of them describes the change and stops unless you say otherwise:

hostingctl environment scale <uuid> --company acme --cpu 450 --dry-run
hostingctl environment scale <uuid> --company acme --cpu 450 --confirm

--dry-run prints what it would send and sends nothing. Without --confirm the command refuses and shows the same sentence, which is deliberate: on a terminal, a change should never be one keystroke away from a question. A scale carries the rest of the environment's configuration across unchanged, so raising a workload's memory does not disturb anything you did not name. --cpu and --memory set every workload's request and preserve its existing request-to-limit headroom. The resulting allocation must still fit the purchased package; the API refuses a change that does not.

To add a login, supply an existing public SSH key file, never its private counterpart:

hostingctl access user add <uuid> --company acme --username acme_accept \
--name "Acme Accept" --public-key-file ~/.ssh/id_ed25519.pub --dry-run
hostingctl access user add <uuid> --company acme --username acme_accept \
--name "Acme Accept" --public-key-file ~/.ssh/id_ed25519.pub --confirm
hostingctl access users <uuid> --company acme --json
hostingctl access user remove <uuid> --company acme --access-user <user-uuid> --confirm

Usernames use lowercase letters, digits and underscores, starting with a letter, and are 3–32 characters long. Use the native user's uuid from the listing to remove it. The key's label defaults to --name; --key-name overrides it. Missing, empty, oversized or private-key files are refused before any API request. Passwords are not accepted on the command line.

Create mailboxes from a declaration​

Use the mailbox CSV or versioned YAML format to create missing mailboxes on an attached email name:

hostingctl mailbox import preview <email-domain-uuid> --company acme \
--format csv --file ./mailboxes.csv
hostingctl mailbox import confirm <email-domain-uuid> --company acme \
--format csv --file ./mailboxes.csv --confirm

Use --format yaml for YAML, and --json for machine-readable outcomes. Confirmation previews the exact file again and submits its review token with the same contents; changes or conflicts are refused at the API. Matching active mailboxes keep their quotas and credentials. --dry-run sends nothing, including no preview request. Files are limited to 64 KiB and never include passwords. Accepted creation still needs asynchronous native provisioning. This operation does not copy old mail, change MX records or alter existing mailboxes.

Mailbox setup and recovery​

Read a mailbox's mail-client settings, email them on, manage its fallback recovery address, and issue reset links:

hostingctl mailbox setup <domain-uuid> <mailbox-uuid> --company acme
hostingctl mailbox instructions <domain-uuid> <mailbox-uuid> --company acme \
--recipient user@example.com --confirm
hostingctl mailbox fallback set <domain-uuid> <mailbox-uuid> --company acme \
--address backup@example.com --confirm
hostingctl mailbox recovery-link copy <domain-uuid> <mailbox-uuid> --company acme --confirm

setup prints the address and username, the incoming IMAP and outgoing SMTP host, port and security, the authentication requirement and the webmail URL. These are the deployment's named values (mail.hosting.haak.co IMAP 993 TLS, SMTP 465 TLS, webmail webmail.hosting.haak.co/webmail), never guessed from your domain or its MX records; anything unconfigured prints Unavailable. A fallback address stays unconfirmed until its verification link is completed, and replacing or removing it revokes outstanding links. recovery-link copy prints the reset URL once with its expiry and a sharing warning. Writes need --confirm; --dry-run describes without sending.

A mailbox owner who cannot sign in needs no Hosting account:

hostingctl mailbox recovery request --address user@example.com --confirm
hostingctl mailbox recovery verify --token-file ~/token --confirm
hostingctl mailbox recovery reset --token-file ~/token --password-file ~/password --confirm

These three commands are the only ones that run without a token or session, aimed with --api-url or HOSTINGCTL_API_URL. The recovery token and the new password come from a mode-600 --<name>-file or --<name>-stdin, never an argument, and are never printed. reset reports the change as recorded and applied in the background; it does not change your application passwords or active sessions.

Mailbox import from CSV or YAML​

Create many mailboxes at once on one attached mail name from a localPart,quota CSV or a versioned YAML document. Uploading and previewing writes nothing; the confirmed import needs the same permission as creating one mailbox by hand.

hostingctl mailbox import preview <domain-uuid> --company acme --format csv --file mailboxes.csv
hostingctl mailbox import confirm <domain-uuid> --company acme --format yaml --file mailboxes.yaml --dry-run
hostingctl mailbox import confirm <domain-uuid> --company acme --format yaml --file mailboxes.yaml --confirm

The --file is a localPart,quota CSV or a versioned YAML document, and --format says which of the two it is (csv or yaml); the file is read from disk and never printed. preview classifies every declaration against the name and reports what it would create, what already matches and what is refused, without writing anything. confirm re-runs that review to obtain the digest the API requires, then creates the reviewed declarations; it needs --confirm, and --dry-run describes the batch without sending it.

An import is create-only: a repeat of matching declarations is a no-op, a conflicting declaration is a refusal, and a refused batch creates nothing. Creating a mailbox does not deliver its password — each new mailbox receives its credential through its own owner — and an import never configures source accounts, starts a sync or changes DNS.

Portable site pattern​

Export one authorized environment's supported setup as YAML, then preview and import it into another environment you are authorized for. The pattern is portable setup intent with logical references, not a clone, and it never carries a secret value, a source-company identifier, customer messages or data.

hostingctl environment portable export <environment-uuid> --company acme --out acme.yaml
hostingctl environment portable preview <environment-uuid> --company acme --file acme.yaml \
--bindings bindings.json
hostingctl environment portable prepare <environment-uuid> --company acme --file acme.yaml \
--bindings bindings.json --confirm
hostingctl environment portable confirm <environment-uuid> --company acme --file acme.yaml \
--bindings bindings.json --confirm

export prints the pattern as YAML on stdout (or writes it to --out <path>); --selection is a JSON file naming the sections and linked names to include. preview classifies every declaration against the target and writes nothing, reporting each section as create, reuse, conflict, unresolved, refused or unsupported plus the declared secrets and any blockers. --bindings is a JSON file that maps the pattern's logical references to authorized target resources and grants the consequential operations a section needs; nothing is adopted by a name alone, and no file carries a secret value. A selection names the sections and linked names to include:

{
"sections": ["hosting", "domains", "dns", "email", "access", "sources"],
"domains": ["shop.example.com"],
"emailDomains": ["example.com"],
"dns": ["example.com"],
"access": true,
"sources": true
}

A bindings file maps the pattern's references to target identifiers and grants the consequential operations. loginKeys carries the public half of a key a declared SFTP/SSH login needs, and a consent left false keeps its operation refused:

{
"bindings": {
"applications": { "web": "<target-application-uuid>" },
"repositories": { "web": "<target-repository-selection-uuid>" },
"domains": { "shop.example.com": "<company-domain-uuid>", "example.com": "<company-domain-uuid>" },
"emailDomains": { "example.com": "<email-domain-uuid>" },
"services": { "cache": "<service-uuid>" },
"siteUsers": { "person@example.com": "<site-user-uuid>" },
"accessUsers": { "deploy": "<access-user-uuid>" },
"members": { "person@example.com": "<company-member-user-uuid>" },
"aliasRecipients": ["external@example.com"],
"loginKeys": { "deploy": "ssh-ed25519 AAAA... deploy@laptop" }
},
"consents": { "serviceAddition": false, "emailInvitations": false, "autoDeploy": false }
}

A binding always names a target that already exists: an existing workload, the repository selection a source builds from (repositories), a company-owned website name and the customer DNS zone a record set lives in (both domains), the attached email name (emailDomains), an existing access login (accessUsers), an existing agency site user (siteUsers), the Company member a grant names (members), and any alias destination the pattern does not itself declare (aliasRecipients). A declaration that creates a resource — a new application, mailbox or login — needs no binding, and a name that merely matches a target is never authorization.

prepare records a reviewed draft without applying resource changes. It may create binding metadata and advance the configuration generation, making declared secret names writable. Set the unset values through the existing secret control, then preview and confirm. confirm records create/reuse intent and queues normal reconciliation; it does not purchase, cut over DNS or remove anything. Its answer is accepted intent rather than a ready site. Both writes need --confirm, and --dry-run describes the change without sending it. A target that changed since the review is refused by the API rather than applied against state nobody saw.

What your token can see​

The CLI can do neither more nor less than the token it is given. A company-scoped token sees that company's environments; a platform-wide command answers a refusal instead, and the CLI reports the API's own message with its request id rather than pretending the tool is broken. A refusal is a useful answer — it is how you tell that the token is the limit rather than the platform.