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

# Authentication

The Python SDK authenticates every request with a single `token` you pass to the `Geopera` client — either a `gpra_` API key or a session token — and the SDK attaches it as a Bearer credential on every call.

Authentication in the SDK is deliberately explicit: you construct `Geopera(token=...)`, and the client sends `Authorization: Bearer <token>` on every request it makes. Geopera accepts both kinds of token interchangeably, and both run the call as the same principal — with its scopes, audit trail, and provenance. There is no token exchange, sign-in handshake, or refresh step inside the SDK: the value you pass is the value sent on the wire. For the underlying credential model — what a `gpra_` key is, how a session token from signing in maps to a principal, and how scopes are evaluated — see [Authentication](/api-reference/authentication) and [Scopes](/api-reference/scopes).

## Authenticate with the Geopera client

The high-level `Geopera` client is the primary, recommended entry point. Import it from the top-level package, construct it with your `token`, and call resource methods — `client.<resource>.<action>(body)` — with the credential applied automatically:

```python
from geopera import Geopera

client = Geopera(token="gpra_...")     # your Geopera API key

res = client.catalog.search({
    "host_name": "earthsearch-aws",
    "collections": ["sentinel-2-l2a"],
    "limit": 10,
})
print(res)
```

The same `Geopera` client works for deeply nested resources — attribute access descends the resource path and the call invokes the operation at that path:

```python
order = client.orders.archive.place({
    # archive order body
})
```

Every method invoked on this client carries the token you supplied. You construct the client once and reuse it across calls and across resources; the underlying HTTP transport (and the `Authorization` header on it) is shared, so there is no benefit to constructing more than one client for the same credential.

### The Geopera constructor

`Geopera` takes three things: the `token`, an optional `base_url`, and any number of extra keyword arguments that are forwarded verbatim to the underlying client.

```python
Geopera(token: str | None = None, base_url: str = "https://api.geopera.com", **client_kwargs)
```

