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

# Profiles & configuration

The `geopera` CLI keeps each identity in a named profile inside a single, mode-hardened credentials file, and resolves the API base URL through a short precedence chain that environment variables and flags can override. This page documents exactly where that state lives, how every setting is resolved, and the edge cases worth knowing before you script against it.

Almost everything here is settled by two functions in the CLI's `config` module — `resolve_profile` and `resolve_api_url` — plus the credential store in the `auth` module. The defaults are deliberately boring: install the CLI, run `geopera login`, and you never touch any of it. The knobs below exist for multi-environment work, CI, and debugging.

## The credential store

Credentials live in one JSON file under your XDG config directory:

```bash
~/.config/geopera/credentials.json
```

The directory is created with mode `0700` (owner-only) and the file is written with mode `0600`, so the session tokens and API keys it holds are never group- or world-readable. The CLI re-`chmod`s both the directory and the file on every write, because `os.makedirs(mode=...)` only applies to the leaf directory it creates and is still subject to your `umask` — the explicit `chmod` guarantees the final permissions regardless.

Writes are **atomic**. The store is written to a sibling `credentials.json.tmp`, opened `0600` from the very first byte (via `os.open(..., O_CREAT | O_TRUNC, 0o600)`) so the secret is never momentarily world-readable, then `os.replace`d over the real file. A crash mid-write can therefore never leave a truncated or partially-secret `credentials.json` behind — you either have the old file intact or the new one complete. The temp file is unlinked in a `finally` block, so a failed write does not litter `.tmp` files.

The file is written with `sort_keys=True` and two-space indentation, so profiles appear alphabetically and diffs stay stable. You normally never edit it by hand:

- `geopera login` creates and updates a profile.
- `geopera logout` clears the active profile's credentials (see the [gotcha](#gotchas) about `api_url`).

If the file is missing, unreadable, or contains malformed JSON, the store loader returns an **empty store** rather than raising — so a corrupt file degrades to "not logged in" instead of crashing every command. The fix in that case is simply to log in again, which rewrites the file cleanly.

## Profiles

Every identity is a top-level key in the store, keyed by **profile name**. Each profile entry has exactly two fields: `api_url` and `auth`.

```json
{
	"default": {
		"api_url": "https://api.geopera.com",
		"auth": {
			"type": "oauth",
			"access_token": "...",
			"refresh_token": "...",
			"expires_at": 1750000000,
			"scope": "openid profile",
			"issuer": "https://api.geopera.com"
		}
	},
	"staging": {
		"api_url": "https://staging.api.geopera.com",
		"auth": { "type": "api_key", "api_key": "gpra_..." }
	}
}
```

The `auth` block is one of two shapes:

| `auth.type` | Fields                                                           | Sent as                                |
| ----------- | ---------------------------------------------------------------- | -------------------------------------- |
| `oauth`     | `access_token`, `refresh_token`, `expires_at`, `scope`, `issuer` | `Authorization: Bearer <access_token>` |
| `api_key`   | `api_key`                                                        | `X-API-Key: <api_key>`                 |

A `type: oauth` profile is what a browser sign-in produces: `geopera login` runs the device flow and stores the resulting **session token** (plus its refresh material and expiry). A `type: api_key` profile comes from `geopera login --api-key` and stores a long-lived `gpra_` key verbatim. The CLI picks the auth header automatically per request — `Authorization: Bearer ...` for a session token, `X-API-Key: ...` for an API key. For how these credentials are obtained and how a session token is refreshed near expiry, see [Authentication](/api-reference/authentication) and the [CLI authentication](/api-reference/sdks/cli/authentication) page.

You can log in to as many profiles as you like; each lives independently in the store. Saving a profile rewrites **only** the one profile you touched and leaves the others intact, so logging into `staging` never disturbs `default`.

### Selecting a profile

Profile selection follows this precedence (highest first):

| Precedence  | Source                    | Notes                                  |
| ----------- | ------------------------- | -------------------------------------- |
| 1 (highest) | `--profile <name>` flag   | Per-command. Wins over everything.     |
| 2           | `GEOPERA_PROFILE` env var | Applies to the whole shell session.    |
| 3 (default) | the literal `"default"`   | Used when neither of the above is set. |

