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

# Client reference

The `@geopera/sdk` package exposes a single fully typed client whose primary surface is a tree of resource namespaces — `client.catalog.search(body)`, `client.orders.archive.place(body)` — with `invoke` and `callOperation` underneath as the low-level escape hatch.

Every capability on the Geopera platform is an **operation** reachable at `POST /v1/op/{operation_id}`. There are no REST-style routes: no path parameters, no query strings, no `GET`/`PUT`/`PATCH`/`DELETE`. List, get, create, and delete intent live in the operation **name** (`catalog.search`, `orders.archive.get`, `orders.cancel`), and every input travels in the JSON request body. The SDK mirrors that model directly — each namespace method, `invoke`, and `callOperation` issue exactly one `POST` and infer the body and return type from the operation id, so calls are checked at compile time and track the live API by construction.

## Exports

The package's public surface is small and stable. It is built around one fully typed client:

```typescript
import { GeoperaClient, GeoperaError, callOperation, DEFAULT_BASE_URL } from '@geopera/sdk';

import type {
	GeoperaClientOptions,
	Namespaces,
	OperationMethod,
	InvokeOptions,
	OperationId,
	OperationInput,
	OperationOutput,
	operations
} from '@geopera/sdk';
```

| Export                 | Kind     | Purpose                                                        |
| ---------------------- | -------- | -------------------------------------------------------------- |
| `GeoperaClient`        | class    | Reusable, fully-typed client with resource namespaces.         |
| `callOperation`        | function | One-shot functional call (throwaway client).                   |
| `GeoperaError`         | class    | Thrown on any non-2xx response; carries the problem+json body. |
| `DEFAULT_BASE_URL`     | const    | `"https://api.geopera.com"`.                                   |
| `GeoperaClientOptions` | type     | Constructor options.                                           |
| `Namespaces`           | type     | The shape of the resource-namespace tree on a client.          |
| `OperationMethod<K>`   | type     | The signature of a single namespace method (operation `K`).    |
| `InvokeOptions`        | type     | Per-call options — currently `{ signal?: AbortSignal }`.       |
| `OperationId`          | type     | Union of every valid `operation_id`.                           |
| `OperationInput<K>`    | type     | Request body type for operation `K`.                           |
| `OperationOutput<K>`   | type     | Success response type for operation `K`.                       |
| `operations`           | type     | The full operations map keyed by `operation_id`.               |

The four runtime values (`GeoperaClient`, `callOperation`, `GeoperaError`, `DEFAULT_BASE_URL`) are regular ESM exports. Everything else is a `type`, erased at build time — import them with `import type` so your bundler drops them cleanly. There are no default exports, no namespaces with side effects, and no globals: importing the package does nothing until you construct a client or call `callOperation`.

`GeoperaError` is covered in detail on the [errors](/api-reference/errors) page. The [operations catalogue](/api-reference/sdks/typescript/operations) lists every `OperationId` and its body and response shapes.

## DEFAULT_BASE_URL

```typescript
export const DEFAULT_BASE_URL = 'https://api.geopera.com';
```

This is the production base URL used when you do not pass `baseUrl`. It is exported so you can reference it directly — for example, to assert in a test that a client points at production, or to build the same URL elsewhere in your code without hardcoding the string.

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

const client = new GeoperaClient({ token: process.env.GEOPERA_TOKEN! });
const isProd = client.baseUrl === DEFAULT_BASE_URL; // true unless baseUrl was overridden
```

## GeoperaClient

The primary entry point. Construct one client and reuse it for the lifetime of your process — it holds your token, headers, and `fetch` implementation, and there is no connection state to leak or pool to drain. Constructing a client is cheap (no I/O, no network), but reusing one keeps your token validation and header set in a single place.

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

const client = new GeoperaClient({ token: process.env.GEOPERA_TOKEN! });

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

### Namespaces

Every operation is reachable as a method on a resource namespace derived from its id. The dotted operation id becomes a property path: `catalog.search` is `client.catalog.search`, and deeply nested operations nest the same way — `orders.archive.place` is `client.orders.archive.place`, `orders.tasking.templates.save` is `client.orders.tasking.templates.save`.

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

// orders.archive.place — deep nesting
const order = await client.orders.archive.place({
	// ...order body
});
```

#### How the namespace tree is built