| Argument          | Default                     | Purpose                                                                                                                                                                                                                                                                                                                                    |
| ----------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `token`           | `None`                      | The credential value — a `gpra_` API key or a session token. If omitted, the client is built **unauthenticated** (see below).                                                                                                                                                                                                              |
| `base_url`        | `"https://api.geopera.com"` | The API root. Override only to target a staging endpoint or a proxy in front of the API.                                                                                                                                                                                                                                                   |
| `**client_kwargs` | —                           | Forwarded straight to the underlying client constructor: `headers`, `cookies`, `timeout`, `verify_ssl`, `follow_redirects`, `httpx_args`, `raise_on_unexpected_status`, and the auth-header fields `prefix` / `auth_header_name`. These are documented under [the low-level client](#the-underlying-client-client-vs-authenticatedclient). |

Because the extra keyword arguments pass through unchanged, anything you can configure on the low-level client you can also configure here, in one call:

```python
from geopera import Geopera

client = Geopera(
    token="gpra_...",
    base_url="https://api.geopera.com",
    headers={"X-Request-Source": "ingest-worker"},
    timeout=30.0,
    raise_on_unexpected_status=True,
)
```

When you pass a `token`, `Geopera` builds an `AuthenticatedClient` under the hood. When you pass `token=None` (or omit it), it builds a plain `Client` with no `Authorization` header — useful only for endpoints that accept anonymous access. Every secured operation requires a credential, so for normal use always supply `token`.

## Authenticate with a session token

A `gpra_` API key is the right credential for headless, server-to-server use. If instead you already hold a session token — for example, from a signed-in user in a browser flow — pass it as `token` exactly the same way. The SDK does not inspect or parse the token; it sends whatever you give it as a Bearer credential:

```python
from geopera import Geopera

# session_token comes from your sign-in flow
client = Geopera(token=session_token)

res = client.catalog.search({"host_name": "earthsearch-aws", "limit": 10})
```

The call then runs as that user's principal, with that user's scopes. A `gpra_` key and a session token are interchangeable at the SDK boundary — the only difference is the principal each resolves to on the server and the scopes attached to it.

## The SDK does not read environment variables

This is the most common surprise: **the SDK never auto-discovers a token.** There is no `GEOPERA_API_KEY` lookup, no dotfile, and no implicit configuration file. If you want the token to come from the environment, read it yourself and pass it in:

```python
import os
from geopera import Geopera

token = os.environ["GEOPERA_API_KEY"]   # you choose the variable name
client = Geopera(token=token)
```

Using `os.environ["..."]` (rather than `os.environ.get(...)`) makes a missing variable fail loudly at startup instead of producing a confusing `401` on the first call. Keep secrets out of source control — load them from your environment, a secrets manager, or your platform's injected configuration, and never hard-code a `gpra_` key in committed code.

## Passing the body as a typed model

Each resource method accepts the request body as a plain dict (shown above) or as the operation's typed input model. When you pass a dict, the fluent client converts it to the operation's input model via that model's `from_dict` before the call; passing the model directly skips the conversion and gives you editor completion and validation before the call leaves your process:

```python
from geopera import Geopera
from geopera.models import CatalogSearchInput

client = Geopera(token="gpra_...")

res = client.catalog.search(
    CatalogSearchInput(
        host_name="earthsearch-aws",
        collections=["sentinel-2-l2a"],
        limit=10,
    ),
)
```

If an operation takes no body, you can call the method with no argument; the client supplies an empty body for you. Both forms authenticate identically — the body shape has nothing to do with the credential.

### Detailed responses from the high-level client

By default a high-level call returns the parsed result model (or a parsed error model). Pass `detailed=True` to get the full typed `Response` — status code plus parsed body — without dropping to the low-level surface:

```python
resp = client.catalog.search(
    {"host_name": "earthsearch-aws", "limit": 10},
    detailed=True,
)
print(resp.status_code, resp.parsed)
```

Any extra keyword arguments you pass to a high-level method are forwarded to the operation **only if that operation declares them** — this is how a per-call `x_api_key` reaches an operation that supports it (see [Per-call header override](#per-call-header-override)). Unknown keyword arguments are silently ignored rather than raising, so a typo in a keyword name will not error.

## The underlying client: Client vs AuthenticatedClient

The high-level `Geopera` client is built on one of two generated low-level clients:

- **`AuthenticatedClient`** — used whenever you pass a `token`. It adds the `Authorization` header to every request.
- **`Client`** — used when no token is given. It is identical except it carries no credential.

Both are immutable [attrs](https://www.attrs.org/) classes constructed with the same keyword arguments. These are exactly the `**client_kwargs` that `Geopera` forwards, so the table below applies equally to `Geopera(...)` and to constructing the low-level client directly:

| Field                        | Default                                                    | Purpose                                                                                                                                           |
| ---------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `base_url`                   | (required on the low-level client; defaulted by `Geopera`) | API root that all relative request paths are joined to.                                                                                           |
| `token`                      | (required on `AuthenticatedClient`)                        | The credential value.                                                                                                                             |
| `prefix`                     | `"Bearer"`                                                 | Joined to the token with a space to form the header value.                                                                                        |
| `auth_header_name`           | `"Authorization"`                                          | The header the credential is written to.                                                                                                          |
| `headers`                    | `{}`                                                       | Static headers sent on every request.                                                                                                             |
| `cookies`                    | `{}`                                                       | Static cookies sent on every request.                                                                                                             |
| `timeout`                    | `None` (httpx default)                                     | An `httpx.Timeout`; exceeding it raises `httpx.TimeoutException`.                                                                                 |
| `verify_ssl`                 | `True`                                                     | TLS verification. Keep `True` in production; set `False` only for local testing against a self-signed endpoint.                                   |
| `follow_redirects`           | `False`                                                    | Whether the transport follows HTTP redirects.                                                                                                     |
| `httpx_args`                 | `{}`                                                       | Extra keyword arguments passed straight through to the `httpx.Client` / `httpx.AsyncClient` constructor (proxies, transport, limits, and so on).  |
| `raise_on_unexpected_status` | `False`                                                    | When `True`, raises `errors.UnexpectedStatus` for any status the OpenAPI contract did not document, instead of returning `None`/an unparsed body. |

You rarely need the low-level client just to authenticate — `Geopera(token=...)` covers it — but it is the right tool when you want explicit sync vs async transports, detailed responses on every call, or an operation the high-level surface does not expose.

```python
from geopera import AuthenticatedClient
from geopera.api.operations import catalog_search
from geopera.models import CatalogSearchInput

client = AuthenticatedClient(
    base_url="https://api.geopera.com",
    token="gpra_...",            # gpra_ API key or session token
)

result = catalog_search.sync(
    client=client,
    body=CatalogSearchInput(
        host_name="earthsearch-aws",
        collections=["sentinel-2-l2a"],
        limit=10,
    ),
)
print(result)   # -> CatalogSearchOutput (typed)
```

Each operation module exposes four call styles:

| Function           | Returns                           | Transport |
| ------------------ | --------------------------------- | --------- |
| `sync`             | parsed model (or `None`)          | blocking  |
| `sync_detailed`    | full `Response` (status + parsed) | blocking  |
| `asyncio`          | parsed model (or `None`)          | async     |
| `asyncio_detailed` | full `Response` (status + parsed) | async     |

The `_detailed` variants return the full response object — `status_code` plus `parsed` — instead of just the parsed value, which is what you want for explicit status handling.

### Reusing the authenticated transport: client.client

You do not have to build the low-level client yourself. The high-level client exposes its underlying client as `client.client`, already configured with your token, base URL, and any extra keyword arguments. Drop down to it for a one-off low-level call without re-supplying the credential:

```python
from geopera import Geopera
from geopera.api.operations import catalog_search
from geopera.models import CatalogSearchInput

client = Geopera(token="gpra_...")

# Reuse the same authenticated transport for a low-level call
result = catalog_search.sync(
    client=client.client,
    body=CatalogSearchInput(host_name="earthsearch-aws", limit=10),
)
```

`client.client` is the exact `AuthenticatedClient` instance the fluent surface uses, so high-level and low-level calls share one HTTP connection pool and one `Authorization` header.

## How the header is built

Internally, `AuthenticatedClient` constructs the credential header from `prefix`, `auth_header_name`, and `token`. With the defaults, the SDK emits:

```http
Authorization: Bearer gpra_...
```

The header is built as `f"{prefix} {token}"` when `prefix` is non-empty, or just the bare `token` when `prefix` is an empty string. All three fields are constructor keyword arguments, so you can override them for a gateway or proxy that expects a different scheme. To send the raw token with no `Bearer` prefix, set `prefix` to an empty string:

```python
from geopera import AuthenticatedClient

# Send the token with no prefix: header becomes "Authorization: <token>"
client = AuthenticatedClient(
    base_url="https://api.geopera.com",
    token="gpra_...",
    prefix="",
)
```

To place the credential under a different header entirely, override `auth_header_name`:

```python
client = AuthenticatedClient(
    base_url="https://api.geopera.com",
    token="gpra_...",
    auth_header_name="X-Proxy-Authorization",
)
```

These overrides are for talking to an intermediary in front of the API. When calling `https://api.geopera.com` directly, keep the defaults. The same fields can be passed through `Geopera(token=..., prefix=..., auth_header_name=...)`.

### The header is materialized on first request

The `Authorization` header is written into the transport the first time the underlying `httpx` client is built — that is, on the first request, or when you enter the client as a context manager. Configure `token`, `prefix`, and `auth_header_name` at construction time. Because the client is immutable, there is no "set the token afterwards" path: to change the credential, construct a new client.

## Per-call header override

Low-level operations accept a per-request `x_api_key` argument **where the operation defines that header** — it is not available on every operation. When present, it lets a single call carry a different credential than the client's default `Authorization` header, which is useful for multi-tenant servers that hold one connection but act on behalf of different principals:

```python
from geopera.api.operations import catalog_search
from geopera.models import CatalogSearchInput

result = catalog_search.sync(
    client=client.client,
    body=CatalogSearchInput(host_name="earthsearch-aws", limit=10),
    x_api_key="gpra_tenant_specific_key",
)
```

The same override flows through the high-level client, because extra keyword arguments are forwarded to the operation when it declares them:

```python
res = client.catalog.search(
    {"host_name": "earthsearch-aws", "limit": 10},
    x_api_key="gpra_tenant_specific_key",
)
```

The argument exists only on operations whose contract declares it. If `x_api_key` is not in an operation's signature, that operation authenticates solely via the client's `Authorization` header — change the credential by constructing a new client instead. Passing `x_api_key` to a high-level call that does not declare it is silently ignored, so verify the operation supports it.

## Deriving clients: with_headers, with_cookies, with_timeout

`AuthenticatedClient` (and `Client`) is immutable. The `with_*` helpers return a **new** client that keeps the same token but layers on additional configuration, rather than mutating the original:

```python
client = AuthenticatedClient(base_url="https://api.geopera.com", token="gpra_...")

# A new client with the same auth plus a request-source header
traced = client.with_headers({"X-Request-Source": "my-app"})

# A new client with the same auth plus a cookie
with_cookie = client.with_cookies({"session_hint": "abc"})

# A new client with the same auth and a stricter timeout
import httpx
quick = client.with_timeout(httpx.Timeout(5.0))
```

Each helper preserves the credential — the derived client is still authenticated with the original token. You can also pass static headers, cookies, and a timeout at construction time:

```python
client = AuthenticatedClient(
    base_url="https://api.geopera.com",
    token="gpra_...",
    headers={"X-Request-Source": "my-app"},
    timeout=httpx.Timeout(30.0),
)
```

For full control of the transport — proxies, custom limits, a shared connection pool — pass `httpx_args`, or set an `httpx.Client` you built yourself with `set_httpx_client` / `set_async_httpx_client`. Note that `set_httpx_client` overrides any headers, cookies, and timeout already configured, so attach the auth header yourself if you replace the transport wholesale.

## Async authentication

Authentication is identical for async usage — the same `AuthenticatedClient` works as an async context manager, and the token is attached the first time the async transport is built:

```python
import asyncio
from geopera import AuthenticatedClient
from geopera.api.operations import catalog_search
from geopera.models import CatalogSearchInput

async def main():
    async with AuthenticatedClient(
        base_url="https://api.geopera.com",
        token="gpra_...",
    ) as client:
        result = await catalog_search.asyncio(
            client=client,
            body=CatalogSearchInput(host_name="earthsearch-aws", limit=10),
        )
        print(result)

asyncio.run(main())
```

Using the client as an `async with` context manager ensures the underlying `httpx.AsyncClient` is opened and closed cleanly. The credential is the same `token` you would use synchronously — there is no async-specific auth path.

## Worked example: env token, scoped client, one call

A complete, runnable pattern for a server that reads its key from the environment, tags its requests, and handles an auth failure cleanly:

```python
import os
from geopera import AuthenticatedClient
from geopera.api.operations import catalog_search
from geopera.models import CatalogSearchInput, Problem

client = AuthenticatedClient(
    base_url="https://api.geopera.com",
    token=os.environ["GEOPERA_API_KEY"],     # you load it; the SDK does not
    headers={"X-Request-Source": "ingest-worker"},
)

resp = catalog_search.sync_detailed(
    client=client,
    body=CatalogSearchInput(
        host_name="earthsearch-aws",
        collections=["sentinel-2-l2a"],
        limit=10,
    ),
)

if resp.status_code == 200:
    print("results:", resp.parsed)            # CatalogSearchOutput
elif resp.status_code in (401, 403) and isinstance(resp.parsed, Problem):
    # Bad/expired token or missing scope — surfaced as RFC 9457 problem+json
    print("auth error:", resp.parsed.title, resp.parsed.detail)
```

A `401` means the token was rejected (missing, malformed, or expired); a `403` means the token authenticated but lacks the [scope](/api-reference/scopes) the operation requires. Both arrive as a typed [`Problem`](/api-reference/errors). The same shape is returned whether you used a `gpra_` key or a session token.

## Common gotchas

- **No env auto-load.** Passing nothing for `token` builds an unauthenticated client; it does not fall back to an environment variable. Secured operations will return `401`. Always supply the token explicitly.
- **Don't double-prefix.** Pass the bare token (`gpra_...` or the raw session token). The SDK adds `Bearer ` for you — including `"Bearer "` in the token value will produce `Bearer Bearer ...` and break the header.
- **Use `client.client` to drop down.** The high-level `Geopera` client exposes its configured low-level client as `client.client`, so you can mix high-level and low-level calls without re-supplying the token.
- **Header is set on first request.** The `Authorization` header is materialized when the underlying transport is first built, so configure `token`, `prefix`, and `auth_header_name` at construction time. The client is immutable — to change the credential, build a new client.
- **`x_api_key` is operation-specific.** It is available only on operations whose contract declares it; it is not a universal override, and passing it where it is not declared is silently ignored.
- **`set_httpx_client` wipes auth.** Replacing the transport with your own `httpx.Client` overrides the headers the SDK configured, including `Authorization`. Re-attach the credential yourself if you take this route.

## Related

- [Authentication](/api-reference/authentication) — the credential model: `gpra_` keys, session tokens from signing in, and how a principal is resolved.
- [Scopes](/api-reference/scopes) — what each credential is permitted to do.
- [Errors](/api-reference/errors) — the `problem+json` shape returned for `401`/`403` and other failures.
- [Operations](/api-reference/operations) — how operations are named and invoked.