```bash
# Use the 'staging' profile for a single command
geopera whoami --profile staging

# Make 'staging' the default for this shell session
export GEOPERA_PROFILE=staging
geopera whoami
```

`--profile` is a shared option accepted by every command that needs credentials (`login`, `logout`, `whoami`, every `geopera <resource> <action>` command, and the raw `op` escape hatch). The resolution rule is the same everywhere: flag, then env var, then `"default"`.

## Resolving the API base URL

The base URL the CLI talks to is resolved **per command** with this precedence (highest first):

| Precedence  | Source                                | Notes                   |
| ----------- | ------------------------------------- | ----------------------- |
| 1 (highest) | `--api-url <url>` flag                | Per-command override.   |
| 2           | `GEOPERA_API_URL` env var             | Whole-session override. |
| 3           | the active profile's stored `api_url` | Captured at login time. |
| 4 (default) | `https://api.geopera.com`             | The production API.     |

The resolved value has any trailing slash stripped, so `https://api.geopera.com/` and `https://api.geopera.com` behave identically and never produce a doubled `//v1/...` path.

```bash
# One-off override against a local backend
geopera catalog search --collections sentinel-2-l2a --limit 10 \
  --api-url http://localhost:8000

# Override for the whole session
export GEOPERA_API_URL=https://staging.api.geopera.com
geopera whoami
```

Because each profile already stores its own `api_url` (captured at login time), you rarely need `--api-url` or `GEOPERA_API_URL` in day-to-day use — switching profiles already switches the endpoint. The override flags are there for pointing an existing identity at a different deployment without re-logging-in, which is most useful when testing a staging or local backend with production-shaped credentials.

## The `GEOPERA_API_TOKEN` escape hatch

For CI and other ephemeral environments where you do not want to write a credentials file at all, set `GEOPERA_API_TOKEN`. When it is present it **bypasses the credential store entirely** — no profile `auth` lookup, no refresh, nothing written to disk:

```bash
export GEOPERA_API_TOKEN="gpra_xxxxxxxxxxxxxxxxxxxxxxxx"
geopera orders list
```

How the value is interpreted depends on its prefix:

- A value beginning with `gpra_` is treated as an **API key** and sent as `X-API-Key`.
- Any other value is treated as an **opaque bearer token** (a session token) and sent as `Authorization: Bearer <value>`.

The opaque-bearer mode lets you inject, for example, a session token minted by your CI provider without the CLI trying to refresh it. In this mode `expires_at` is set to `null`, so **no proactive refresh is ever attempted** — the token is used exactly as given, and a 401 surfaces as an error rather than triggering a refresh-and-retry. (Refresh-on-401 only ever runs for a stored `oauth` profile that actually carries a `refresh_token`.)

`--api-url` / `GEOPERA_API_URL` still apply on top of the env-token path, and the active profile's stored `api_url` is still consulted as the default. That means even when the token comes from the environment, the endpoint is resolved with the full precedence chain above, so you can point an env-token command at any deployment:

```bash
GEOPERA_API_TOKEN="$CI_GEOPERA_TOKEN" \
  geopera orders list --api-url https://staging.api.geopera.com
```

Note the asymmetry: `GEOPERA_API_TOKEN` short-circuits the **auth** lookup but does **not** short-circuit URL resolution. The profile is still resolved (so the profile name shown by `whoami` reflects `--profile`/`GEOPERA_PROFILE`), and its stored `api_url` is still used as the level-3 default — only the profile's `auth` block is ignored in favour of the env token.

## Environment variables

| Variable            | Purpose                                                         | Precedence note                                                  |
| ------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- |
| `GEOPERA_PROFILE`   | Active profile name                                             | Below `--profile`, above the `"default"` fallback                |
| `GEOPERA_API_URL`   | API base URL                                                    | Below `--api-url`, above the profile's stored `api_url`          |
| `GEOPERA_API_TOKEN` | Opaque bearer / API key; bypasses the credential store's `auth` | Overrides the entire stored `auth`; URL resolution is unaffected |

There is no env var for the credential file location — the path is fixed at `~/.config/geopera/credentials.json`. If you need an isolated store (for example to keep CI credentials out of your interactive profile), prefer the `GEOPERA_API_TOKEN` escape hatch over relocating the file.

## Worked example: a dedicated CI profile

Suppose you maintain a long-lived `default` profile for interactive work and want CI to run against staging with a scoped API key, without ever writing to the runner's home directory.

