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

# Client Configuration

The recommended Python entry point is `Geopera` — a fluent, namespaced client you construct once with a token and then call as `client.<resource>.<action>(body)`. Every configuration knob is a keyword argument on the constructor, and any keyword you pass beyond `token` and `base_url` is forwarded verbatim to the underlying [`httpx`](https://www.python-httpx.org) client. So configuring the SDK is, in practice, configuring httpx with a few Geopera-specific knobs for auth.

```python
from geopera import Geopera

client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")
result = client.catalog.search({"collections": ["sentinel-2-l2a"], "limit": 10})
```

The `token` is your bearer token — either a `gpra_`-prefixed API key or a session token minted by a browser sign-in. Both are used the same way; there is no token-exchange or refresh step. See [Authentication](/api-reference/authentication) for where to get one. `base_url` defaults to `https://api.geopera.com`, so for the production API you only need a token.

Under the hood `Geopera` wraps one of two immutable [`attrs`](https://www.attrs.org) client classes that the package also exports directly:

- `AuthenticatedClient` — adds a bearer token and the auth-header knobs. This is what `Geopera` builds when you pass a `token`, and it backs every authenticated operation.
- `Client` — the same surface without a token, for the rare unauthenticated request. `Geopera` builds this when `token` is omitted.

Both are the low-level escape hatch: `client.client` on a `Geopera` instance exposes the underlying `AuthenticatedClient` so you can reach its builders and the raw httpx client.

## Constructor options

Pass all options as keyword arguments to `Geopera(...)`. Only `token` is needed for authenticated use; `base_url` defaults to the production API. Everything else is forwarded to the httpx client.

| Option                       | Type                            | Default                     | Purpose                                                                                                                                     |
| ---------------------------- | ------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`                      | `str`                           | `None`                      | Bearer token: a `gpra_` API key or a session token. Omit it to build an unauthenticated `Client`.                                           |
| `base_url`                   | `str`                           | `"https://api.geopera.com"` | Root URL; every request is made relative to it.                                                                                             |
| `prefix`                     | `str`                           | `"Bearer"`                  | Scheme placed before the token in the auth header.                                                                                          |
| `auth_header_name`           | `str`                           | `"Authorization"`           | Header the token is written to.                                                                                                             |
| `timeout`                    | `httpx.Timeout`                 | `None`                      | Per-request timeout. `None` uses httpx's default (5s).                                                                                      |
| `verify_ssl`                 | `str \| bool \| ssl.SSLContext` | `True`                      | TLS verification. Keep `True` in production.                                                                                                |
| `follow_redirects`           | `bool`                          | `False`                     | Whether to follow 3xx redirects.                                                                                                            |
| `headers`                    | `dict[str, str]`                | `{}`                        | Default headers sent with every request.                                                                                                    |
| `cookies`                    | `dict[str, str]`                | `{}`                        | Default cookies sent with every request.                                                                                                    |
| `httpx_args`                 | `dict[str, Any]`                | `{}`                        | Extra kwargs forwarded verbatim to the `httpx.Client` / `httpx.AsyncClient` constructor. The hook for transports, proxies, and pool limits. |
| `raise_on_unexpected_status` | `bool`                          | `False`                     | Raise `errors.UnexpectedStatus` for status codes not in the OpenAPI document.                                                               |

A fully configured client:

```python
import httpx
from geopera import Geopera

client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    base_url="https://api.geopera.com",
    timeout=httpx.Timeout(30.0, connect=5.0),
    verify_ssl=True,
    follow_redirects=False,
    headers={"User-Agent": "acme-pipeline/1.4"},
    raise_on_unexpected_status=True,
)
```

Internally, `Geopera(token=..., base_url=..., **client_kwargs)` constructs `AuthenticatedClient(base_url=base_url, token=token, **client_kwargs)` (or `Client(base_url=base_url, **client_kwargs)` when `token` is `None`). Every knob below is therefore identical whether you set it on `Geopera` or on the raw client — the fluent client just threads the keyword through.

## Base URL

`base_url` is the only host-level setting. Every operation is sent as a `POST` to a path relative to it. Point it at the production API (the default), a regional gateway, or a local mock during testing:

```python
from geopera import Geopera

# Production (default — base_url can be omitted)
client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")

# A gateway in front of the API, or a local mock
client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    base_url="http://localhost:8787",
)
```

## Authentication knobs

The token is written into the auth header lazily, the first time the underlying httpx client is built. The value is computed as:

```python
# prefix + space + token, or just the bare token when prefix is empty
header_value = f"{prefix} {token}" if prefix else token
headers[auth_header_name] = header_value
```

With the defaults (`prefix="Bearer"`, `auth_header_name="Authorization"`) a request carries:

```http
POST /v1/op/catalog.search HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
```

This is what the Geopera API expects, so you rarely change these. They exist for the edge cases:

- **Bare token (no scheme).** Set `prefix=""` to send the raw token with no `Bearer ` prefix.
- **Custom header.** Set `auth_header_name="X-Api-Key"` (or similar) to send the token somewhere other than `Authorization` — useful behind a proxy that reserves `Authorization` for itself.

```python
from geopera import Geopera

