CLI
geopera is the official command-line interface for the platform — a structured command tree where every operation is its own command, with typed --flags derived from the request schema and a --json fallback for complex bodies.
Overview
The CLI is a thin auth-and-dispatch shell over the typed geopera Python SDK. It does two things and does them well: it manages your credentials (sign-in, token refresh, the choice between a bearer token and an API key), and it dispatches every platform operation. There is no per-command business logic — each command is generated from the platform’s operation schema — so the surface stays in lock-step with the API and a newly shipped operation is callable the moment your snapshot includes it.
Two layers sit on top of that dispatch core:
- A structured command tree that mirrors the operation namespace directly into commands. An operation id like
catalog.searchbecomesgeopera catalog search;orders.archive.placebecomesgeopera orders archive place;orders.tasking.templates.savebecomesgeopera orders tasking templates save. The shape of the tree is exactly the shape of the Operations reference. This is the recommended way to use the CLI: discoverable, typed, and tab-completable. - A low-level escape hatch,
geopera op <operation_id>, that dispatches any operation id with a raw JSON body. It is hidden from--helpbut always available — useful for scripting where the operation id is dynamic, or to reach an operation that is not yet in your snapshot.
Each command in the tree exposes the fields of its request body. Scalar fields are plain --flags; array-of-scalar fields are repeatable; boolean fields are paired --x / --no-x; and fields marked [required] in --help must be supplied. Anything more complex — nested objects, arrays of objects, free-form geometry — is supplied with --json, and any --flags you add override matching keys from that JSON.
Every operation is a POST. The CLI does not expose, and the API does not offer, “idiomatic GET routes” — there is one verb and the variation lives entirely in the request body.
The package is 0.2.0 and currently in Beta.
Install
pip install geopera-cliThis pulls in typer, httpx, click, and the geopera SDK (>=2.0.0). It requires Python 3.11+ (tested on 3.11, 3.12, and 3.13).
Installation registers a single console command, geopera:
geopera --helpRunning geopera with no arguments also prints help, so the entry point is self-describing.
Hello, world
Sign in once, confirm who you are, then run an operation as a command:
# Sign in (opens your browser; RFC 8628 device flow)
geopera login
# Who am I?
geopera whoami
# Search a catalog
geopera catalog search --collections sentinel-2-l2a --limit 10login establishes a credential under your active profile; every other command loads that credential and carries it to its operation.
How the command tree is built
The structured tree is generated from a spec snapshot — a spec.json file bundled inside the installed package — rather than fetched at runtime. That makes the tree offline, instant, and tab-completable, and it refreshes with each release of the CLI. For each POST /v1/op/{operation_id} path in the snapshot the generator:
- Splits the operation id on
.into namespace segments —orders.archive.placebecomes the groupsordersthenarchiveand the leaf commandplace. - Reads the operation’s JSON request-body schema and turns each scalar or array-of-scalar field into a flag. Strings, integers, numbers, and booleans become typed flags; arrays of those become repeatable flags. Nullable fields (modeled as
anyOf: [<real>, {type: null}]) are collapsed to their underlying type, so they flag cleanly. - Leaves every complex field (nested objects, arrays of objects) off the flag list — those are reachable only through
--json. - Adds the universal
--json,--profile, and--api-urloptions to every leaf.
Operation ids are guaranteed collision-free as a tree: no id is a prefix of another, so every id is an unambiguous leaf and no name is ever both a group and a command.
Operations that require an admin:* scope are excluded from the generated tree — they are internal and stay reachable only through the op escape hatch for the rare privileged caller. Everything customer-facing is in the tree.
Run operations as commands
Each command surfaces its request fields as flags. Scalars become --flags, arrays are repeatable, and booleans toggle with --x / --no-x:
# Scalar + repeatable-array flags
geopera catalog search \
--collections sentinel-2-l2a \
--collections landsat-c2-l2 \
--limit 10Field names are mapped to flags by replacing underscores with dashes, so a request field item_id is supplied as --item-id. Run any command with --help to see its fields, which are required, and their types:
geopera catalog search --helpRequired fields are annotated [required] in their help text. Each leaf’s help also prints the underlying Operation: <id> so you can always cross-reference the Operations reference.
Deeply nested operations
The command tree goes as deep as the namespace. Placing an archive order is geopera orders archive place:
geopera orders archive place \
--item-id S2B_36MYE_20240101_0_L2A \
--product analyticComplex bodies with —json
When a request has a large or nested body — or a field the generator left off the flag list because it is a nested object — pass the body as JSON instead of (or alongside) flags. --json accepts an inline string, a file with @, or stdin with -:
# inline JSON
geopera catalog search --json '{"collections": ["sentinel-2-l2a"], "limit": 10}'
# from a file
geopera orders archive place --json @order.json
# from stdin (composes with jq, heredocs, other tools)
echo '{"collections": ["sentinel-2-l2a"]}' | geopera catalog search --json -Flags override keys from --json, so you can keep a base request in a file and tweak one field on the command line. The JSON is parsed first into the request body, then any flags you set are written over the top:
geopera catalog search --json @base-search.json --limit 50For a leaf command, --json must resolve to a JSON object (the request body). Passing a JSON array or scalar to a leaf is an error; the raw op command is the place for non-object bodies.
Output and exit codes
Output is always pretty-printed JSON on stdout, so the CLI composes cleanly with jq and shell pipelines. On failure the platform’s RFC 9457 application/problem+json response is rendered as a readable [status] message — detail line on stderr and the process exits non-zero:
| Exit code | Meaning |
|---|---|
0 | Success. |
1 | Local/usage error — auth not configured, unreadable --json file, malformed JSON, bad invocation. |
2 | Operation error — the platform returned a problem+json response (4xx/5xx). |
See Errors for the full problem+json taxonomy.
Commands
| Command | What it does |
|---|---|
geopera login | Authenticate. Device flow by default; --api-key KEY stores a key for headless use. |
geopera logout | Clear the active profile’s stored credentials (best-effort logout). |
geopera whoami | Show the authenticated principal, org, and scopes (validates the session). |
geopera <resource> <action> [flags] | Run an operation as a command, with schema-derived --flags and a --json fallback. |
geopera orders list | Curated alias over orders.list with table-formatted output. |
geopera op OPERATION_ID [JSON] | Low-level escape hatch (hidden): dispatch any operation id with a raw JSON body. |
Two global options are accepted on every command (including each generated leaf):
| Flag | Env | Default |
|---|---|---|
--profile NAME | GEOPERA_PROFILE | default |
--api-url URL | GEOPERA_API_URL | https://api.geopera.com |
The full reference for each command and flag lives on Commands.
Authenticate
geopera login runs the OAuth 2.0 Device Authorization Grant (RFC 8628) with PKCE. It generates a PKCE verifier/challenge pair, requests a device and user code, prints the user code, opens the verification URL in your browser, and polls until you approve:
geopera login
# To sign in, visit:
# https://api.geopera.com/device?user_code=WDJB-MJHT
# And confirm this code:
# WDJB-MJHTOnce approved, the resulting session token (and its refresh token) are stored under the active profile. The CLI confirms the sign-in with a userinfo round-trip and prints who you are. Session tokens are then refreshed automatically — proactively when within 30 seconds of expiry, and reactively on any 401. The platform rotates the refresh token, so both tokens are rewritten on each refresh.
Two flags shape the device flow:
# Print the code but do not auto-open a browser (headless terminals).
geopera login --no-browser
# Request a specific OAuth scope.
geopera login --scope "openid profile"API keys (headless and CI)
For CI and other non-interactive use, store a minted API key instead of signing in through a browser. Keys carry the gpra_ prefix and are sent on every authenticated request as the X-API-Key header:
geopera login --api-key gpra_xxxxxxxx
# keep the key out of shell history by piping it in:
printf '%s' "$GEOPERA_KEY" | geopera login --api-key -login --api-key validates the key with a userinfo round-trip before saving it, so a bad key fails fast. The API key is the credential — there is no token exchange, no password grant, and no refresh step for keys; the value you store is the value that authenticates each request. (A browser sign-in instead yields a short-lived session token, used the same way.)
Both credential types resolve to the same principal model — scopes, audit, and provenance are identical. See Authentication for the device flow, key storage, and the credential store, and the platform Authentication and Scopes references for how credentials are minted and what they authorise.
whoami
geopera whoami validates the active credential and prints the resolved principal. Add --json for the raw userinfo document:
geopera whoami
# sub: usr_01H8...
# principal_type: user
# org_id: org_01H8...
# scope: openid profile
# api_url: https://api.geopera.com
# profile: defaultLow-level escape hatch
geopera op is a hidden, generic command that dispatches any operation id with a raw JSON body. Use it for scripting where the operation id is dynamic, to reach an admin:* operation excluded from the tree, or to call an operation that is not in your CLI’s snapshot. The structured geopera <resource> <action> form is preferred for everyday use.
The body is JSON, supplied as a positional argument, from a file with --file, or from stdin with -:
# inline JSON
geopera op catalog.search '{"collections": ["sentinel-2-l2a"], "limit": 10}'
# from a file
geopera op orders.archive.place --file order.json
# from stdin (composes with jq, heredocs, other tools)
echo '{}' | geopera op orders.list -Operations with no required body can be called bare — an empty body defaults to {}:
geopera op orders.listDiscover operations
--list enumerates every operation id from the live OpenAPI document, so the list always matches the deployment you are pointed at:
geopera op --listThis prints one operation id per line on stdout (with a N operations. count on stderr), making it easy to filter:
geopera op --list | grep '^orders\.'The mechanics of body resolution, exit codes, and listing are detailed on op.
Profiles and configuration
Multiple identities are namespaced by profile. Each is stored under its own key in the credential store and selected with --profile or GEOPERA_PROFILE:
geopera login --profile staging --api-url https://staging.api.geopera.com
GEOPERA_PROFILE=staging geopera whoami
geopera whoami --profile staging # equivalentCredentials live in ~/.config/geopera/credentials.json (directory 0700, file 0600). Each top-level key is a profile holding its api_url and an auth block — {"type": "oauth", ...} for a session token, or {"type": "api_key", "api_key": "gpra_..."} for a key.
The API base URL resolves in this order: --api-url flag → GEOPERA_API_URL → the profile’s stored api_url → https://api.geopera.com. For ephemeral CI use, GEOPERA_API_TOKEN supplies a session token or API key directly, bypassing the store entirely; a value starting with gpra_ is treated as an API key. See Configuration for the full precedence rules and store format.
Worked example: search, then estimate
A complete two-step flow — search a catalog, pull a result id out with jq, then price an estimate — using structured commands and standard shell tooling:
# 1. Authenticate once (CI-style, no browser).
export GEOPERA_API_TOKEN="gpra_..."
# 2. Search the catalog and keep the first matching item id.
ITEM=$(geopera catalog search \
--collections sentinel-2-l2a \
--limit 1 | jq -r '.features[0].id')
# 3. Price an order against that item.
geopera orders estimate --item-id "$ITEM" --product analyticBecause GEOPERA_API_TOKEN is set, no login step is required and nothing is written to disk — ideal for ephemeral runners. More patterns, including masked secrets and matrix jobs, are on CI recipes.
Gotchas
- Underscores become dashes. A request field
max_cloud_coveris the flag--max-cloud-cover. Use--helpon the leaf to see the exact flag names. - Nested fields have no flag. If a field is a nested object or an array of objects it will not appear as a
--flag; supply it through--json. Mixing the two is fine — flags win on key collisions. --jsonon a leaf must be an object. Non-object bodies (arrays, scalars) are only valid through the rawopcommand.- Admin operations aren’t in the tree. Operations gated behind
admin:*scope are reachable only viageopera op; you will not find ageopera <resource> <action>form for them. - The tree is a snapshot. It reflects the spec bundled with your installed CLI version. To pick up brand-new operations as named commands, upgrade the package — or reach them immediately with
geopera opandgeopera op --list.
Explore
Links
- PyPI: pypi.org/project/geopera-cli
- Source: github.com/geo-pera/geopera-cli