<!-- Source: https://docs.geopera.com/api-reference/sdks/cli/op · Markdown for LLMs -->

# 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](/api-reference/sdks/cli/commands) for the full set and the [Operations](/api-reference/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 / 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](/api-reference/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 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`:

```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 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](/api-reference/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 token** — `GEOPERA_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](/api-reference/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

<CardGrid columns={2}>
  <InfoCard title="CLI command reference" description="The structured per-command reference — the recommended way to invoke operations." href="/api-reference/sdks/cli/commands" />
  <InfoCard title="Operations" description="The full catalogue of typed operations and their naming conventions." href="/api-reference/operations" />
  <InfoCard title="Errors" description="The problem+json schema and error types returned by every operation." href="/api-reference/errors" />
  <InfoCard title="Authentication" description="Browser sign-in and API keys: how the CLI attaches the right auth header." href="/api-reference/authentication" />
</CardGrid>
