Command Line
Drive the Campaign API from a terminal or a coding agent, with the same Organization API Key.
hb is the Campaign API with a terminal in front of it. It calls the same host, with the same API Key, over the same typed client the Developer Portal uses, so nothing here can do something the API would refuse.
export HB_API_KEY=hb_live_sk_...
hb auth whoamihumanbased is the same program under a longer name, for a shell where hb is already taken.
What it is, and what it is not
It is a messenger. Who you are, whether you may change this Campaign, whether the document is valid, whether the money is there — every one of those is the platform's answer, and hb exists to send your intent accurately and put the reply in front of you unaltered.
So there are things it deliberately cannot do. It cannot confirm a launch: that needs a signed-in person, and no flag stands in for one. It cannot publish or pause a Campaign, because neither has an API operation an API Key can call. It cannot create, rotate or revoke an API Key. And it never writes your Key to disk — a profile records where to send a request, never what to send with it.
There is no hb login. The Key arrives from HB_API_KEY or --token-stdin, for one run, and is never persisted. If a command appears to need a stored credential, that is a bug, not a missing feature.
Install
The package is not published yet. Until it is, build it from the monorepo:
pnpm --filter @humanbased/cli build
node packages/cli/dist/hb.js --versionThe shape of every command
hb <resource> <verb> [id] [--flags]--json puts exactly one envelope on standard output and nothing else. Progress, warnings and suggested next commands always go to standard error, so a pipe carries only the payload:
hb campaign list --json | jq '.data.items[].campaign_id'The envelope is stable, and data holds the API's own payload unchanged:
{
"apiVersion": "humanbased.cli/v1",
"command": "campaign.list",
"requestId": "e9dc2862-d165-44ce-b54a-42111284bd40",
"context": {
"profile": null,
"apiOrigin": "https://api.humanbased.ai",
"organizationId": "10571000020000000001"
},
"data": { "items": [] },
"notices": [],
"error": null
}Those seven keys are the whole envelope, on success and on failure alike. error === null is the check — there is no ok field to read. data usually carries the payload on success and is null on a failure, but do not branch on it: a refusal that names its findings puts them in error.details, and some commands report a receipt as data while exiting non-zero.
notices is for what is true, not a failure, and would otherwise only reach a person on stderr. It is [] almost always. Today one thing fills it: hb campaign launch execute for a proposal made on another machine, where this client cannot say which campaign the launch is for. If you are driving a launch from an agent, stop on a non-empty notices rather than on the exit code — the launch succeeds, and what you were not told is what it launched.
Exit codes
The exit code is the machine-readable half of the answer. Branch on it, and on error.code inside the envelope — never on error.message, which is prose and gets reworded.
| Exit | Meaning | What to do |
|---|---|---|
| 0 | Done, under that command's documented completion rule | Nothing |
| 1 | The document is invalid | Read findings, fix the file |
| 2 | The command or its arguments are wrong | Fix the command line |
| 3 | No usable credential | Set HB_API_KEY |
| 4 | This credential may not do it | Check hb auth whoami |
| 5 | A conflict, or an outcome a person must decide | Read the message; do not retry blindly |
| 6 | Transient | Retry, same idempotency key |
| 7 | The backend attempted it and it failed | Inspect the receipt |
| 8 | A spend cap or a quota | Raise the cap, or wait |
| 9 | Your wait deadline elapsed | The server job may still be running |
| 10 | This client and the deployment disagree about the contract | Upgrade one of them |
| 130 | Interrupted | Nothing was retried |
Where requests go
Four fields describe a destination, and precedence runs flag, then environment variable, then profile:
hb config profiles set staging \
--environment staging \
--api-url https://api.staging.humanbased.ai \
--org 10571000020000000001
hb config profiles use stagingThere is no default host. A command with nowhere to go says so rather than guessing, because guessing production is the one wrong answer that looks like it worked. An --api-url must match its environment: production and staging accept only their own origin, and local accepts only loopback. A profile that has been hand-edited into an unknown shape is refused rather than partially honoured.
If a profile names an organization, a write checks the credential actually belongs to it before anything is sent. A Key for the wrong organization is a 403 at the first write otherwise, which is a confusing way to learn it. Reads do not carry the check: they cannot spend anything, and failing them on a mismatched profile would refuse work the key can legitimately do.
Every command
N makes a network call, W changes server state, K takes an idempotency key, H needs a person outside this tool.
| Command | Needs | What it does | |
|---|---|---|---|
hb auth whoami | — | N | Report the verified caller behind this credential |
hb capabilities | campaign:read | N | What this release supports and what this credential may do |
hb config profiles list | — | Every configured profile | |
hb config profiles show | — | One profile, current by default | |
hb config profiles set | — | Create or replace a profile | |
hb config profiles use | — | Make a profile the default | |
hb config profiles remove | — | Forget a profile | |
hb campaign list | campaign:read | N | Campaigns this credential can read |
hb campaign get | campaign:read | N | One campaign, and this caller's capabilities on it |
hb campaign versions list | campaign:read | N | Version history, newest first |
hb campaign versions inspect | campaign:read | N | One version's document, ready to edit and send back |
hb campaign versions diff | campaign:read | N | What changed between two stored versions |
hb campaign observation | campaign:read | N | Counts for one event-time window, with their denominators |
hb campaign init | campaign:read | N | Write a local candidate, from a template or an existing version |
hb campaign validate | campaign:author | N | Validate a candidate against the authoritative validator |
hb campaign feasibility | campaign:author | N | Quoted pay range for a candidate, and what cannot be answered |
hb campaign plan | campaign:author | N | The change set applying a candidate would produce |
hb campaign create | campaign:author | NWK | Allocate a campaign, provision its pipeline and write version 1 |
hb campaign apply | campaign:author | NWK | Apply a candidate to a campaign's draft, once |
hb campaign actions inspect | campaign:author | N | The receipt for one write, by action id or by request key |
hb campaign launch propose | campaign:launch | NW | Describe the launch this credential intends, for a person to confirm |
hb campaign launch execute | campaign:launch | NWKH | Run the launch a person confirmed, once |
hb template list | campaign:read | N | Templates a candidate from this organization may reference |
hb template get | campaign:read | N | One template's field contract |
hb template init | — | Write a starting template document | |
hb template validate | campaign:author | N | Validate a template document against the authoritative validator |
hb template apply | campaign:author | NWK | Create or update a template, once |
Capabilities do not imply one another. A command's entry above is what every way of calling it needs, and hb capabilities uses that to say available or unavailable, naming what is missing rather than only refusing.
Two commands need campaign:read in addition, but only in the one mode that reads a stored version:
hb campaign applywith--base <n>hb campaign planwhen planning against an existing campaign (--createreads no version)
So a key with campaign:author and no campaign:read applies a reviewed plan (--plan) and recovers a lost response (--resume), and gets a 403 only from --base. hb capabilities reports such a command as partly available and names the mode, rather than calling the whole command unavailable and sending you to mint a key you do not need.
hb --help --json prints this same inventory, including each command's flags and the API operations it calls. It needs no credential and makes no network call, which makes it the right thing for an agent to read first.
Changing a Campaign
Five commands change server state. The rest cannot, and hb --help --json is how you check that claim rather than trusting this sentence.
A change is a document, a check, and one send:
hb campaign versions inspect 10571000020000000042 --out campaign.json
# edit campaign.json
hb campaign validate --file campaign.json
hb campaign plan 10571000020000000042 --file campaign.json
hb campaign apply 10571000020000000042 --file campaign.json --base 4 --idempotency-key change-2026-09-22-aLeaving --version off means the newest one, which is almost always what you
want. There is no latest keyword: omission already says it, and two spellings
for one thing is how a reader ends up using the one that does not work. The
version number inspect reports is what goes in --base on the apply, and
that is what stops a change landing on a base somebody else moved.
plan is worth running every time. It reports what would change and what the platform would refuse, before anything is sent — and apply re-runs it internally and stops if the answer moved, so a Campaign someone else edited in between cannot be overwritten on the strength of a stale plan.
A placeholder is refused locally. hb campaign init writes <DECIDE: …> on every number you have to choose, and validate, plan, create and apply all stop while one remains. The platform's own validator does not look inside a workflow node's parameters, so without this a reward amount could be applied as the sentence asking you to pick one. --allow-placeholders overrides it.
Retrying safely
--idempotency-key is a name you choose for one intended change, and it is what makes a retry safe: the same key replays the first attempt's receipt instead of doing the work twice.
Nothing is ever re-sent automatically. When a response is lost, ask what happened:
hb campaign actions inspect --request-key change-2026-09-22-aFour answers, four different moves:
status | What it means | What to do |
|---|---|---|
succeeded | The change landed | Nothing |
failed | It was attempted and rejected | Fix the cause, use a new key |
in_progress | Still running | Wait, then ask again |
uncertain | A write was attempted and the platform cannot say whether it landed | Read steps, look at the Campaign, decide as a person |
uncertain is the one to slow down for. Never retry it, and never reuse that key — the whole point of the state is that nobody knows yet which half of the work exists.
Launching
Launching spends money, so it is three steps and the middle one is not yours:
hb campaign launch propose 10571000020000000042
# a person confirms that exact proposal, with a signed-in session (see below)
hb campaign launch execute \
--proposal 10571000020000000777 \
--idempotency-key launch-2026-09-22propose writes a proposal and moves no money. Confirmation is session-only and publishes no capability at all, so a Key confirming its own proposal is exactly what the design refuses — which is why hb has no command for it. execute runs only a proposal a person already confirmed, and only once.
propose opens the confirmation page. It resolves the page from the environment your profile names, and opens it when you are at a terminal — --no-browser prints the URL instead, which is also what happens when nothing is watching, such as in CI or under an agent. The page reads the proposal from the server, so what a person agrees to is what the server says the launch is, not what your machine printed.
That page is not built yet. The launchconfirm operation exists and is session-only, but the Developer Portal does not surface it, so the URL propose opens will not resolve until that page ships. Until then, confirm by calling the operation with a session — propose prints the exact body when it cannot name a page. Worth knowing before you build a launch into a pipeline.
execute names no campaign, and takes no --wait. POST launch carries the proposal id and your key and nothing else, so a campaign id on the command line would bind to nothing — and on a machine that did not make the proposal it would print one campaign while launching another. Instead, when propose ran here, execute tells you which campaign and what amount before it sends. When it did not, it says so — and you should confirm what the proposal is for before continuing.
If a launch's response is lost
Re-send the same --idempotency-key. The server looks the key up in its action ledger before it does anything else and replays the first attempt's receipt, so the same key returns replayed: true, the same action id, and launches nothing a second time. A different key is a different action and would launch again — so a lost response is recovered by repeating the command verbatim, never by minting a fresh key.
On uncertain — the platform could not determine what happened, usually a billing call that never answered — re-sending is still safe and is also not a fix: it replays that same uncertainty. That one is a person's call, from the receipt's steps, the campaign and the budget.
What is missing is the other route: reading a launch receipt out of band. The action-receipt operation covers campaignfile.apply and template.apply only, so you cannot ask "what became of action X" without re-sending its key (IN-5641).
What has no command yet
Datasets, collections and listings have no Campaign API operations, so they have no commands. Neither do submissions, payouts or Key management. hb capabilities reports what the deployment you are pointed at actually supports, which is a better answer than this page after either side ships.
The command contract names five more. Four of them have no API operation behind them at all, so a command would have to invent one:
| Contract command | Why there is none |
|---|---|
schema campaignfile | No schema operation is published. hb campaign validate asks the authoritative validator instead, which is the same question answered by the thing that decides. |
auth keys list | No key-listing operation. hb auth whoami reports the key you are holding, which is all the API exposes. |
publish | No operation; publishing is a Portal action. |
pause | No operation; pausing is a Portal action. |
The fifth, operations inspect, is implemented — as hb campaign actions inspect, against getActionreceipt. The contract asks the lookup to take the canonical command as well as the key, and that matters: the receipt operation accepts only campaignfile.apply and template.apply, so a lookup that did not say which write it meant would answer about whichever one shared the key string. --action is that parameter, and asking it about a launch is refused rather than answered from the wrong family.
For an agent
Three things make this usable without a person reading output:
hb --help --jsonis the full inventory: commands, flags, capabilities, and the API operations behind each one. No credential, no network.--json --non-interactivenever prompts and never reads standard input except for--token-stdin.- A failure envelope carries
error.nextAction, an object withcodeand a runnablecommand, when there is a well-defined next step. It isnullrather than a suggestion that still needs a value filled in.
Do not parse human output. It is written for a person and changes accordingly.