Locally, mint and store the staging key under its own profile so you can test the exact path CI will use:

```bash
geopera login --api-key gpra_xxxxxxxxxxxxxxxxxxxxxxxx \
  --api-url https://staging.api.geopera.com \
  --profile staging

geopera whoami --profile staging
```

```bash
sub:             svc-ci@example.org
principal_type:  service_account
org_id:          org_01H...
scope:           orders:read orders:write
api_url:         https://staging.api.geopera.com
profile:         staging
```

In CI, do **not** log in at all — hand the key in through the environment so nothing is persisted on the runner:

```yaml
# .github/workflows/smoke.yml (excerpt)
env:
  GEOPERA_API_TOKEN: ${{ secrets.GEOPERA_STAGING_KEY }}
  GEOPERA_API_URL: https://staging.api.geopera.com
steps:
  - run: geopera orders list
```

Because `GEOPERA_API_TOKEN` starts with `gpra_`, the CLI sends it as an API key and the credential store is never read or written. If you would rather pin the endpoint per command than via `GEOPERA_API_URL`, drop the env var and add `--api-url https://staging.api.geopera.com` to each invocation — the precedence is identical, the flag simply wins one level higher.

## Inspecting the resolved configuration

`geopera whoami` is the quickest way to confirm which identity, endpoint, and profile a command will actually use — it prints the resolved `api_url` and `profile` alongside the principal:

```bash
geopera whoami --profile staging --api-url https://staging.api.geopera.com
```

```bash
sub:             svc-ci@example.org
principal_type:  service_account
org_id:          org_01H...
scope:           orders:read orders:write
api_url:         https://staging.api.geopera.com
profile:         staging
```

For the machine-readable form, add `--json` to get the raw `userinfo` payload (see the [gotcha](#gotchas) about what `--json` is bound to internally):

```bash
geopera whoami --json
```

## Gotchas

- **There is no `geopera --version`.** The CLI does not register a `--version` flag. To check what you have installed, query the package directly — for example `pip show geopera` — since the CLI is a thin shell over the published `geopera` SDK.
- **`logout` keeps `api_url`.** `geopera logout` removes only the `auth` block from the active profile; the profile's stored `api_url` is preserved. A subsequent `geopera login` against that profile therefore defaults back to the same endpoint. If `logout` finds no stored `auth`, it reports `No stored credentials for profile '<name>'.` and changes nothing.
- **`--json` is internally the `raw` parameter.** Commands such as `whoami` accept a `--json` flag that switches output from the human-readable summary to raw JSON. In the CLI source this option is bound to a parameter named `raw`, so any error message or shell-completion entry referring to `raw` is talking about `--json`. (This is unrelated to the structured commands' `--json` body flag, which carries an operation's request payload.)
- **`GEOPERA_API_TOKEN` overrides auth but not the URL.** Setting it ignores the stored `auth` for the resolved profile, but the profile is still resolved and its `api_url` is still used as the level-3 default. Set `GEOPERA_API_URL` or `--api-url` if you also need to change the endpoint.
- **A non-`gpra_` API key warns but still works.** `geopera login --api-key` prints a note to stderr if the key does not start with `gpra_`, then validates and stores it anyway. New Geopera keys are expected to use the `gpra_` prefix.
- **A corrupt store degrades gracefully.** If `credentials.json` is unreadable or contains invalid JSON, the loader treats it as empty rather than erroring — every command then behaves as "not logged in" until you `geopera login` again, which rewrites the file.
- **Trailing slashes are stripped.** Whatever endpoint you supply via `--api-url`, `GEOPERA_API_URL`, or a stored `api_url`, the resolved value has its trailing `/` removed, so `https://api.geopera.com/` and `https://api.geopera.com` are equivalent.

## See also

- [CLI authentication](/api-reference/sdks/cli/authentication) — `login`, `logout`, `whoami`, the device flow, and API keys
- [Authentication](/api-reference/authentication) — session tokens, `gpra_` API keys, and how the API validates them
- [Scopes](/api-reference/scopes) — what a credential is allowed to do
- [Operations](/api-reference/operations) — the `POST /v1/op/{operation_id}` model the CLI dispatches to
- [Errors](/api-reference/errors) — the problem+json shape the CLI surfaces on failure