The namespace surface is not a hand-written object — it is a `Proxy`. Reading an unknown string property off the client descends one path segment and returns another callable proxy node; **calling** a node invokes the operation at the accumulated dotted path. So `client.orders` is a node for the `orders` prefix, `client.orders.archive` extends it, and `client.orders.archive.place(body)` is exactly `client.invoke("orders.archive.place", body)`.

A few consequences are worth knowing:

- **Known members pass through.** Real instance members — `invoke`, `baseUrl` — and any `symbol` key are returned as-is, never shadowed by a namespace node. Only unknown string properties descend into the tree.
- **The client is not a thenable.** The proxy explicitly passes `then` through (it is not present on the instance, so it reads as `undefined`), which means `await client` does not hang or resolve to a half-built node. You only ever `await` the promise returned by a leaf call.
- **Operation ids form a collision-free tree.** No operation id is a strict prefix of another, so a node is never simultaneously a callable operation and a parent group. You will not find an `orders.archive` that is both callable and has `.place` underneath.
- **Typos surface in the types, not at runtime.** Because the tree is a proxy, `client.catalog.serach` (misspelled) is a `Namespaces`-typed access that TypeScript rejects at compile time. If you reach it through an `as any` cast, the misspelled path is sent to the API verbatim and comes back as a `GeoperaError` (an unknown operation), not a thrown `TypeError`.

#### Method signature

Each leaf method has the signature:

```typescript
type OperationMethod<K extends OperationId> = (
	body: OperationInput<K>,
	options?: InvokeOptions
) => Promise<OperationOutput<K>>;
```

The `body` is type-checked against that operation's input model and the resolved promise is typed as its output model — both are inferred, so you get completion and errors as you type. Every namespace method delegates to `invoke`, so they share its exact request, error, and cancellation behaviour.

When an operation's input model is **fully optional** (every field optional, i.e. the input type accepts `{}`), `OperationMethod` makes the `body` argument itself optional, so you can call it with no arguments:

```typescript
// Operation whose input model is fully optional — body may be omitted.
const me = await client.account.whoami();

// Equivalent, passing an explicit empty body.
const meExplicit = await client.account.whoami({});
```

For operations that require fields, the `body` argument is required and TypeScript will flag a missing or incomplete body.

#### Cancellation and timeouts

The second argument is `InvokeOptions`, currently an optional `AbortSignal` for cancellation and timeouts. The SDK does not impose its own timeout, so if you want one, wire an `AbortController` to a timer:

```typescript
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 30_000);
try {
	const out = await client.catalog.search(
		{ collections: ['sentinel-2-l2a'], limit: 50 },
		{ signal: controller.signal }
	);
	// use out
} finally {
	clearTimeout(timer);
}
```

When the signal aborts, the underlying `fetch` rejects with an `AbortError` (`DOMException` with `name === "AbortError"`), **not** a `GeoperaError` — no response ever arrived. Distinguish the two when you catch:

```typescript
try {
	await client.catalog.search({ collections: ['sentinel-2-l2a'] }, { signal });
} catch (err) {
	if (err instanceof GeoperaError) {
		// The server answered with a non-2xx status.
	} else if (err instanceof Error && err.name === 'AbortError') {
		// The request was cancelled / timed out before a response.
	} else {
		// Network failure, DNS, TLS, etc. — fetch rejected.
		throw err;
	}
}
```

You can reuse one `AbortSignal` across several concurrent calls to cancel them as a group, or pass `AbortSignal.timeout(30_000)` on runtimes that support it for a one-liner timeout without managing a controller.

The full namespace tree is typed as `Namespaces`; you rarely name it directly, since the methods are reached through the client instance.

### Constructor

```typescript
new GeoperaClient(opts: GeoperaClientOptions)
```

The constructor validates its inputs eagerly and **throws synchronously** in two cases:

- **Missing token** — if `opts` is falsy or `opts.token` is empty (`""`, `undefined`, `null`):
  `GeoperaClient: \`token\` is required (a Geopera API key or session token).`
- **No `fetch` available** — if neither `opts.fetch` nor `globalThis.fetch` exists (for example Node below 18 without a polyfill):
  `GeoperaClient: no global \`fetch\` available — pass \`fetch\` (Node below 18).`

Because these throw at construction rather than on the first request, a misconfigured client fails fast and loudly — there is no deferred error to chase later. The token is not validated against the API at construction (that would require a network round trip); an invalid or revoked key surfaces as a `GeoperaError` with a `401` status on the first call. See [authentication](/api-reference/authentication).

On success it normalizes `baseUrl` by stripping trailing slashes, so `https://api.geopera.com/` and `https://api.geopera.com` behave identically.

