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.
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.
| Credential | Looks like | Best for | How you get it |
|---|---|---|---|
| Session token | eyJhbGciOiJ... | Signed-in users, browser sessions, short-lived service tokens | Signing in to Geopera |
| API key | gpra_... | Headless servers, automation, CI, SDK and CLI usage | api_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.
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | — | Required. 1–200 chars. A human label. |
permissions | string[] | ["read"] | Any of read, write, process. |
rate_limit | integer | org default | Optional per-key requests/hour cap. Must be > 0. |
expires_in_days | integer | never | Optional. Omit for a key that never expires. Must be > 0. |
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 key — this is the only time you will ever see it:
{
"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
prefixis a short, non-secret fragment used only to identify the key inapi_keys.listand audit logs. The full secret inkeyis 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.
curl -X POST https://api.geopera.com/v1/op/api_keys.list \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{}'[
{
"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.
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" }'{ "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:
X-API-Key: gpra_...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:
| Kind | Used by | Authority |
|---|---|---|
user | A signed-in user | The self-serve scope set for the user’s organization |
api_key | An organization API key (gpra_...) | The key’s read / write / process permissions, mapped onto scopes |
worker | Processing workers | A narrow, job-bound scope — register their own job’s outputs and nothing else |
service | Internal / system callers | Full 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.
# 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:
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:
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 includesWWW-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 neededread/write/processpermission).429— the per-key rate limit was exceeded.
Gotchas
- The secret is shown once.
api_keys.createreturnskeyexactly once;api_keys.listnever does. Store it immediately in your secret manager. - Revoke by
id, not by secret.api_keys.revoketakes the key’sid(a UUID), which you get fromapi_keys.createorapi_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 — includewriteand/orprocessat creation time, or mint a new key. - Prefer
Authorization: Bearer. UseX-API-Keyor?key=only for tools that cannot send custom headers, and never log a URL that contains?key=.
Next steps
- Scopes — the
resource:actionmodel and wildcard forms that govern what a principal can call. - Rate limits — per-key limits,
429responses, and the rate-limit headers. - Errors — the
application/problem+jsonshape returned on failure.