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

# TypeScript SDK Quickstart

Go from an empty project to your first authenticated, type-checked operation call with `@geopera/sdk` in a couple of minutes: every capability is exposed as a typed resource namespace — you write `client.catalog.search({...})` and `client.orders.archive.place({...})` — with the request body **and** the awaited return type inferred from the method you call, so the whole API surface is checked at compile time and tracks the live API by construction.

## Install

The package is published to npm as [`@geopera/sdk`](https://www.npmjs.com/package/@geopera/sdk). It ships ESM + CJS, is tree-shakeable, ships its own `.d.ts` types, and has **no runtime dependencies** — it is a thin, typed wrapper over `fetch`.

```bash
npm install @geopera/sdk
# or: pnpm add @geopera/sdk
# or: yarn add @geopera/sdk
```

It runs anywhere `fetch` is available: Node 18+, Deno, Bun, edge runtimes, and the browser. On Node versions older than 18 (which lack a global `fetch`), pass your own `fetch` in the client options — see [Advanced configuration](#advanced-configuration). If no `fetch` is reachable and you do not supply one, the constructor throws immediately rather than failing on the first request.

## Authenticate

The client takes a single `token`. That token is **either** a minted Geopera API key (prefix `gpra_`) **or** a session token obtained by signing in to Geopera. The platform accepts both and runs every call as that principal, with its scopes, audit trail, and provenance. The token is sent verbatim as `Authorization: Bearer <token>` on every request — there is no exchange, refresh, or grant step to perform in the SDK.

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

// Headless / server-to-server — a Geopera API key (create one in the portal).
const client = new GeoperaClient({ token: 'gpra_...' });

// Or a signed-in user's session token.
const client = new GeoperaClient({ token: userSessionToken });
```

Keep API keys server-side; never ship a `gpra_` key in browser bundles. In the browser, construct the client with a short-lived user session token instead. For how tokens are issued and what governs a call, see [Authentication](/api-reference/authentication) and [Scopes](/api-reference/scopes).

The `token` is required — constructing a client without one throws synchronously:

```typescript
new GeoperaClient({} as any);
// Error: GeoperaClient: `token` is required (a Geopera API key or session token).
```

## Your first call

Every operation is a typed method on a resource namespace: the operation id `catalog.search` is `client.catalog.search(...)`, and deeply nested ids map to deeper namespaces — `orders.archive.place` is `client.orders.archive.place(...)`. Here is a complete, runnable example that searches the catalog:

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

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

const res = await client.catalog.search({
	collections: ['sentinel-2-l2a'],
	limit: 10
});
//    ^? typed as the catalog.search output model

console.log(res);
```

That is the entire happy path: one import, one constructor, one `await`. The base URL defaults to `https://api.geopera.com`, so there is nothing else to wire up. Every call is a `POST` to `/v1/op/{operationId}` under the hood — there are no GET routes, no path parameters, and no query strings to assemble. Browse every namespace and method in the [Operations reference](/api-reference/operations).

## How type inference works

Each namespace method is generated from its operation, so TypeScript resolves the request body (the operation's input model) and the success model (its output) from the method you call alone. These types are derived from the same OpenAPI document that drives the REST API and the Python SDK, so there is nothing hand-maintained to drift.

Three things follow from this:

- **A wrong body is a compile-time error.** Misname a field, omit a required one, or pass the wrong type and your editor flags it before you ever run the code.
- **The result is typed for you.** The awaited value of `client.catalog.search(...)` is exactly the `catalog.search` output model — autocomplete and `noUncheckedIndexedAccess` work end to end.
- **Discoverable by namespace.** Type `client.orders.` and your editor lists every order operation, each with its own typed body and return.

```typescript
// ✅ Body checked against the operation's input model.
await client.catalog.search({ collections: ['sentinel-2-l2a'], limit: 10 });

// ❌ Compile error: `limt` is not a known field, and `collections` must be string[].
await client.catalog.search({ collections: 'sentinel-2-l2a', limt: 10 });
```

Namespaces encode the verb — there are no HTTP verbs or path params to choose. List, get, create, and place semantics live in the method name (for example `client.organizations.create(...)`, `client.orders.archive.place(...)`). The namespace tree is built from a Proxy: accessing a property descends one path segment, and calling a node invokes the operation at the accumulated path. Operation ids are collision-free as a tree, so a node is never simultaneously a group and a callable — `client.orders` is a namespace you can keep descending, while `client.orders.archive.place` is the method you call.

The Proxy is also Promise-safe: it deliberately does **not** expose a `then`, so awaiting a namespace node accidentally (or returning a `client.x.y` reference from an `async` function) will not be mistaken for a thenable and silently resolved. You always have to call the method to fire a request.

### Output resolves the first present 2xx

Operations do not all answer `200`. The resolved return type reflects whichever 2xx the operation declares, in priority order **200 → 201 → 202**: if the operation documents a `200` body, that is the awaited type; otherwise the `201` body; otherwise the `202` body. In practice money and asynchronous routes return `201` (created) or `202` (accepted), and the awaited type reflects whatever that operation actually declares:

```typescript
// e.g. a create route that answers 201 — the awaited type is its 201 model.
const org = await client.organizations.create({ name: 'Acme' });

// e.g. an async/long-running route that answers 202 — the awaited type is its 202 model.
const job = await client.orders.tasking.place({
	/* ...tasking body... */
});
```

You do not configure this; it is built into the method's return type. Your code just sees the correct success model regardless of which 2xx status the operation uses. At runtime the client treats every `res.ok` status the same way — it parses the JSON body and returns it — so a `200`, `201`, and `202` all flow through the same path and are typed accordingly.

## A complete, error-handled example

A non-2xx response throws a `GeoperaError`. Everything else returns the parsed, typed body. Here is an end-to-end program that authenticates, runs a call with a timeout, and handles each failure mode distinctly:

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

const token = process.env.GEOPERA_TOKEN;
if (!token) throw new Error('Set GEOPERA_TOKEN in the environment.');

const client = new GeoperaClient({ token });

async function searchRecentScenes() {
	const controller = new AbortController();
	const timeout = setTimeout(() => controller.abort(), 10_000);

	try {
		const res = await client.catalog.search(
			{ collections: ['sentinel-2-l2a'], limit: 10 },
			{ signal: controller.signal }
		);
		return res; // typed as the catalog.search output model
	} catch (err) {
		if (err instanceof GeoperaError) {
			// Platform answered with a non-2xx status + RFC 9457 problem+json body.
			switch (err.status) {
				case 401:
				case 403:
					console.error('Auth/scope problem on', err.operation, err.problem);
					break;
				case 422:
					console.error('Invalid request body:', err.problem);
					break;
				case 429:
					console.error('Rate limited — back off and retry:', err.problem);
					break;
				default:
					console.error('Operation failed:', err.status, err.operation, err.problem);
			}
			throw err;
		}
		if (err instanceof DOMException && err.name === 'AbortError') {
			// The AbortController fired — request was cancelled / timed out.
			console.error('catalog.search timed out');
			throw err;
		}
		// Network failure, JSON parse failure, etc. — surfaced as-is.
		throw err;
	} finally {
		clearTimeout(timeout);
	}
}

await searchRecentScenes();
```

`GeoperaError` carries three fields you can branch on:

| Field       | Type      | Meaning                                                                                     |
| ----------- | --------- | ------------------------------------------------------------------------------------------- |
| `status`    | `number`  | The HTTP status of the failing response (e.g. `403`, `422`, `429`).                         |
| `operation` | `string`  | The operation id that failed (e.g. `catalog.search`).                                       |
| `problem`   | `unknown` | The parsed RFC 9457 `problem+json` body Geopera returns for business and permission errors. |

Its `message` is a stable, human-readable summary of the form `Geopera operation '<operation>' failed (HTTP <status>)`. Only the platform's own non-2xx responses become a `GeoperaError`; transport-level failures (DNS, TLS, an aborted signal) propagate as the native errors `fetch` throws, so distinguish them with `instanceof GeoperaError`. For the full problem+json schema and the catalogue of error types, see [Errors](/api-reference/errors).

## Escape hatch: invoke()

The typed namespaces are the recommended surface. Underneath, every call routes through one generic method, `invoke(operationId, body, options?)`, which POSTs `body` to `/v1/op/{operationId}` and returns the parsed response. Reach for it when the operation id is **dynamic** — chosen at runtime, or not known to the generated namespaces:

```typescript
const opId = 'catalog.search'; // resolved at runtime
const res = await client.invoke(opId, { collections: ['sentinel-2-l2a'], limit: 10 });
```

`invoke` is generic over the operation id string literal, so when you pass a literal it stays fully typed (body and return inferred); pass a `string` variable and you opt into looser typing. It shares the same auth, error handling, and 2xx resolution as the namespace methods — in fact the namespaces are literally typed shorthands that call straight into `invoke`. The third `options` argument is the same `{ signal }` shape the namespace methods accept.

### One-shot: callOperation()

For a single call where constructing and holding a client is overkill, the package exports a functional helper, `callOperation`, that builds a client, runs one operation, and returns the typed result:

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

const org = await callOperation('organizations.create', { name: 'Acme' }, 'gpra_...');
```

Its signature is `callOperation(operationId, body, token, options?)`. The fourth argument accepts the same client knobs plus a per-call `signal`:

```typescript
const res = await callOperation('catalog.search', { limit: 50 }, process.env.GEOPERA_TOKEN!, {
	baseUrl: 'https://staging-api.geopera.com',
	headers: { 'X-Request-Source': 'my-app' },
	fetch: myFetch,
	signal: ac.signal
});
```

It is exactly `new GeoperaClient({ token, baseUrl, headers, fetch }).invoke(op, body, { signal })` — reach for `GeoperaClient` directly when you make more than one call so the client (and its config) is reused.

## Advanced configuration

The constructor accepts a few optional fields alongside `token`:

| Option    | Type                     | Default                   | Purpose                                                                       |
| --------- | ------------------------ | ------------------------- | ----------------------------------------------------------------------------- |
| `token`   | `string`                 | — (required)              | API key (`gpra_...`) or session token.                                        |
| `baseUrl` | `string`                 | `https://api.geopera.com` | Target a different environment (e.g. staging). Trailing slashes are stripped. |
| `headers` | `Record<string, string>` | `{}`                      | Extra headers sent on every request.                                          |
| `fetch`   | `typeof fetch`           | global `fetch`            | Custom fetch implementation (undici, a test stub, Node below 18).             |

```typescript
const client = new GeoperaClient({
	token: 'gpra_...',
	baseUrl: 'https://staging-api.geopera.com',
	headers: { 'X-Request-Source': 'my-app' },
	fetch: myFetch
});
```

The client always sets `Authorization`, `Content-Type: application/json`, and `Accept: application/json`; your `headers` are merged in afterward, so you can add tracing or routing headers but should not rely on overriding the auth or content-type headers. The resolved `baseUrl` is exposed read-only as `client.baseUrl`.

Each call accepts a trailing `options` argument with an `AbortSignal` for cancellation and timeouts:

```typescript
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), 5_000);
try {
	await client.catalog.search({ limit: 50 }, { signal: ac.signal });
} finally {
	clearTimeout(t);
}
```

The signal is forwarded straight to `fetch`, so aborting rejects the in-flight call with the runtime's native `AbortError` (not a `GeoperaError`).

## Exported types

For building your own typed helpers, the package exports the type machinery behind the client:

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

// A reusable, fully-typed wrapper for one operation.
type SearchInput = OperationInput<'catalog.search'>;
type SearchOutput = OperationOutput<'catalog.search'>;

// e.g. a function that accepts any catalog.search body and returns its result.
async function runSearch(body: SearchInput): Promise<SearchOutput> {
	return client.catalog.search(body);
}
```

`OperationId` is the union of every operation id the API exposes; `OperationInput<K>` and `OperationOutput<K>` give you the request and success models for any id. They are derived from the Geopera OpenAPI document, so they stay in sync with the platform and with the Python SDK.

## Gotchas

- **Keep `gpra_` keys server-side.** API keys grant your account's scopes; do not embed them in browser builds. Use a user session token client-side instead.
- **Node below 18 has no global `fetch`.** Pass a `fetch` implementation in the client options, or the constructor throws on construction (not on first call).
- **`baseUrl` trailing slashes are normalised.** A `baseUrl` of `https://x.test/` is stored as `https://x.test`, so request URLs never double up the slash.
- **An empty response body is returned as `undefined`.** Operations that answer with no JSON resolve to `undefined` rather than throwing — type the result accordingly if you target such an operation.
- **Only platform non-2xx responses throw `GeoperaError`.** Aborts surface as the native `AbortError`; network and JSON-parse failures propagate as their own errors. Always gate on `err instanceof GeoperaError` before reading `status` / `problem`.
- **Namespace nodes are not thenables.** `const p = client.catalog.search` is a reference, not a pending request; you must call it (`client.catalog.search(body)`) to send anything.

## Next steps

- [Operations reference](/api-reference/operations) — every operation, its namespace method, and its body.
- [Authentication](/api-reference/authentication) and [Scopes](/api-reference/scopes) — issuing tokens and what governs a call.
- [Errors](/api-reference/errors), [Idempotency](/api-reference/idempotency), [Pagination](/api-reference/pagination), and [Rate limits](/api-reference/rate-limits) — cross-cutting behaviour shared by every operation.
- [Core concepts](/api-reference/concepts) — the operation model behind the SDK.