client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    prefix="",                     # send the token with no scheme
    auth_header_name="X-Api-Key",  # send it in a custom header
)
```

> The auth header is materialised once, when the httpx client is first constructed. Changing the token, `prefix`, or `auth_header_name` after the first request has no effect on an already-built client — create a new `Geopera` instead.

The same token works whether it is a `gpra_` API key or a session token from a browser sign-in: the API key _is_ the bearer token, so there is no exchange, OIDC, or refresh round-trip. See [Authentication](/api-reference/authentication) for the difference between the two and where to mint each.

## Timeouts

`timeout` accepts an `httpx.Timeout`. When a request exceeds it, the operation raises `httpx.TimeoutException`. Pass a single number for an overall budget, or break it out per phase:

```python
import httpx
from geopera import Geopera

# 30s overall
client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    timeout=httpx.Timeout(30.0),
)

# Fine-grained: 5s to connect, 60s to read (e.g. long-running op responses)
client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    timeout=httpx.Timeout(60.0, connect=5.0),
)
```

Leaving `timeout=None` (the default) defers to httpx's built-in 5-second default — usually too tight for large operation payloads, so set it explicitly. To change the timeout on an already-built client without rebuilding it, use `with_timeout` (see [Builder methods](#builder-methods)).

## TLS verification

`verify_ssl` is forwarded to httpx's `verify`. It accepts:

- `True` (default) — verify against the system trust store. Use this in production.
- `False` — disable verification. Only for local testing against a self-signed dev gateway; never in production.
- a path string to a CA bundle, or a pre-built `ssl.SSLContext` for custom roots or mTLS.

```python
import ssl
from geopera import Geopera

ctx = ssl.create_default_context(cafile="/etc/ssl/corp-root.pem")
client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    base_url="https://gateway.internal",
    verify_ssl=ctx,
)
```

## Redirects

`follow_redirects` is `False` by default. The Geopera API does not rely on redirects for operation calls, so leave this off unless a proxy or load balancer in front of the API issues a redirect you explicitly want the client to chase.

## Default headers and cookies

`headers` and `cookies` are merged into every request. Use `headers` for a custom `User-Agent`, a tracing header, or a tenant selector your gateway reads:

```python
from geopera import Geopera

client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    headers={"User-Agent": "acme-pipeline/1.4", "X-Trace-Source": "nightly-etl"},
    cookies={"route": "eu-west"},
)
```

Do not set `Idempotency-Key` here as a default — it must be unique per write. See [Idempotency](/api-reference/idempotency).

## Builder methods

When you need a variant of an existing client, reach the underlying client through `client.client` and use its builders. Three methods return a _new_ client derived from the current one, leaving the original untouched. If the underlying httpx client has already been built, the new values are also pushed onto it.

| Method                  | Returns    | Effect                             |
| ----------------------- | ---------- | ---------------------------------- |
| `with_headers(headers)` | new client | Merge additional default headers.  |
| `with_cookies(cookies)` | new client | Merge additional default cookies.  |
| `with_timeout(timeout)` | new client | Replace the timeout configuration. |

```python
import httpx
from geopera import Geopera

client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")

# A variant for a slow batch job, with extra tracing — the original is unchanged.
batch_client = (
    client.client
    .with_timeout(httpx.Timeout(120.0))
    .with_headers({"X-Trace-Source": "batch-reprocess"})
)
```

`batch_client` here is a raw `AuthenticatedClient`. You can call operations on it directly through the low-level module surface (see [The low-level escape hatch](#the-low-level-escape-hatch)). For most cases it is simpler to construct a second `Geopera` with the variant settings up front:

```python
import httpx
from geopera import Geopera

base = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")
batch = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    timeout=httpx.Timeout(120.0),
    headers={"X-Trace-Source": "batch-reprocess"},
)
```

The builders matter most when you are already working with the escape-hatch client and want short-lived variants without mutating shared state.

## The underlying httpx client

You can reach (or replace) the httpx client directly via `client.client`. This is the extension point for anything not exposed as a constructor knob — connection pool limits, proxies, transports, and **retries**.

| Method                           | Purpose                                                                          |
| -------------------------------- | -------------------------------------------------------------------------------- |
| `get_httpx_client()`             | Return the sync `httpx.Client`, building it from your config if not yet created. |
| `set_httpx_client(client)`       | Replace the sync client with one you built.                                      |
| `get_async_httpx_client()`       | Return the async `httpx.AsyncClient`, building it on demand.                     |
| `set_async_httpx_client(client)` | Replace the async client.                                                        |

```python
from geopera import Geopera

client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")

# Reach the raw httpx.Client (constructs it if needed)
raw_httpx = client.client.get_httpx_client()
```

> `set_httpx_client` / `set_async_httpx_client` override **all** other settings on the client — including `headers`, `cookies`, `timeout`, and the auth header. If you swap in your own httpx client, you are responsible for setting the `Authorization` header yourself.

```python
import httpx
from geopera import Geopera

client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")

# You own the auth header now — set it yourself.
client.client.set_httpx_client(
    httpx.Client(
        base_url="https://api.geopera.com",
        headers={"Authorization": "Bearer gpra_xxxxxxxxxxxxxxxxxxxxxxxx"},
        timeout=httpx.Timeout(45.0),
    )
)
```

### Context managers

The underlying clients are context managers. Entering opens the connection pool; exiting closes it. Use `with` for sync and `async with` for async so connections are cleaned up:

```python
from geopera import Geopera

client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")

with client.client:
    client.catalog.search({"collections": ["sentinel-2-l2a"], "limit": 10})
# pool closed on exit
```

You cannot enter the same client twice — that is an httpx constraint on the underlying client. For a long-lived service, build one `Geopera` at startup and reuse it; httpx pools and reuses connections across calls.

## Proxies

There is no dedicated proxy knob — route through httpx via `httpx_args`. Pass a single proxy URL with `proxy`, or per-scheme mounts with `mounts`:

```python
import httpx
from geopera import Geopera

# Single proxy for all traffic
client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    httpx_args={"proxy": "http://proxy.internal:8080"},
)

# Per-scheme transports (e.g. only proxy HTTPS)
client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    httpx_args={
        "mounts": {
            "https://": httpx.HTTPTransport(proxy="http://proxy.internal:8080"),
        }
    },
)
```

## Connection pool limits

Tune the connection pool through `httpx_args` with `httpx.Limits` — useful when running many concurrent operations and you want to cap or widen the pool:

```python
import httpx
from geopera import Geopera

client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    httpx_args={
        "limits": httpx.Limits(max_connections=20, max_keepalive_connections=10),
    },
)
```

## Retries

There are **no built-in retries.** A failed request raises immediately. You add retries yourself through `httpx_args` by supplying an `httpx.HTTPTransport` (or `AsyncHTTPTransport`) with a `retries` count — this retries connection-level failures (DNS, connect, broken pipe):

```python
import httpx
from geopera import Geopera

client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    httpx_args={"transport": httpx.HTTPTransport(retries=3)},
)
```

For an async client, pass an `AsyncHTTPTransport`:

```python
import httpx
from geopera import Geopera

client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    httpx_args={"transport": httpx.AsyncHTTPTransport(retries=3)},
)
```

httpx's transport `retries` only covers connection establishment — it does **not** retry on HTTP status codes like `429` or `503`. To retry on responses (with backoff and `Retry-After`), wrap the transport in a custom transport that inspects the response. The example below honours [rate-limit](/api-reference/rate-limits) responses and retries a small set of statuses:

```python
import time
import httpx
from geopera import Geopera


class RetryTransport(httpx.HTTPTransport):
    """Retry idempotent-safe statuses with backoff, honouring Retry-After."""

    RETRY_STATUSES = {429, 502, 503, 504}

    def __init__(self, *args, max_retries: int = 3, **kwargs):
        super().__init__(*args, **kwargs)
        self.max_retries = max_retries

    def handle_request(self, request: httpx.Request) -> httpx.Response:
        attempt = 0
        while True:
            response = super().handle_request(request)
            if response.status_code not in self.RETRY_STATUSES or attempt >= self.max_retries:
                return response
            response.read()
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2.0 ** attempt
            response.close()
            time.sleep(delay)
            attempt += 1


