CLI
The cuvo command, how it signs you in, and how an operation becomes a command.
@cuvo-health-us/cli puts the whole v1 contract on the command line. Every command is generated from the
same OpenAPI document the API validates against, so it cannot offer an
operation or a flag the contract does not have.
Published under the next tag, prerelease while the developer product settles, the same as the
SDKs:
pnpm add -g @cuvo-health-us/cli@next
Node 20 or newer.
Signing in
cuvo login
This opens your browser, signs you in as a portal member, and keeps the tokens in
~/.config/cuvo/credentials.json at mode 0600. The CLI registers itself once as a native public
client through dynamic registration, holds no secret, and catches the
authorization code on a listener bound to 127.0.0.1. It asks for offline_access alongside the
scopes its commands need, so it holds a refresh token and you sign in once. A member's token is
always a test mode principal; reaching live data needs a live client or a live API key.
On a machine with no browser of its own, a server over SSH or a container, sign in with the device flow instead:
cuvo login --device
The CLI prints a short code and the page to type it into, opens that page here when there is a
browser to open, and polls the authorization server until you have confirmed it on whichever
machine does have one. It writes the same profile cuvo login writes, so nothing downstream knows
which door you used. --no-open keeps it from opening anything locally. The code you confirm is
the only one you ever see: the code the CLI redeems for the token never reaches the screen.
For a server, or for live mode, sign in as a confidential client instead:
CUVO_CLIENT_SECRET=… cuvo login --client-id oac_…
The secret is never a flag. It comes from CUVO_CLIENT_SECRET or a prompt that does not echo, so
ps and your shell history never see it. cuvo whoami says which organization and mode the profile reaches, and cuvo logout
revokes the refresh token and forgets the profile. --profile <name> keeps several logins side by
side, which is how you hold a test member and a live client at once.
Calling an operation
cuvo <group> <verb> [arguments] [flags]
The group is the first path segment after /v1/. The verb is the operation's own name with the
group's noun removed, so GET /v1/cases is cuvo cases list and
POST /v1/cases/{id}/release_hold is cuvo cases release-hold. Path parameters are positional, in
the order the path names them; query parameters and flat body fields are flags.
cuvo cases list --status in_review --limit 100
cuvo cases release-hold case_abc
cuvo patients create --first-name Ada --last-name Lovelace --body-file address.json
cuvo webhook-endpoints create --url https://example.com/hooks --event-types case.approved.v1
cuvo visits list-slots --state CA --from 2026-09-12T00:00:00Z --to 2026-09-15T00:00:00Z
Sixty-five commands across seventeen groups, every one of them generated. Seven more are written by
hand: login, logout, whoami, completion, init, events tail and webhooks listen.
cuvo --help lists the groups and the scope each one spends, cuvo <group> --help its verbs, and cuvo <group> <verb> --help the flags, the scope, the
modes, and the fields only JSON can carry.
Bodies, arrays and booleans
A nested object, an array of objects, or a body that is one of several shapes cannot be written as
flags. Pass it whole with --body-file <path> or --body - for standard input; flags then override
single fields on top of it. An array of scalars is a repeatable flag. A boolean is --flag and
--no-flag, so a field can be turned off as well as on.
echo '{"decision":"approve","note":"looks fine"}' | cuvo test decide-case case_abc --body -
cuvo webhook-endpoints update whe_1 --no-include-resource
Following the feed
cuvo events tail --type order.shipped.v1
polls GET /v1/events and prints one event per line as JSON until you
stop it. Without --since it starts from the moment you ran it.
Listening locally
cuvo webhooks listen --forward-to http://127.0.0.1:4242/webhook
subscribes to this integration's realtime channel and POSTs every event
to that URL with a Cuvo-Signature header, signed the way a delivery is signed, so the handler you
have already written verifies it with verifySignature unchanged. The signing secret is minted for
the run and printed once, on stderr, when the listener starts: put that in CUVO_WEBHOOK_SECRET
while you develop. --events case.approved.v1 narrows what is forwarded and repeats. One line of
JSON per event on stdout, carrying the event id, its type, the status the handler answered and how
long it took. --forward-to must be https, or http on 127.0.0.1 or localhost.
It is a mirror of the event stream, not a delivery. Nothing is retried, nothing is acknowledged, and no delivery row is written anywhere: a handler that answers 500 gets a line on stderr and the event is gone. The retry ladder, the delivery log and a secret that survives the process belong to a real webhook endpoint, and that is what a retry path has to be tested against.
Completions
cuvo completion zsh > ~/.zsh/completions/_cuvo
cuvo completion bash > ~/.local/share/bash-completion/completions/cuvo
cuvo completion fish > ~/.config/fish/completions/cuvo.fish
The script is printed from the command tree this build carries, so it completes groups, verbs and each verb's flags. Nothing is checked in anywhere, which means nothing goes stale: print it again after upgrading the CLI and the operations the contract gained complete too.
Starting a project
cuvo init my-integration --language typescript
writes five files into that directory: a smoke script that creates a patient, records both consents,
files a case, plays the clinician approving it through test mode and prints the
events that came out; a webhook handler that verifies Cuvo-Signature before it parses anything; a
README; and a .env.example. --language python writes the same five against the Python SDK, and
with neither flag the CLI asks when there is a terminal to ask.
It scaffolds and stops: nothing is installed, and the commands to run next are printed on stderr. A
directory that already holds something is refused unless you pass --force.
Output
JSON on stdout, always, unless --table asks for aligned columns. Progress goes to stderr and
--quiet silences it, so a pipe carries the answer and nothing else. A failure is the same
problem document on stderr whether the API refused the request or the CLI never sent
it, so an agent switches on code and never parses two shapes.
| Exit | What happened |
|---|---|
| 0 | The command ran |
| 1 | The request was refused, or could not be made |
| 2 | The command line did not make sense |
The transport underneath is @cuvo-health-us/api, so the CLI inherits what the SDK already
guarantees: an Idempotency-Key minted for every write, retries with backoff on 429 and on the
server faults a second attempt can clear, Cuvo-Organization when you name an organization, and an
access token refreshed a minute before it runs out.