### GeoperaClientOptions

```typescript
interface GeoperaClientOptions {
	/** A Geopera API key (`gpra_...`) or a session token. */
	token: string;
	/** API base URL. Defaults to `https://api.geopera.com`. */
	baseUrl?: string;
	/** Extra headers sent on every request. */
	headers?: Record<string, string>;
	/** Custom fetch (test stub, undici, etc.). Defaults to the global `fetch`. */
	fetch?: typeof fetch;
}
```

| Field     | Type                     | Default            | Notes                                                                                                                                                                                                 |
| --------- | ------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`   | `string`                 | — (required)       | Either a minted API key with the `gpra_` prefix or a session token (obtained by signing in to Geopera). Sent as `Authorization: Bearer <token>`. See [authentication](/api-reference/authentication). |
| `baseUrl` | `string`                 | `DEFAULT_BASE_URL` | Trailing slashes are stripped. Point this at a staging host when needed.                                                                                                                              |
| `headers` | `Record<string, string>` | `{}`               | Merged into every request. Useful for tracing headers.                                                                                                                                                |
| `fetch`   | `typeof fetch`           | `globalThis.fetch` | Inject a stub for tests or `undici` on older runtimes.                                                                                                                                                |

The API key **is** the bearer token: there is no token exchange, sign-in handshake, or refresh step for keys. A browser sign-in instead yields a short-lived **session token**, which you pass in exactly the same `token` slot. Both go out as `Authorization: Bearer <token>`.

**Header precedence.** The `headers` you supply are spread **after** the built-in headers (`Authorization`, `Content-Type`, `Accept`) on each request, so a key you provide overrides the SDK default for that name. This is deliberate — it lets you replace `Content-Type` or `Accept` when you must — but it also means a stray `Authorization` in `headers` silently replaces the bearer token. Reserve `headers` for additive concerns such as `X-Request-Id` or `X-Request-Source`:

```typescript
const client = new GeoperaClient({
	token: process.env.GEOPERA_TOKEN!,
	headers: {
		'X-Request-Source': 'ingest-worker',
		'X-Request-Id': crypto.randomUUID()
	}
});
```

Because `headers` is captured once at construction, every request from that client carries the same set. For a per-request header (such as a unique request id), use one client per logical unit of work, or fall back to `callOperation`, whose `headers` apply only to that single call.

**Custom `fetch`.** The injected `fetch` must match the global `fetch` signature. Use it to:

- run on Node below 18, by passing `undici`'s `fetch`;
- stub the network in unit tests, returning canned `Response` objects;
- thread requests through a proxy or add instrumentation by wrapping the global.

```typescript
// Test stub: assert the URL/headers and return a canned response.
const client = new GeoperaClient({
	token: 'gpra_test',
	fetch: async (url, init) => {
		expect(String(url)).toBe('https://api.geopera.com/v1/op/catalog.search');
		return new Response(JSON.stringify({ features: [] }), {
			status: 200,
			headers: { 'content-type': 'application/json' }
		});
	}
});
```

### baseUrl

```typescript
readonly baseUrl: string;
```

The resolved, slash-normalized base URL. It is read-only — set it through the constructor, not after construction. Every request is built as `POST {baseUrl}/v1/op/{operation_id}`, so overriding `baseUrl` is how you point a client at a staging or self-hosted environment while keeping every operation id and body identical.

### invoke

`invoke` is the low-level method that every namespace method calls. Reach for it directly when you need a **dynamic** operation id (one chosen at runtime), or any operation that the namespace tree does not yet surface.

```typescript
invoke<K extends OperationId>(
  op: K,
  body: OperationInput<K>,
  options?: InvokeOptions,
): Promise<OperationOutput<K>>
```

The generic `K` is pinned to the literal `op` you pass, so `body` is type-checked against that operation's input model and the resolved promise is typed as its output model. The equivalent of `client.catalog.search(body)` is:

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

Mechanically, `invoke`:

1. issues `POST {baseUrl}/v1/op/{op}` with headers `Authorization: Bearer <token>`, `Content-Type: application/json`, `Accept: application/json`, followed by your constructor `headers` (which therefore win on conflict);
2. serializes the request with `JSON.stringify(body ?? {})`, so a nullish body becomes `{}` on the wire;
3. reads the full response as text, then parses it as JSON when the text is non-empty (an empty `202`/`204`-style body resolves to `undefined`);
4. on a non-2xx status, throws `GeoperaError` carrying the status, the parsed problem+json body, and the operation id;
5. otherwise returns the parsed body typed as `OperationOutput<K>`.

