Authentication

Every request to the Geopera API carries a single Authorization: Bearer <token> header, and that one slot accepts either a session token (obtained by signing in to Geopera) or a minted API key.

http
Authorization: Bearer <token>

There is no separate auth verb and no per-operation key parameter. Whatever you put in the Bearer slot is resolved to a principal, the principal’s scopes are checked against the operation’s required scope, and the call runs as that identity — with the same audit and provenance whether the token came from a signed-in user or a headless integration. See Concepts for how operations are invoked (POST /v1/op/{operation_id}).

Two kinds of token, one header

The same Authorization: Bearer header accepts two credential shapes. The server disambiguates them automatically — you never tell it which kind you are sending.

CredentialLooks likeBest forHow you get it
Session tokeneyJhbGciOiJ...Signed-in users, browser sessions, short-lived service tokensSigning in to Geopera
API keygpra_...Headless servers, automation, CI, SDK and CLI usageapi_keys.create operation (or the portal)

Because both shapes live in the same header, the Python, TypeScript, and CLI clients work unmodified regardless of which credential you hold.

API keys

An API key is a long-lived secret with the prefix gpra_ (lowercase g-p-r-a-underscore). Keys are SHA-256 hashed at rest — Geopera stores only the hash and a short non-secret prefix, so a key is shown exactly once, at creation time, and can never be retrieved again. If you lose it, mint a new one and revoke the old.

Keys are issued per organization. The key acts as the organization, and its read / write / process permissions map onto the scopes its requests are allowed to use. Each key also carries its own rate limit.

Mint a key — api_keys.create

Creating a key is itself an operation: POST /v1/op/api_keys.create. The caller must already be authenticated (as a user, or with a key that holds api_keys:write) and belong to an organization — the new key inherits that organization.

FieldTypeDefaultNotes
namestringRequired. 1–200 chars. A human label.
permissionsstring[]["read"]Any of read, write, process.
rate_limitintegerorg defaultOptional per-key requests/hour cap. Must be > 0.
expires_in_daysintegerneverOptional. Omit for a key that never expires. Must be > 0.
bash
curl -X POST https://api.geopera.com/v1/op/api_keys.create \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-pipeline",
    "permissions": ["read", "process"],
    "expires_in_days": 90
  }'

The response returns the raw secret in keythis is the only time you will ever see it:

json
{
	"id": "0b6e6f2a-1d3c-4a9e-9f2b-2c1d8e7a4b10",
	"key": "gpra_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
	"prefix": "gpra_xxx",
	"name": "ci-pipeline",
	"permissions": ["read", "process"],
	"expires_at": "2026-09-18T00:00:00Z",
	"created_at": "2026-06-20T00:00:00Z"
}

Note — The prefix is a short, non-secret fragment used only to identify the key in api_keys.list and audit logs. The full secret in key is never stored in plaintext and never returned again.

List keys — api_keys.list

Lists every key in your organization as metadata only — secrets and hashes are never included. Requires api_keys:read. The input body is empty.

bash
curl -X POST https://api.geopera.com/v1/op/api_keys.list \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
json
[
	{
		"id": "0b6e6f2a-1d3c-4a9e-9f2b-2c1d8e7a4b10",
		"name": "ci-pipeline",
		"prefix": "gpra_xxx",
		"permissions": ["read", "process"],
		"rate_limit": 5000,
		"is_active": true,
		"expires_at": "2026-09-18T00:00:00Z",
		"created_at": "2026-06-20T00:00:00Z",
		"last_used_at": "2026-06-20T11:04:00Z",
		"total_requests": 4821
	}
]

Revoke a key — api_keys.revoke

Revocation is permanent and immediate. Requires api_keys:write. Pass the key’s id (not the secret). A key that is not owned by your organization returns 404.

bash
curl -X POST https://api.geopera.com/v1/op/api_keys.revoke \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "key_id": "0b6e6f2a-1d3c-4a9e-9f2b-2c1d8e7a4b10" }'
json
{ "ok": true, "id": "0b6e6f2a-1d3c-4a9e-9f2b-2c1d8e7a4b10" }

Rotating a key

Geopera has no in-place rotation. Rotate by overlap: mint the replacement with api_keys.create, deploy it, then call api_keys.revoke on the old key once nothing is using it. Listing keys and watching last_used_at and total_requests tells you when the old key is safe to retire.

Session tokens

For human sign-in and short-lived service tokens, sign in to Geopera to obtain a session token. Session tokens are short-lived and refreshed automatically by the SDKs and CLI when you sign in through them.

Put the session token into the Authorization: Bearer slot exactly as you would an API key — the server accepts both shapes in the same header.

