Using geopera op
geopera op is the low-level escape hatch built into the Geopera CLI: it sends a single operation by id with a raw JSON body and prints the response. Every capability in the platform is reached through one generic endpoint — POST /v1/op/{operation_id} — so geopera op can invoke any operation, including ones that do not yet have a first-class command. It is the right tool for scripting, for piping JSON between programs, and for reaching new backend operations the moment they ship.
Prefer the structured commands
For everyday and interactive use, reach for the structured commands. Every operation has a first-class command that mirrors its id, with scalar fields exposed as typed --flags, arrays as repeatable flags, booleans as --x / --no-x, and validation built in:
# Structured (recommended)
geopera catalog search --collections sentinel-2-l2a --limit 10The command tree mirrors operation ids one-to-one — catalog.search becomes geopera catalog search, orders.archive.place becomes geopera orders archive place, and orders.tasking.templates.save becomes geopera orders tasking templates save. Structured commands also accept a complete JSON body for the parts that are not flag-shaped, via --json '<json>', --json @file.json, or --json - (stdin). See the command reference for the full set and the Operations catalogue for naming conventions.
geopera op is hidden from --help and exists for the cases the structured tree does not cover well:
- Scripting and pipelines — when you already have a complete JSON body, want to version-control it as a file, or want to pipe it in from another tool.
- Operations not in the snapshot — any operation that does not yet have a generated command is still reachable by id, with zero CLI upgrade required.
- Exact-fidelity bodies — when you want to send precisely the JSON you typed, with no flag-to-field translation in between.
If a structured command exists for what you are doing, use it. The rest of this page documents the escape hatch in full.
How it works
geopera op dispatches one operation by id with a JSON request body. The HTTP verb is always POST — there are no GET, PUT, or DELETE forms. List, get, estimate, place, and delete semantics all live in the operation id, never in an HTTP method. A request is sent to POST /v1/op/{operation_id} with Content-Type: application/json and the body you supply.
geopera op OPERATION_ID [BODY] [OPTIONS]OPERATION_ID is the operation name (for example orders.list, catalog.search, orders.archive.estimate). Because dispatch is fully generic, a new backend operation is instantly usable as geopera op <new.op> even before a structured command is generated for it.
| Argument / option | Description |
|---|---|
OPERATION_ID | The operation to invoke. Required unless --list is given. |
BODY | Positional JSON request body. Use - to read JSON from stdin. |
--file, -f <path> | Read the JSON request body from a file. |
--list | List available operation ids and exit. Ignores OPERATION_ID and BODY. |
--api-url <url> | API base URL override (env GEOPERA_API_URL). Default https://api.geopera.com. |
--profile <name> | Stored identity to use (env GEOPERA_PROFILE, default default). |
You must be authenticated first. Run geopera login for a browser sign-in (the resulting session token is used as a bearer), or geopera login --api-key gpra_... for headless use (the API key is sent as X-API-Key). The dispatcher loads the active profile and attaches the correct auth header automatically — you never construct it yourself. For the full picture see Authentication.
Supplying the request body
The body is resolved from exactly one source. The dispatcher checks the sources in this fixed precedence order and the first source that is present wins:
--file/-f <path>— the file’s contents are read and parsed as JSON. Highest precedence.- Positional
BODYargument, when it is not-. -as the positionalBODY— JSON is read from stdin.- Nothing supplied — the body defaults to
{}.
After a source is selected, its text is stripped of surrounding whitespace; if what remains is empty, the body becomes {}. Otherwise it is parsed as JSON. Invalid JSON, or an unreadable --file, is reported to stderr and the command exits with code 1 before any request is sent.
# 1. From a file (highest precedence)
geopera op catalog.search --file ./search.json
# 2. Inline positional JSON
geopera op orders.get '{"order_id": "ord_01HZX..."}'
# 3. From stdin (positional BODY must be exactly '-')
echo '{"limit": 50}' | geopera op orders.list -
# 4. No body — sends {}
geopera op orders.listBecause --file has the highest precedence, a --file value is used even if a positional BODY is also present — the positional argument is then silently ignored. To pipe JSON in, you must pass - explicitly as the positional argument; stdin is not read otherwise, so a bare pipe without - leaves the body as {}.
Precedence at a glance
| You ran | Body sent |
|---|---|
geopera op X -f a.json '{"k":1}' | contents of a.json (file wins) |
geopera op X '{"k":1}' | {"k":1} (positional) |
... \| geopera op X - | stdin contents |
... \| geopera op X | {} (stdin not read without -) |
geopera op X | {} |
geopera op X '' | {} (empty after strip) |
geopera op X '{bad' | nothing sent; Error: Invalid JSON body, exit 1 |
Output
On success the parsed JSON response is pretty-printed to stdout with two-space indentation, with the original key order preserved (keys are not sorted). This makes geopera op directly composable with tools like jq:
geopera op orders.list | jq '.orders[].id'Output edge cases:
- An empty
2xxresponse (no body) printsnull— the parsed value isNone, which serializes to JSONnull. - A
2xxbody that is not valid JSON is printed verbatim as raw text rather than reformatted. - All success output goes to stdout; informational lines (such as the
--listcount) go to stderr, so a clean stdout is always safe to pipe.
Errors
Operation failures are returned by the API as RFC 9457 problem+json. The dispatcher renders them as a single readable line on stderr and exits with code 2:
Error: [status] message — detailThe message is taken from the problem document’s detail, falling back to title, then message, then error_description, then error, and finally a generic "<operation_id> failed with HTTP <status>". The trailing — detail is appended only when a distinct detail field is present and differs from message. For example:
$ geopera op orders.get '{"order_id": "nope"}'
Error: [404] Order not found — No order exists with id 'nope'$ geopera op catalog.search '{}'
Error: [422] Request validation failed — body.bbox: field requiredLocal and authentication problems are distinct from operation errors and exit with code 1, not 2. These include: invalid JSON in the body, an unreadable --file, no stored credentials (Not logged in (profile '...')), an API key rejected with 401, and a session token that is expired and cannot be refreshed. They are reported as Error: <message>.
For session-token credentials the dispatcher refreshes transparently: a 401 triggers exactly one silent token refresh and a single retry of the same request. API keys are never refreshed — a 401 on an API key surfaces immediately as a local error (exit 1).
| Exit code | Meaning |
|---|---|
0 | Operation succeeded; JSON (or null) printed to stdout. |
1 | Local / auth failure — invalid JSON body, unreadable --file, no credentials, or a rejected API key. Nothing was sent, or auth failed. |
2 | The operation reached the API and returned a problem+json error. |
For the full problem+json schema and the catalogue of error types, see Errors.
Discovering operations
geopera op --list fetches the live API description from {api_url}/openapi.json, extracts every POST /v1/op/{id} path, and prints the ids one per line, sorted, followed by a count on stderr. This requires network access to the API but does not require a request body — any positional arguments are ignored.
$ geopera op --list
catalog.search
orders.archive.estimate
orders.get
orders.list
...
227 operations.Because the count line is written to stderr, redirecting it away leaves a clean list on stdout:
geopera op --list 2>/dev/null | grep '^orders\.'--list honours --api-url / GEOPERA_API_URL and the active profile’s stored URL when choosing which API to describe. If the description cannot be fetched (no network, a wrong --api-url, or a non-200 from /openapi.json), the command reports Error: Could not fetch operation list: ... and exits non-zero.
Profiles, API URL, and environment overrides
geopera op resolves the same identity and endpoint as every other command:
- Profile —
--profile <name>, elseGEOPERA_PROFILE, elsedefault. Each profile is a separately stored identity in~/.config/geopera/credentials.json. - API base URL —
--api-url <url>, elseGEOPERA_API_URL, else the URL stored in the active profile, elsehttps://api.geopera.com. - Ephemeral token —
GEOPERA_API_TOKENlets CI supply an opaque token without writing to the credential store. A value beginning withgpra_is treated as an API key (sent asX-API-Key); any other value is treated as a session token (sent asAuthorization: Bearer ...). This is the cleanest way to rungeopera opin a pipeline.
# Run against staging with an ephemeral key, no login, no stored profile.
export GEOPERA_API_URL=https://staging.api.geopera.com
export GEOPERA_API_TOKEN=gpra_xxxxxxxxxxxxxxxx
geopera op orders.list | jq '.orders | length'# Target a named profile explicitly for a one-off call.
geopera op --profile staging catalog.search --file ./search.jsonWorked example: estimate an archive order
orders.archive.estimate previews the cost and parameters of an archive order before you place it. With no body it sends {}:
geopera op orders.archive.estimateTo estimate a concrete request, supply a body. Reading it from a file keeps the JSON version-controlled and out of shell history:
{
"collection": "sentinel-2-l2a",
"bbox": [4.85, 52.35, 4.95, 52.4],
"datetime": "2024-06-01T00:00:00Z/2024-06-30T23:59:59Z"
}geopera op orders.archive.estimate --file ./estimate.jsonThe structured equivalent is geopera orders archive estimate, where each scalar field is a typed flag and complex sub-objects still accept --json @estimate.json.
Worked example: a catalog search
catalog.search queries the catalogue. The same body can be passed inline, from a file, or over stdin — pick whichever fits your shell.
# Inline
geopera op catalog.search '{
"bbox": [-122.5, 37.7, -122.3, 37.9],
"datetime": "2024-01-01T00:00:00Z/2024-03-31T23:59:59Z",
"collections": ["sentinel-2-l2a"],
"limit": 25
}'# From stdin, then post-process with jq
geopera op catalog.search - <<'JSON' | jq '.features | length'
{
"bbox": [-122.5, 37.7, -122.3, 37.9],
"collections": ["sentinel-2-l2a"],
"limit": 25
}
JSONFor interactive use, the structured command is shorter — geopera catalog search --collections sentinel-2-l2a --limit 25. Paging for operations that return many items is driven by parameters in the request body (for example a cursor or token field), not by the CLI — see Pagination.
Worked example: scripting a place-after-estimate flow
Because geopera op reads and writes plain JSON, it chains cleanly. Here the estimate output is reshaped with jq and fed straight into the place call over stdin, with errors surfacing through the exit code:
set -euo pipefail
estimate=$(geopera op orders.archive.estimate --file ./estimate.json)
echo "$estimate" \
| jq '{collection, bbox, datetime, estimate_id: .estimate_id}' \
| geopera op orders.archive.place -If orders.archive.place returns a problem+json error it exits 2, and set -e aborts the script before anything downstream runs.
Notes and gotchas
- Quote the JSON body so your shell does not split it on spaces. Single quotes around the whole object are simplest; for bodies that themselves contain single quotes, prefer
--fileor stdin. --filealways beats a positional body. If you pass both, the positional argument is ignored.- Stdin is only read when the positional
BODYis exactly-. A bare pipe without-leaves the body as{}. - An empty or whitespace-only body, from any source, is treated as
{}— not as a JSON error. - The verb is always
POST; there is no way to issue a GET throughgeopera op, because the platform exposes no GET operations. --listneeds network access, ignores any positional arguments, and writes its count to stderr.- Session tokens refresh automatically — a
401triggers one silent refresh and retry. API keys are never refreshed; a rejected key fails immediately. geopera opis hidden from--helpon purpose. It is supported, but the structured commands are the recommended surface.