The third argument is `InvokeOptions`, carrying an optional `AbortSignal` for cancellation and timeouts — identical to the namespace methods:

```typescript
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 30_000);
try {
	const out = await client.invoke(
		'catalog.search',
		{ collections: ['sentinel-2-l2a'], limit: 50 },
		{ signal: controller.signal }
	);
	// use out
} finally {
	clearTimeout(timer);
}
```

**Dynamic ids stay type-safe.** Because `op` is a typed `OperationId`, even a runtime-chosen id is checked against the union — but the input and output collapse to the union of all operations unless you narrow `K`. Narrow it for precise typing:

```typescript
// `op` is known statically through a generic — body/result stay precise.
function run<K extends OperationId>(client: GeoperaClient, op: K, body: OperationInput<K>) {
	return client.invoke(op, body);
}
```

There is no GET/PUT/PATCH/DELETE surface and no path or query parameters: list, get, and delete intent live in the operation **name**, and all inputs travel in the JSON body. See [concepts](/api-reference/concepts) for the operation model and [operations](/api-reference/operations) for the naming conventions.

## callOperation

A functional one-shot equivalent of `invoke` for scripts and single calls. Like `invoke`, it is a low-level entry point — prefer the namespace methods when you have a long-lived client.

```typescript
function callOperation<K extends OperationId>(
	op: K,
	body: OperationInput<K>,
	token: string,
	options?: {
		baseUrl?: string;
		headers?: Record<string, string>;
		fetch?: typeof fetch;
		signal?: AbortSignal;
	}
): Promise<OperationOutput<K>>;
```

```typescript
import { callOperation } from '@geopera/sdk';

const results = await callOperation(
	'catalog.search',
	{ collections: ['sentinel-2-l2a'], limit: 10 },
	process.env.GEOPERA_TOKEN!
);
```

`callOperation` constructs a fresh `GeoperaClient` on **every** call: it splits `signal` out of `options` into the per-call `invoke` options and forwards the rest (`baseUrl`, `headers`, `fetch`) to the constructor alongside `token`. That means each call re-validates the token, re-resolves `fetch`, and applies its own `headers` — convenient for a single request, but the per-call client allocation makes it the wrong tool for a loop.

The shape mirrors the constructor options minus `token` (passed positionally) plus `signal` (which belongs to the call, not the client). Anything you would set on `GeoperaClientOptions` you can set here per call.

> **Reuse a `GeoperaClient` for many calls.** When you make more than one request, build one client and call its namespace methods repeatedly instead of calling `callOperation` in a loop. Same token validation, same `fetch`, no per-call construction.

```typescript
// Prefer this for repeated calls.
const client = new GeoperaClient({ token: process.env.GEOPERA_TOKEN! });
for (const collection of collections) {
	await client.catalog.search({ collections: [collection], limit: 10 });
}
```

## Exported types

The SDK is fully typed, so the types track the live API by construction. They are derived from the generated `operations` map, which means adding or changing an operation upstream flows into `OperationId`, `OperationInput`, `OperationOutput`, and the namespace tree with no hand-maintenance.

### Namespaces

```typescript
type Namespaces;
```

The shape of the resource-namespace tree exposed on a `GeoperaClient` instance — `catalog`, `orders`, and so on, each carrying its operation methods (and nested namespaces for deep ids like `orders.archive.place`). It is built recursively from `OperationId`: each dotted id is split into path segments, and every leaf is an `OperationMethod` for that id. The client instance is declared to extend `Namespaces`, so you usually access methods through `client.<resource>.<action>` rather than naming `Namespaces` directly.

### OperationMethod

```typescript
type OperationMethod<K extends OperationId> =
	Record<string, never> extends OperationInput<K>
		? (body?: OperationInput<K>, options?: InvokeOptions) => Promise<OperationOutput<K>>
		: (body: OperationInput<K>, options?: InvokeOptions) => Promise<OperationOutput<K>>;
```

The signature of a single namespace method. Every leaf in the `Namespaces` tree is an `OperationMethod` for its operation id, so `client.catalog.search` is exactly `OperationMethod<"catalog.search">`.

The conditional is the part worth noting: when the operation's input model is fully optional — i.e. `Record<string, never>` (an empty `{}`) is assignable to it — the `body` argument is itself optional and the method can be called with no arguments. For every other operation the `body` is required.