client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    httpx_args={"transport": RetryTransport(retries=3, max_retries=4)},
)
```

Because every operation is invoked as a `POST` ([RPC over HTTP](/api-reference/concepts)), only retry writes that are safe to repeat. Send an `Idempotency-Key` on retried writes so a duplicate delivery is collapsed server-side — see [Idempotency](/api-reference/idempotency).

## Unexpected statuses

`raise_on_unexpected_status` is `False` by default: a response whose status code is not described in the API's OpenAPI document is returned to you as-is for inspection. Set it to `True` to instead raise `errors.UnexpectedStatus` and fail fast in pipelines.

```python
from geopera import Geopera

client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    raise_on_unexpected_status=True,
)
```

This is orthogonal to normal API errors — documented error responses are RFC 9457 `application/problem+json` bodies that the typed operation functions surface for you. See [Errors](/api-reference/errors).

## The low-level escape hatch

`Geopera` is a thin wrapper. When you need the typed `Response` object, the async variants, or the per-operation `sync` / `sync_detailed` / `asyncio` / `asyncio_detailed` functions, drop to the raw `AuthenticatedClient` plus the generated operation modules. Construct the client directly, or reuse the one `Geopera` already built via `client.client`:

```python
import httpx
from geopera import AuthenticatedClient
from geopera.api.operations import catalog_search
from geopera.models import CatalogSearchBody

client = AuthenticatedClient(
    base_url="https://api.geopera.com",
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    timeout=httpx.Timeout(60.0, connect=5.0),
    raise_on_unexpected_status=True,
)

body = CatalogSearchBody.from_dict({"collections": ["sentinel-2-l2a"], "limit": 10})

# Parsed model only
result = catalog_search.sync(client=client, body=body)

# Full typed Response (status code, headers, parsed body)
response = catalog_search.sync_detailed(client=client, body=body)

# Async equivalents
# result = await catalog_search.asyncio(client=client, body=body)
# response = await catalog_search.asyncio_detailed(client=client, body=body)
```

Every configuration knob on `AuthenticatedClient` is exactly the one you would pass to `Geopera` — `Geopera` simply forwards them. The escape-hatch client and a fluent `Geopera` built with the same kwargs behave identically. See the [Python SDK reference](/api-reference/sdks/python/reference) for the full operation surface.

## Worked example

A production-grade client built through the fluent surface: explicit timeouts, connection retries plus status-aware retries, a tracing header, pool limits, and strict failure on unexpected statuses.

```python
import time
import httpx
from geopera import Geopera


class RetryTransport(httpx.HTTPTransport):
    RETRY_STATUSES = {429, 502, 503, 504}

    def __init__(self, *args, max_retries: int = 4, **kwargs):
        super().__init__(*args, **kwargs)
        self.max_retries = max_retries

    def handle_request(self, request: httpx.Request) -> httpx.Response:
        attempt = 0
        while True:
            response = super().handle_request(request)
            if response.status_code not in self.RETRY_STATUSES or attempt >= self.max_retries:
                return response
            response.read()
            retry_after = response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2.0 ** attempt)
            response.close()
            attempt += 1


client = Geopera(
    token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
    base_url="https://api.geopera.com",
    timeout=httpx.Timeout(60.0, connect=5.0),
    verify_ssl=True,
    headers={"User-Agent": "acme-pipeline/2.0"},
    raise_on_unexpected_status=True,
    httpx_args={
        "transport": RetryTransport(retries=3, max_retries=4),
        "limits": httpx.Limits(max_connections=20, max_keepalive_connections=10),
    },
)

with client.client:
    result = client.catalog.search({"collections": ["sentinel-2-l2a"], "limit": 10})
# the pool is closed on exit
```

## Next steps

- [Authentication](/api-reference/authentication) — minting a `gpra_` key versus using a session token.
- [Errors](/api-reference/errors) — the `application/problem+json` shape returned on failures.
- [Rate limits](/api-reference/rate-limits) — `Retry-After` and the `429` you retry against.
- [Idempotency](/api-reference/idempotency) — making retried writes safe.
- [Python SDK reference](/api-reference/sdks/python/reference) — calling operations once the client is configured.