Legacy header and query fallbacks

The Authorization: Bearer header is the recommended path for every client. For older geospatial tools (QGIS, ArcGIS) that cannot set arbitrary headers, two fallbacks accept a raw API key:

http
X-API-Key: gpra_...
http
GET /...?key=gpra_...

These exist only for compatibility. Prefer Authorization: Bearer — it is the only form all SDKs, the CLI, and the MCP gateway use, and the only one that also accepts session tokens. The ?key= form puts the secret in the URL, where it can leak into logs and referrers, so reserve it for tools that leave you no choice.

Principals and the resolver

Whatever credential you send, the resolver turns it into a principal. The principal’s kind determines how its scopes are derived and how every action is attributed:

KindUsed byAuthority
userA signed-in userThe self-serve scope set for the user’s organization
api_keyAn organization API key (gpra_...)The key’s read / write / process permissions, mapped onto scopes
workerProcessing workersA narrow, job-bound scope — register their own job’s outputs and nothing else
serviceInternal / system callersFull trust (not customer-facing)

An api_key principal acts as its owning user for write operations (so a headless client can run a full workflow), while the key’s own id is recorded for per-key provenance. Every write is attributed to a principal for audit.

Scopes and rate limits

Authorization is a separate gate from authentication. Each operation declares the one resource:action scope it requires, and the resolved principal must hold it — see Scopes for the full model and the wildcard forms. An API key’s read / write / process permissions are what produce its scopes, so a key with only read cannot call a write operation no matter how the request is shaped.

API keys also carry per-key rate limits: a throttled key receives 429 with the standard rate-limit headers, independent of any other key or user in the organization.

Worked example: mint, use, rotate

Create a read-only key, use it for a catalog search, then rotate it.

bash
# 1. Mint (authenticated as a user or a key with api_keys:write)
NEW=$(curl -s -X POST https://api.geopera.com/v1/op/api_keys.create \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"search-bot","permissions":["read"]}')
KEY=$(echo "$NEW" | python3 -c 'import sys,json;print(json.load(sys.stdin)["key"])')
KEY_ID=$(echo "$NEW" | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')

# 2. Use it — runs as the api_key principal with read scope
curl -s -X POST https://api.geopera.com/v1/op/catalog.search \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"collections":["sentinel-2-l2a"],"limit":10}'

# 3. Rotate: mint a replacement, deploy it, then revoke the old id
curl -s -X POST https://api.geopera.com/v1/op/api_keys.revoke \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{\"key_id\":\"$KEY_ID\"}"

Python

The geopera package’s AuthenticatedClient takes the same token for either credential:

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

# token = a gpra_ API key OR a session token from sign-in
client = AuthenticatedClient(
    base_url="https://api.geopera.com",
    token="gpra_...",
)

result = catalog_search.sync(
    client=client,
    body=CatalogSearchInput(
        host_name="api.geopera.com",
        collections=["sentinel-2-l2a"],
        limit=10,
    ),
)
print(result)

TypeScript

@geopera/sdk’s GeoperaClient likewise accepts either credential as token:

typescript
import { GeoperaClient } from '@geopera/sdk';

// token = a gpra_ API key OR a signed-in user's session token
const geopera = new GeoperaClient({ token: 'gpra_...' });

const results = await geopera.invoke('catalog.search', {
	collections: ['sentinel-2-l2a'],
	limit: 10
});

Errors

Authentication and authorization failures are returned as application/problem+json:

  • 401 — missing, malformed, expired, or revoked token. The response includes WWW-Authenticate: Bearer. A malformed session token and an unknown API key both surface as a single neutral “invalid authentication credentials” message so neither path leaks which it was.
  • 403 — authenticated, but the principal lacks the operation’s required scope (or an API key is missing the needed read / write / process permission).
  • 429 — the per-key rate limit was exceeded.

Gotchas

  • The secret is shown once. api_keys.create returns key exactly once; api_keys.list never does. Store it immediately in your secret manager.
  • Revoke by id, not by secret. api_keys.revoke takes the key’s id (a UUID), which you get from api_keys.create or api_keys.list.
  • Keys are org-scoped. A key can only manage and act within its own organization; revoking a key from another org returns 404.
  • Permissions gate scopes. Granting ["read"] is not enough for write or process operations — include write and/or process at creation time, or mint a new key.
  • Prefer Authorization: Bearer. Use X-API-Key or ?key= only for tools that cannot send custom headers, and never log a URL that contains ?key=.

Next steps

  • Scopes — the resource:action model and wildcard forms that govern what a principal can call.
  • Rate limits — per-key limits, 429 responses, and the rate-limit headers.
  • Errors — the application/problem+json shape returned on failure.