### InvokeOptions

```typescript
interface InvokeOptions {
	/** Cancel the request / enforce a timeout. */
	signal?: AbortSignal;
}
```

The per-call options accepted by namespace methods and by `invoke`. It currently carries only an optional `AbortSignal`. It is a distinct, named type so you can build helpers that thread call options through without restating the shape.

### operations

```typescript
type operations;
```

The full map of every operation, keyed by `operation_id`, generated from the API surface. Each entry describes the operation's `requestBody` and `responses`. You rarely use it directly; `OperationId`, `OperationInput`, and `OperationOutput` are derived from it and are the types you reach for.

### OperationId

```typescript
type OperationId = keyof operations;
```

The union of every valid operation id (for example `"catalog.search"`, `"orders.archive.place"`). Passing any other string to `invoke` or `callOperation` is a compile-time error, and reading an unknown property off the namespace tree is similarly rejected. Use it to constrain your own helpers to real operations.

### OperationInput

```typescript
type OperationInput<K extends OperationId> = operations[K] extends {
	requestBody: { content: { 'application/json': infer B } };
}
	? B
	: Record<string, never>;
```

The request body type for operation `K`, resolved from its `application/json` request body. Operations that take no body resolve to `Record<string, never>` (i.e. `{}`) — which is exactly the condition that makes `OperationMethod`'s `body` argument optional.

### OperationOutput

```typescript
type OperationOutput<K extends OperationId> = operations[K] extends {
	responses: infer R;
}
	? R extends { 200: { content: { 'application/json': infer O } } }
		? O
		: R extends { 201: { content: { 'application/json': infer O } } }
			? O
			: R extends { 202: { content: { 'application/json': infer O } } }
				? O
				: unknown
	: unknown;
```

The success body type for operation `K`. Operations answer `200`, `201`, or `202` depending on the route (creation and money routes typically answer `201`/`202`), and `OperationOutput` resolves whichever 2xx the operation declares, preferring `200`, then `201`, then `202`, and falling back to `unknown` if none is typed.

You can name these types in your own signatures without restating the shapes:

```typescript
import type { OperationInput, OperationOutput } from '@geopera/sdk';

type SearchBody = OperationInput<'catalog.search'>;
type SearchResult = OperationOutput<'catalog.search'>;

async function search(client: GeoperaClient, body: SearchBody): Promise<SearchResult> {
	return client.catalog.search(body);
}
```

## Worked example

A small CLI-style script that constructs one client, runs a search with a timeout, and surfaces problem+json errors and cancellations cleanly:

```typescript
import { GeoperaClient, GeoperaError } from '@geopera/sdk';
import type { OperationOutput } from '@geopera/sdk';

async function main() {
	const token = process.env.GEOPERA_TOKEN;
	if (!token) {
		throw new Error('Set GEOPERA_TOKEN to a gpra_ key or a session token.');
	}

	// One client, reused for every call.
	const client = new GeoperaClient({
		token,
		headers: { 'X-Request-Source': 'search-script' }
	});

	const controller = new AbortController();
	const timer = setTimeout(() => controller.abort(), 30_000);

	try {
		const results: OperationOutput<'catalog.search'> = await client.catalog.search(
			{ collections: ['sentinel-2-l2a'], limit: 10 },
			{ signal: controller.signal }
		);
		console.log(JSON.stringify(results, null, 2));
	} catch (err) {
		if (err instanceof GeoperaError) {
			// RFC 9457 problem+json is preserved on err.problem.
			console.error(`Operation '${err.operation}' failed (HTTP ${err.status})`);
			console.error(err.problem);
			process.exitCode = 1;
			return;
		}
		if (err instanceof Error && err.name === 'AbortError') {
			console.error('Search timed out after 30s.');
			process.exitCode = 1;
			return;
		}
		throw err;
	} finally {
		clearTimeout(timer);
	}
}

main();
```

## Related

- [Authentication](/api-reference/authentication) — `gpra_` API keys and session tokens.
- [Scopes](/api-reference/scopes) — what a token is allowed to invoke.
- [Errors](/api-reference/errors) — the problem+json body carried by `GeoperaError`.
- [Concepts](/api-reference/concepts) — the operation model behind `POST /v1/op/{operation_id}`.
- [Operations](/api-reference/operations) — the operation naming model.
- [Operations catalogue](/api-reference/sdks/typescript/operations) — every `OperationId` and its types.
