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

# 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.

```typescript
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>`.

```typescript
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](/api-reference/authentication).

If `token` is missing or empty, the constructor throws synchronously, before any request is made:

```typescript
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 &lt; 18, etc.). |

```typescript
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:

```typescript
// 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:

```http
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:

```typescript
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:

```typescript
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.

```typescript
const client = new GeoperaClient({
	token: 'gpra_...',
	headers: {
		'X-Request-Source': 'batch-ingest',
		'Idempotency-Key': 'a1b2c3d4-...'
	}
});
```

A few notes on header behaviour:

- Headers in `headers` are 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 fresh `Idempotency-Key`), build a short-lived client per call or use the one-shot helper below.
- Your `headers` are spread **after** the client's own headers, so a header you supply with one of those names overrides the client's value. Do not set `Authorization` here — use `token` instead.
- For request-deduplication semantics of the `Idempotency-Key` header, see [Idempotency](/api-reference/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:

```typescript
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.

```typescript
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.

```typescript
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

<CardGrid>
  <InfoCard title="Never ship a gpra_ key to the browser">
    A `gpra_` API key carries your account's full scope set and never expires until revoked. Bundling one into client-side code exposes it to anyone who opens dev tools. Treat API keys as server-only secrets.
  </InfoCard>
  <InfoCard title="Use a short-lived session token in the browser">
    For browser code, sign the user in and use the resulting session token. It is scoped to that user and expires on its own, so a leak has a small blast radius compared with a long-lived key.
  </InfoCard>
</CardGrid>

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:

```typescript
// 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:

```typescript
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](/api-reference/scopes) |

For the structure of these responses and how to read every field, see [Errors](/api-reference/errors).

## Related

- [Authentication](/api-reference/authentication) — how the platform validates API keys and session tokens
- [Scopes](/api-reference/scopes) — what each credential is allowed to do
- [Errors](/api-reference/errors) — the problem+json shape returned on `401` / `403`
- [Idempotency](/api-reference/idempotency) — safe retries via the `Idempotency-Key` header
