Skip to content

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.

ExitWhat happened
0The command ran
1The request was refused, or could not be made
2The 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.