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 and 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)
ArgumentDefaultPurpose
tokenNoneThe 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_kwargsForwarded 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.

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). 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 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:

FieldDefaultPurpose
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.
timeoutNone (httpx default)An httpx.Timeout; exceeding it raises httpx.TimeoutException.
verify_sslTrueTLS verification. Keep True in production; set False only for local testing against a self-signed endpoint.
follow_redirectsFalseWhether 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_statusFalseWhen 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:

FunctionReturnsTransport
syncparsed model (or None)blocking
sync_detailedfull Response (status + parsed)blocking
asyncioparsed model (or None)async
asyncio_detailedfull 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 the operation requires. Both arrive as a typed Problem. 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 — the credential model: gpra_ keys, session tokens from signing in, and how a principal is resolved.
  • Scopes — what each credential is permitted to do.
  • Errors — the problem+json shape returned for 401/403 and other failures.
  • Operations — how operations are named and invoked.