TypeScript SDK Authentication
The @geopera/sdk client takes a single credential — the token option — which accepts either a gpra_ API key or a short-lived session token, and sends it as an Authorization: Bearer header on every request; there is no environment fallback, no token exchange, and no automatic refresh.
The recommended setup
Construct one GeoperaClient with your token and reuse it. Every capability is reached through the typed client.<resource>.<action>(body) surface, and the same Authorization header is attached to each call automatically.
import { GeoperaClient } from '@geopera/sdk';
const client = new GeoperaClient({ token: 'gpra_...' });
const results = await client.catalog.search({
collections: ['sentinel-2-l2a'],
limit: 10
});The constructor is the only place a credential enters the SDK. Once the client exists, you never pass the token again — client.catalog.search(...), client.orders.place(...), and every other operation inherit it.
The token option
GeoperaClient has exactly one required constructor option: token. There is no separate apiKey field and no separate bearer field. The same option accepts both credential kinds because both are presented to the API identically — as Authorization: Bearer <token>.
import { GeoperaClient } from '@geopera/sdk';
// A gpra_ API key.
const client = new GeoperaClient({ token: 'gpra_live_key_value' });
// A session token (obtained by signing a user in to Geopera).
const sessionClient = new GeoperaClient({ token: sessionToken });A token is one of:
| Credential | Looks like | Where it comes from | Lifetime |
|---|---|---|---|
| Geopera API key | gpra_... | Minted for your account; carries a fixed scope set | Valid until revoked |
| Session token | a signed token | Yielded by a browser sign-in, used the same way as an API key | Short-lived; expires per the sign-in |
The client does not inspect or parse the value — it forwards it verbatim in the bearer header. The API key is the bearer token: there is no token-exchange, OIDC, or password-grant step. A browser sign-in simply yields a session token that you hand to the same token option. For how the platform validates each credential type and resolves its scopes, see Authentication.
If token is missing or empty, the constructor throws synchronously, before any request is made:
new GeoperaClient({ token: '' });
// Error: GeoperaClient: `token` is required (a Geopera API key or session token).All constructor options
GeoperaClientOptions has one required field and three optional ones.
| Option | Type | Required | Default | Purpose |
|---|---|---|---|---|
token | string | yes | — | The gpra_ API key or session token sent as the bearer. |
baseUrl | string | no | https://api.geopera.com | Override the API origin (staging, a proxy, a mock). |
headers | Record<string, string> | no | {} | Extra headers merged into every request. |
fetch | typeof fetch | no | the global fetch | Custom fetch (test stub, undici, Node < 18, etc.). |
import { GeoperaClient, DEFAULT_BASE_URL } from '@geopera/sdk';
const client = new GeoperaClient({
token: process.env.GEOPERA_TOKEN!,
baseUrl: DEFAULT_BASE_URL, // exported for reference; this is the default
headers: { 'X-Request-Source': 'ingest-worker' },
fetch: globalThis.fetch
});baseUrl has any trailing slashes stripped, so https://api.geopera.com/ and https://api.geopera.com behave identically. If no fetch is available (older Node without a global fetch) and you do not pass one, the constructor throws:
// On a runtime without a global fetch:
new GeoperaClient({ token: 'gpra_...' });
// Error: GeoperaClient: no global `fetch` available — pass `fetch` (Node < 18).How the token is sent
Each operation is invoked with POST /v1/op/{operation_id} and the token in the Authorization header. Every operation is a POST — there are no GET routes:
POST /v1/op/catalog.search HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_...
Content-Type: application/json
Accept: application/json
{ "collections": ["sentinel-2-l2a"], "limit": 10 }The same header is attached whether the token is an API key or a session token. The client always sets Authorization, Content-Type: application/json, and Accept: application/json; your headers are spread after these.
No environment fallback
The SDK does not read any environment variable. There is no GEOPERA_API_KEY or GEOPERA_TOKEN lookup inside the client — you must pass token explicitly. To source the credential from the environment, read it in your own code and hand it to the constructor:
import { GeoperaClient } from '@geopera/sdk';
const token = process.env.GEOPERA_TOKEN;
if (!token) {
throw new Error('GEOPERA_TOKEN is not set');
}
const client = new GeoperaClient({ token });Keeping the read in your code means the credential is never silently picked up from an unexpected source, and the failure mode (a missing variable) is yours to handle explicitly.
No automatic token refresh
The client holds the token you give it for the lifetime of the instance and never rotates, refreshes, or re-fetches it. There is no refresh logic in the SDK, and there is no refresh-token grant for gpra_ keys — the key itself is the credential.
API keys (gpra_...) remain valid until they are revoked. Session tokens follow whatever lifetime the sign-in flow sets and are short-lived by design. The SDK neither imposes nor assumes any expiry window; it simply forwards whatever you give it.
If your session tokens expire on a schedule you control, refresh them in your own auth layer and construct a fresh client (or build a new one per request) when they roll over:
async function clientForUser(): Promise<GeoperaClient> {
const token = await getFreshSessionToken(); // your refresh logic
return new GeoperaClient({ token });
}Because the constructor is cheap, building a new GeoperaClient per refreshed token — or per request — is a perfectly reasonable pattern.
Passing extra request context
Anything beyond the credential travels as a custom header via the headers option. These headers are merged into every request the client makes, alongside the Authorization, Content-Type, and Accept headers the client sets itself.
const client = new GeoperaClient({
token: 'gpra_...',
headers: {
'X-Request-Source': 'batch-ingest',
'Idempotency-Key': 'a1b2c3d4-...'
}
});A few notes on header behaviour:
- Headers in
headersare applied to every call from that client instance — they are fixed at construction, not per call. For a value that changes between calls (such as a freshIdempotency-Key), build a short-lived client per call or use the one-shot helper below. - Your
headersare spread after the client’s own headers, so a header you supply with one of those names overrides the client’s value. Do not setAuthorizationhere — usetokeninstead. - For request-deduplication semantics of the
Idempotency-Keyheader, see Idempotency.
Cancelling requests
Every operation accepts a per-call AbortSignal, so you can time out or cancel a request without affecting the client’s credentials. The structured surface and invoke both take it as a trailing options argument:
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 5_000);
try {
const out = await client.catalog.search(
{ collections: ['sentinel-2-l2a'], limit: 10 },
{ signal: ctrl.signal }
);
} finally {
clearTimeout(timer);
}One-shot calls with callOperation
If you only need a single call and do not want to keep a client around, callOperation takes the same token (and optional baseUrl, headers, fetch, signal) as positional and option arguments. It constructs a throwaway GeoperaClient internally, so the authentication semantics are identical.
import { callOperation } from '@geopera/sdk';
const result = await callOperation(
'catalog.search',
{ collections: ['sentinel-2-l2a'], limit: 10 },
'gpra_...',
{ headers: { 'X-Request-Source': 'one-off' } }
);The low-level escape hatch: client.invoke
client.invoke(operationId, body, options?) is the dynamic form behind the typed surface — client.catalog.search(body) is exactly client.invoke("catalog.search", body). Reach for invoke when the operation id is computed at runtime; otherwise prefer the structured surface, which infers the body and return types for you.
const out = await client.invoke('catalog.search', {
collections: ['sentinel-2-l2a'],
limit: 10
});The token handling is identical either way — invoke is the same call path the structured surface uses internally, so the Authorization header behaves the same.
Browser caveats
Never ship a gpra_ key to the browser
Use a short-lived session token in the browser
The safest browser pattern is to keep the gpra_ key on your server, sign users in there, and hand each browser only its own short-lived session token:
// Browser: receives a short-lived session token from your own backend.
const token = await fetch('/api/geopera-session').then((r) => r.text());
const client = new GeoperaClient({ token });If you must call Geopera from a trusted server context, keep the key in an environment variable or secret manager and construct the client there — never inline a gpra_ literal into source that ships to clients.
Worked example
A complete, runnable script that authenticates with a token from the environment, attaches a custom header, invokes one operation through the structured surface, and surfaces an authentication failure cleanly:
import { GeoperaClient, GeoperaError } from '@geopera/sdk';
const token = process.env.GEOPERA_TOKEN;
if (!token) {
throw new Error('Set GEOPERA_TOKEN to a gpra_ key or a session token.');
}
const client = new GeoperaClient({
token,
headers: { 'X-Request-Source': 'auth-example' }
});
try {
const out = await client.catalog.search({
collections: ['sentinel-2-l2a'],
limit: 5
});
console.log(out);
} catch (err) {
if (err instanceof GeoperaError) {
// 401 = missing/invalid token; 403 = token lacks the required scope.
console.error(`HTTP ${err.status} on ${err.operation}`, err.problem);
} else {
throw err;
}
}A bad, revoked, or expired token comes back as a GeoperaError with status 401, and a valid token that lacks the operation’s scope comes back as 403. Both carry an RFC 9457 problem+json body in err.problem, and err.operation names the operation id that failed.
Common authentication errors
| Status | Meaning | Fix |
|---|---|---|
401 | Token missing, malformed, revoked, or (session token) expired | Supply a valid token; sign in again to obtain a fresh session token if it expired |
403 | Token is valid but lacks the operation’s required scope | Mint a key / issue a token with the needed scope — see Scopes |
For the structure of these responses and how to read every field, see Errors.
Related
- Authentication — how the platform validates API keys and session tokens
- Scopes — what each credential is allowed to do
- Errors — the problem+json shape returned on
401/403 - Idempotency — safe retries via the
Idempotency-Keyheader