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:

bash
# Structured (recommended)
geopera catalog search --collections sentinel-2-l2a --limit 10

The 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.

bash
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 / optionDescription
OPERATION_IDThe operation to invoke. Required unless --list is given.
BODYPositional JSON request body. Use - to read JSON from stdin.
--file, -f <path>Read the JSON request body from a file.
--listList 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:

  1. --file / -f <path> — the file’s contents are read and parsed as JSON. Highest precedence.
  2. Positional BODY argument, when it is not -.
  3. - as the positional BODY — JSON is read from stdin.
  4. 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.

bash
# 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.list

Because --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 ranBody 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:

bash
geopera op orders.list | jq '.orders[].id'

Output edge cases:

  • An empty 2xx response (no body) prints null — the parsed value is None, which serializes to JSON null.
  • A 2xx body 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 --list count) 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:

bash
Error: [status] message — detail

The 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:

bash
$ geopera op orders.get '{"order_id": "nope"}'
Error: [404] Order not found — No order exists with id 'nope'
bash
$ geopera op catalog.search '{}'
Error: [422] Request validation failed — body.bbox: field required

Local 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 codeMeaning
0Operation succeeded; JSON (or null) printed to stdout.
1Local / auth failure — invalid JSON body, unreadable --file, no credentials, or a rejected API key. Nothing was sent, or auth failed.
2The 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.

bash
$ 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:

bash
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>, else GEOPERA_PROFILE, else default. Each profile is a separately stored identity in ~/.config/geopera/credentials.json.
  • API base URL--api-url <url>, else GEOPERA_API_URL, else the URL stored in the active profile, else https://api.geopera.com.
  • Ephemeral tokenGEOPERA_API_TOKEN lets CI supply an opaque token without writing to the credential store. A value beginning with gpra_ is treated as an API key (sent as X-API-Key); any other value is treated as a session token (sent as Authorization: Bearer ...). This is the cleanest way to run geopera op in a pipeline.
bash
# 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'
bash
# Target a named profile explicitly for a one-off call.
geopera op --profile staging catalog.search --file ./search.json

Worked 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 {}:

bash
geopera op orders.archive.estimate

To estimate a concrete request, supply a body. Reading it from a file keeps the JSON version-controlled and out of shell history:

json
{
	"collection": "sentinel-2-l2a",
	"bbox": [4.85, 52.35, 4.95, 52.4],
	"datetime": "2024-06-01T00:00:00Z/2024-06-30T23:59:59Z"
}
bash
geopera op orders.archive.estimate --file ./estimate.json

The 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.

bash
# 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
}'
bash
# 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
}
JSON

For 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:

bash
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 --file or stdin.
  • --file always beats a positional body. If you pass both, the positional argument is ignored.
  • Stdin is only read when the positional BODY is 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 through geopera op, because the platform exposes no GET operations.
  • --list needs network access, ignores any positional arguments, and writes its count to stderr.
  • Session tokens refresh automatically — a 401 triggers one silent refresh and retry. API keys are never refreshed; a rejected key fails immediately.
  • geopera op is hidden from --help on purpose. It is supported, but the structured commands are the recommended surface.

See also