---
author: OOMOL
author_url: https://oomol.com/about/
dateModified: 2026-09-10
title: OOMOL TypeScript SDK reference
description: Reference the shared configuration, types, errors, and API surface
  for Connector, ProjectConnector, and OpenConnector.
lang: en
canonical_url: https://oomol.com/docs/connector-sdk/
markdown_url: https://oomol.com/docs/connector-sdk.md
---

# OOMOL TypeScript SDK reference

[`@oomol-lab/connector`](https://github.com/oomol-lab/connector-sdk) contains three TypeScript clients. Choose the client that matches the product path before using the shared configuration and API reference on this page:

- [`Connector`](/docs/connector-client/) calls accounts connected to your own OOMOL account through the hosted gateway.
- [`ProjectConnector`](/docs/project-connector/) connects and calls accounts owned by users of your SaaS product.
- [`OpenConnector`](/docs/openconnector-sdk/) calls an OpenConnector runtime that you operate.

The clients share transport behavior, precise action types, and the error model. Their keys, account boundaries, and available methods differ.

The package is lightweight and zero-dependency; the SDK builds each request and parses the response. TypeScript can call actions such as `gmail.search_threads`, `slack.post_message`, and `notion.append_block` directly.

## Install

```sh
npm install @oomol-lab/connector   # or: bun add / pnpm add / yarn add
```

Requires Node ≥ 18, for the built-in `fetch` and `AbortController`. The SDK ships only `dist`, has zero runtime dependencies, and is `sideEffects: false`, so it tree-shakes cleanly. Use `Connector` and `ProjectConnector` from a trusted server-side runtime such as Node, Bun, Deno, or an edge worker. Browser apps should call your backend; never bundle a personal or Project API key into client code.

## Three clients at a glance

| | `Connector` | `ProjectConnector` | `OpenConnector` |
| --- | --- | --- | --- |
| Authentication | Personal `api_…` key | Project `oo_proj_…` key | Optional runtime token `oct_…` |
| Connects to | OOMOL-hosted gateway | OOMOL-hosted gateway | An OpenConnector runtime you operate |
| Account resource | Personal or Team connections | Connected accounts for external users | Connections in the runtime |
| Use case | Call accounts connected by an individual or Team | Connect and call accounts for SaaS product users | Call a self-hosted runtime |
| Integration guide | [Connector SDK](/docs/connector-client/) | [ProjectConnector](/docs/project-connector/) | [OpenConnector SDK](/docs/openconnector-sdk/) |

## `Connector`: call personal or Team connections

### Get an API key

You need an OOMOL Connector **personal API key**, shaped like `api_…`. Create one in the OOMOL Console:

<https://console.oomol.com/api-key>

Set it as an environment variable. This guide uses `OOMOL_API_KEY` throughout. The gateway authorizes every request and receives the key as `Authorization: Bearer <apiKey>`.

> A personal `api_…` key runs actions on personal or Team connections. To connect accounts for end users, use a Project key (`oo_proj_…`) and `ProjectConnector`; see the [ProjectConnector integration guide](/docs/project-connector/). To call a self-hosted runtime, use its optional runtime token (`oct_…`) and `OpenConnector`; see the [OpenConnector SDK guide](/docs/openconnector-sdk/).

### Quickstart

Construct a client, then call an action. The two forms below are equivalent.

```ts
import { Connector } from "@oomol-lab/connector";

const oomol = new Connector({ apiKey: process.env.OOMOL_API_KEY! });

// Path 1: dynamic string. Callable for any action id.
const { threads } = await oomol.execute("gmail.search_threads", { query: "from:boss" });

// Path 2: namespace sugar. Same call underneath.
const result = await oomol.gmail.search_threads({ query: "is:unread" });
```

`execute` returns the action output directly. When you also want the execution metadata, use `executeRaw`:

```ts
const raw = await oomol.executeRaw("gmail.search_threads", { query: "from:ceo" });
raw.data;        // the same value execute() returns
raw.executionId; // server-assigned execution id (useful for support / log correlation)
raw.actionId;    // echoed action id
raw.message;     // human-readable message from the success envelope
```

The core call flow is `execute` and `executeRaw`.

### `Connector` concepts

The SDK model has five core concepts. Authentication, credentials, and provider calls are handled by the gateway:

| Term | What it is |
| --- | --- |
| **Gateway** | The hosted OOMOL Connector service this client talks to. It holds credentials, performs the actual provider calls, and returns a uniform envelope. The SDK runs **no** integration logic locally. |
| **Provider / service** | A third-party API (`gmail`, `slack`, `github`, `notion`, …). It is the `<service>` prefix of an action id. |
| **Action** | One operation on a provider, identified as `"<service>.<action>"` (e.g. `gmail.search_threads`). Actions are provided by the gateway and called by the client. |
| **Connection** | A stored, already-authorized credential for a provider. You never touch tokens; you name which connection to use via `connectionName`. OAuth and credential lifecycle are the gateway's job. |
| **Team** | Optional tenant scoping, sent as the `x-oo-team-name` header. |

The same vocabulary carries into the SDK surface: an action id is always `"<service>.<action>"`, `connectionName` selects a stored connection, and, for the project client, `externalUserId` identifies one of your end users.

### Common operations

| Want to… | Use | Notes |
| --- | --- | --- |
| Run a modeled action | `execute` / `executeRaw` | The typed one-liner. `executeRaw` also returns `{ executionId, actionId, message }`. |
| Hit an endpoint not yet modeled as an action | `proxy` | Passthrough to the upstream API, with the connection's credentials injected by the gateway. |
| Feed actions to an LLM / build dynamic forms | `catalog` | Runtime JSON Schema (2020-12) for any action or provider. |
| Discover what's connected | `apps.list` | Read-only list of the connections you've already linked. |
| Let *your* users connect *their* accounts | `ProjectConnector` | A separate, project-scoped client to connect accounts on behalf of your end users and run actions for them. |

Provider and action coverage is supplied by the gateway. Check the [OOMOL public catalog API](https://connector.oomol.com/v1/catalog) for catalog-wide provider and action totals, or browse the [App directory](/apps/directory/). Discover providers for your configured gateway at runtime with `oomol.catalog.providers()`; callable actions depend on account authorization and access policy.

### Precise types (optional)

The dynamic string path compiles for **any** `actionId`. By default, every action is loosely typed (`Record<string, any>` in and out), which keeps new actions callable immediately. For precise per-action input/output types plus JSDoc, install the companion types package and add **one side-effect import per provider** you use:

```ts
import { Connector } from "@oomol-lab/connector";
import "@oomol-lab/connector-types/gmail";   // precise types + JSDoc for gmail.*
import "@oomol-lab/connector-types/slack";   // …and slack.*

const oomol = new Connector({ apiKey: process.env.OOMOL_API_KEY! });

await oomol.gmail.search_threads({ query: "from:boss" }); // input + output now precise
await oomol.notion.append_block({ pageId, text });        // notion not imported → still loosely callable
```

```sh
npm install -D @oomol-lab/connector-types
```

Precise types come from `@oomol-lab/connector-types` and side-effect imports, leaving the project free of generated files. Registered actions get literal completion and exact input/output types; **unregistered ones degrade to `Record<string, any>`**. When the types package lags the backend, new actions remain callable through the loose fallback. The core runtime and types package are released separately.

> Requires `moduleResolution` set to `bundler`, `node16`, or `nodenext` so the subpath imports (`@oomol-lab/connector-types/gmail`) resolve. See the [`@oomol-lab/connector-types`](https://github.com/oomol-lab/connector-types) repository for setup details.

### Configuration

Every field except `apiKey` is optional:

```ts
new Connector({
  apiKey: process.env.OOMOL_API_KEY!,        // required
  baseUrl: "https://connector.oomol.com/v1", // default
  team: "acme",                              // default team → x-oo-team-name
  connectionName: "work",                    // default connection (prefer per-call / using())
  timeoutMs: 30_000,                         // default per-request timeout
  maxRetries: 2,                             // default; retries 429 / 5xx / network with backoff + jitter
  fetch: customFetch,                        // inject for tests / proxies / tracing
});
```

| Field | Default | Notes |
| --- | --- | --- |
| `apiKey` | — | Required. Sent as `Authorization: Bearer <apiKey>`. |
| `baseUrl` | `https://connector.oomol.com/v1` | Override explicitly in the client configuration. |
| `team` | — | Which tenant the call runs under. |
| `connectionName` | — | Which stored connection to use when a provider has more than one. Use it as a client default for simple setups; for multiple connections, set it per call or via `using()`. |
| `timeoutMs` | `30_000` | Per-request timeout in milliseconds. |
| `maxRetries` | `2` | Retries on 429 / 5xx / network errors, with exponential backoff and jitter. |
| `fetch` | global `fetch` | Inject a custom `fetch` for tests, proxy agents, or tracing. |

For actions with side effects, such as `gmail.send_email` or `slack.post_message`, pass `{ retries: 0 }` unless that action provides an idempotency mechanism you have verified. A network failure can occur after the provider accepted the request, so an automatic retry may repeat the operation.

#### Scopes and per-call options

Three layers resolve in this precedence: **per-call options > `using()` scope > client defaults.**

`using()` returns an immutable scoped sub-client that merges the given defaults; the original client is untouched:

```ts
const work = oomol.using({ connectionName: "work", team: "acme" });
await work.gmail.search_threads({ query: "label:urgent" }); // runs under "work" / "acme"
```

Per-call options apply only to that call and have the highest precedence:

```ts
await oomol.execute(
  "gmail.search_threads",
  { query: "from:ceo" },
  {
    team: "acme",           // override team for this call
    connectionName: "alt",  // pick a different connection for this call
    timeoutMs: 10_000,      // tighter timeout for this call
    retries: 0,             // disable retries for this call
    signal: controller.signal, // forward an AbortSignal
  },
);
```

`connectionName` resolves with the same layering. It is carried on the wire as the `x-oo-connector-alias` header (the gateway field name is `alias`); the SDK surface uses `connectionName`.

### Proxy: call an upstream endpoint directly

`proxy` calls an upstream API endpoint directly. The gateway injects credentials from the selected connection, while the request and response keep the upstream API shape.

```ts
// Typed GET. The request path uses the `endpoint` field.
const repos = await oomol.proxy<Array<{ name: string }>>("github", {
  endpoint: "/user/repos",
  method: "GET",
  query: { per_page: 5, sort: "updated" },
});
repos.status;               // upstream HTTP status
repos.data.map((r) => r.name);

// POST with a body and upstream headers; these headers go to the provider.
await oomol.proxy("github", {
  endpoint: "/repos/acme/widgets/issues",
  method: "POST",
  headers: { "X-GitHub-Api-Version": "2022-11-28" },
  body: { title: "Tracking issue", labels: ["chore"] },
});
```

`endpoint` accepts either a path (resolved against the provider's base URL) or a full URL, which is useful for providers with regional hosts such as `https://eu.posthog.com/api/...`. `method` is one of `GET | POST | PUT | PATCH | DELETE`. The response is `{ status, headers, data }`. The proxy body is strict on the backend: unknown top-level keys are rejected as `invalid_input`.

### Catalog: introspect providers and actions

The catalog provides read-only runtime metadata for dynamic forms, validation, and LLM tool definitions. Input/output schemas use JSON Schema (2020-12) and are independent of the compile-time types package.

```ts
// List providers; optionally narrow server-side.
const all = await oomol.catalog.providers();                       // every provider
const mailish = await oomol.catalog.providers({ q: "mail" });      // free-text search → ?q=
const some = await oomol.catalog.providers({ service: ["gmail", "slack"] }); // restrict → ?service=…

// All actions of one service.
const actions = await oomol.catalog.actions("gmail");

// Full metadata for one action, including runtime JSON Schema.
const meta = await oomol.catalog.action("gmail.search_threads");
meta.name;          // human-readable name
meta.requiredScopes;// OAuth scopes the action needs
meta.inputSchema;   // JSON Schema (2020-12) for the input
meta.outputSchema;  // JSON Schema (2020-12) for the output
```

Each provider carries `{ service, displayName, iconUrl, homepageUrl, categories, authTypes }`.

### Apps: list your connected accounts

`apps.list()` returns a read-only view of the connections the gateway already holds for you. Connection creation and removal happen in Console.

```ts
const apps = await oomol.apps.list();
for (const app of apps) {
  // { id, service, status, connectionName, … }; connectionName is null when none is set.
  console.log(`${app.service}: id=${app.id} status=${app.status} connectionName=${app.connectionName}`);
}

// Target a specific connection by passing its connectionName back as the per-call selector.
const work = apps.find((a) => a.connectionName === "work");
if (work) {
  await oomol.execute("gmail.search_threads", { query: "is:unread" }, { connectionName: "work" });
}
```

### Error handling

Failures throw a typed `ConnectorError`. Caller cancellation (an aborted `AbortSignal`) rejects with the standard `AbortError`, so it can be handled separately from gateway or transport errors.

```ts
import { Connector, ConnectorError, isRetryable } from "@oomol-lab/connector";

try {
  await oomol.slack.post_message({ channel: "#general", text: "shipped" });
} catch (err) {
  if (err instanceof ConnectorError) {
    err.code;        // discriminable union, e.g. "rate_limited", "credential_expired"
    err.status;      // HTTP status (0 for client-side / network errors)
    err.requestId;   // failure-correlation id
    err.actionId;    // when applicable
    err.executionId; // when applicable
    err.data;        // upstream response body, e.g. on provider_error
    if (isRetryable(err)) {
      // 429 / 5xx / network / rate_limited / proxy_upstream_timeout / request_in_progress
    }
  } else {
    throw err; // non-ConnectorError, e.g. AbortError from caller cancellation; rethrow
  }
}
```

`err.code` is an **open** union: known backend codes get autocompletion, and new backend codes still pass through as strings. Keep a default branch when handling it. Common codes, grouped:

| Group | Codes |
| --- | --- |
| Input / request | `invalid_input`, `invalid_request_payload`, `invalid_request_signature` |
| App / provider | `app_not_found`, `app_not_ready`, `app_auth_type_mismatch`, `provider_not_found`, `provider_not_configured`, `provider_config_not_found`, `provider_error`, `profile_not_found` |
| Credential / auth | `credential_expired`, `scope_missing`, `user_oauth_client_required` |
| Connection selection | `connection_ambiguous`, `connection_account_conflict`, `connection_alias_conflict`, `connection_request_not_found`, `connected_account_not_found` |
| Proxy | `proxy_not_supported`, `proxy_upstream_error`, `proxy_upstream_timeout`, `proxy_response_too_large` |
| Rate / concurrency | `rate_limited`, `request_in_progress`, `request_key_conflict`, `request_key_used` |
| Client-only (status 0, no request sent or transport failure) | `client_invalid_request`, `client_timeout`, `client_network_error`, `client_wait_timeout` |

`isRetryable(err)` returns `true` for `rate_limited`, `proxy_upstream_timeout`, `request_in_progress`, HTTP 429, any 5xx, and transport failures (status 0). It returns `false` for client validation errors (`client_invalid_request`) and the `waitForConnection` cap (`client_wait_timeout`); those cases usually need a call or waiting-flow change.

#### Cancellation and timeouts

Forward an `AbortSignal` to cancel; set `timeoutMs` to bound a single call. The built-in retry layer handles transient failures. Use `retries: 0` for a single deterministic attempt.

```ts
const controller = new AbortController();
setTimeout(() => controller.abort(), 50);
try {
  await oomol.execute("gmail.search_threads", { query: "huge" }, { signal: controller.signal });
} catch (err) {
  (err as Error).name; // "AbortError"
}
```

## `ProjectConnector`: connect accounts for product users

`Connector` runs actions on **your own** connections. `ProjectConnector` is for SaaS products: *your* end users link *their own* Gmail / Slack / GitHub / … accounts through your app, and your backend runs actions on their behalf. This is the managed-auth model used by products such as [Composio](https://composio.dev) and [Pipedream Connect](https://pipedream.com/docs/connect).

With OAuth, the user authorizes on a gateway-hosted page; provider tokens stay in the gateway and your code holds opaque identifiers. `project.connect.apiKey` and `project.connect.customCredential` are different: your trusted backend receives the user's secret and sends it to the gateway. Keep those secrets out of browsers, prompts, model context, and logs.

### The end-user identifier

**`externalUserId`** is the user-isolation key for the project client. You choose it, typically from your own user database. Project operations such as connecting an account, waiting for a connection, and executing an action are scoped to it. Pass the same `externalUserId` consistently and the gateway keeps each user's connections isolated.

### Construct the project client

`ProjectConnector` is a separate client built with a **Project API key** (`oo_proj_…`). It provides Project-scoped operations such as `connect.*`, `waitForConnection`, `getUserProfile`, `execute`, `executeRaw`, and `forUser`.

```ts
import { ProjectConnector } from "@oomol-lab/connector";

const project = new ProjectConnector({ apiKey: process.env.OOMOL_PROJECT_API_KEY! }); // oo_proj_...
```

> **Console setup comes first.** Before your backend connects accounts, an administrator creates a Project, a provider config, and a Project API key in OOMOL Console. The one-time setup and matching backend REST flow are covered in the [Connector for SaaS guide](/docs/connector-saas/). This SDK is the typed wrapper over the same runtime API.

### OAuth: create a link, then await completion

OAuth has two steps: create a pending connection request, send the user to authorize, then wait for completion.

```ts
// 1. Create a pending connection request for one of your users.
const request = await project.connect.oauth("user_42", {
  service: "gmail",
  connectionName: "work",                          // the name to assign; reuse it later to target this account
  returnUri: "https://app.example.com/connected",  // where the gateway returns the user after the callback
});

// 2. Send your user to the provider's authorization page.
redirectUserTo(request.authorizationUrl);

// 3. Poll until the user finishes (or it fails / expires). Returns the final connection request.
const connected = await project.waitForConnection(request);
connected.status;             // "connected" | "failed" | "expired"
connected.connectedAccountId; // the stored account id once connected
```

The `authorizationUrl` first opens a Connector-hosted entry page that names your project and the provider the user is about to authorize, then sends the user to the provider's OAuth page.

![Connector authorization entry showing my-saas connecting to Gmail before redirecting to Gmail](/img/docs/connector-sdk/en/authorization-entry.webp)

`waitForConnection` polls until the request leaves the `initiated` state and returns it. If the user does not finish authorization, the request naturally becomes `expired`. If `maxWaitMs` (default `600_000`ms, matching the request's expiry) elapses first, it throws a `ConnectorError` with code `client_wait_timeout`; an aborted `signal` rejects with the standard `AbortError`.

When your `returnUri` is hit, the gateway appends query parameters you can read on your callback page:

```text
status=success
service=gmail
providerConfigId=pc-1
externalUserId=user_42
connectedAccountId=ca-1
```

…or, on cancellation / provider error:

```text
status=error
code=<connector-error-code>
message=<human-readable-message>
```

### API key / custom credential: synchronous

API-key and custom-credential connects return an account immediately. Only OAuth needs `waitForConnection`.

```ts
// The end user's own upstream key (e.g. an OpenAI sk-…), not your oo_proj_ key.
const account = await project.connect.apiKey("user_42", { service: "openai", apiKey: "sk-..." });
account.available; // whether the account can execute actions right now

// Provider-specific credential fields, validated by the gateway against the provider config.
await project.connect.customCredential("user_42", {
  service: "jira",
  values: { email: "user@acme.com", token: "..." },
});
```

Every `connect.*` call identifies the provider by **exactly one** of `service` or `providerConfigId`. Use `providerConfigId` when a project has more than one config for the same service; `service` is the simple case.

### Who did they connect as?

`getUserProfile` reads the third-party account holder behind a connected account: who your end user actually is on the provider. The gateway fetches it live from the provider and normalizes it into one shape across providers.

```ts
const { service, profile, fetchedAt } = await project.getUserProfile(account.connectedAccountId);

profile.id;          // stable provider-side user id
profile.kind;        // "user" | "bot" | "service_account" | "unknown" (open union)
profile.username;    // handle, or null
profile.displayName; // display name, or null
profile.avatarUrl;   // avatar URL, or null
profile.email;       // email, or null when the granted scopes do not expose it
profile.metadata;    // provider-specific fields the gateway exposes
```

Fields that a provider omits or the granted scopes do not cover (`email` most often) remain present in the response with a `null` value. `fetchedAt` is the Unix timestamp in milliseconds when the gateway read the profile. Pass a `connectedAccountId` from a `connect.apiKey` / `connect.customCredential` result, or from a `ConnectionRequest` that reached `connected`. An unknown id rejects with `connected_account_not_found`; a provider missing the `userProfile` capability rejects with `profile_not_found`; an inactive or unusable account may reject with `app_not_ready`, `app_auth_type_mismatch`, or `credential_expired`. The scoped sub-client has it too: `user.getUserProfile(connectedAccountId)`.

### Execute on the user's behalf

```ts
// The provider service is derived from the actionId prefix ("gmail").
// Without connectionName / connectedAccountId, the user's latest active account is used.
const out = await project.execute(
  "user_42",
  "gmail.search_threads",
  { query: "is:unread" },
  { connectionName: "work" },
);
```

Account selection precedence: `connectedAccountId` (a specific account) beats `connectionName` (an account by its name); with neither, the gateway uses the user's latest active account for that provider. `project.executeRaw` returns the same `{ data, executionId, actionId, message }` envelope as the personal client.

### Scope to one user

`forUser` binds the `externalUserId` once so later calls do not repeat the id:

```ts
const user = project.forUser("user_42");
const request = await user.connect.oauth({ service: "slack" });
const slack = await user.waitForConnection(request);
if (slack.status === "connected") {
  await user.execute(
    "slack.post_message",
    { channel: "#general", text: "shipped" },
    { connectedAccountId: slack.connectedAccountId },
  );
}
```

`project.execute` reuses the same [`@oomol-lab/connector-types`](https://github.com/oomol-lab/connector-types) registry as the personal path: imported providers get precise input/output, and the rest stay loosely callable.

### Authorization-request and account lifecycle

| Object | When you get it | Key fields |
| --- | --- | --- |
| `ConnectionRequest` | returned by `connect.oauth`, re-read via `getConnectionRequest` / `waitForConnection` | `id`, `status` (`initiated` → `connected` / `failed` / `expired`), `authorizationUrl`, `connectedAccountId`, `externalUserId`, `connectionName`, `expiresAt` |
| `ConnectedAccount` | returned synchronously by `connect.apiKey` / `connect.customCredential`; pointed to by a completed OAuth request | `id` / `connectedAccountId`, `status` (`active`, `reauth_required`, `error`, `disconnected`), `available`, `externalUserId`, `connectionName`, `service` |

`available` is `true` only when everything needed to execute is in place: the provider config is active, the account is active, the underlying app is active, and a credential exists. Both status fields are **open** unions; keep a default branch to handle new backend statuses.

### Coming from Composio / Pipedream?

| Composio / Pipedream | `@oomol-lab/connector` |
| --- | --- |
| `userId` / `external_user_id` | `externalUserId` |
| `connectedAccounts.initiate` / `createConnectToken` (OAuth) | `project.connect.oauth` |
| `connectedAccounts.initiate` + `AuthScheme.APIKey` | `project.connect.apiKey` |
| `waitForConnection()` | `project.waitForConnection()` |
| `tools.execute(slug, { userId, arguments })` | `project.execute(externalUserId, actionId, input)` |
| `composio.getEntity(userId)` | `project.forUser(externalUserId)` |

## `OpenConnector`: call a self-hosted runtime

Running the open-source Connector server yourself — on localhost, in Docker, or on your own infra? **`OpenConnector`** is the personal client for it. It mirrors the `Connector` surface (both call paths, `proxy`, `catalog`, `apps`) pointed at the server *you* run, so the code you already wrote barely changes. Standing the server up is a separate topic — see the [OpenConnector self-hosting guide](/docs/openconnector-self-hosting/).

The `OpenConnector` client points to a runtime you operate. That runtime's configuration determines account organization, connection selection, and access policy.

```ts
import { OpenConnector } from "@oomol-lab/connector";

const open = new OpenConnector(); // local/private setup only; defaults to http://localhost:3000

await open.execute("hackernews.get_top_stories", {});             // path 1 — dynamic string
await open.gmail.search_threads({ query: "from:boss" });          // path 2 — namespace sugar, same registry types
await open.proxy("github", { endpoint: "/user", method: "GET" }); // path 3 — passthrough to an un-modeled endpoint
await open.catalog.search("send email", { limit: 5 });            // catalog extras: search, services
await open.apps.list();                                           // read-only view of the runtime's connections
```

Both call paths and the precise-types workflow match the personal `Connector`; the same `@oomol-lab/connector-types` side-effect imports apply. OpenConnector accepts a **relative path** beginning with `/` for `proxy.endpoint`; an absolute URL returns `invalid_input`. A provider missing a proxy executor returns `proxy_not_supported`.

### Point it at your server

`baseUrl` accepts the server **origin**, and the client appends the API path prefix. A runtime without tokens is suitable only for localhost or an otherwise private network. Before exposing it through a public URL, create a runtime token (`oct_…`) in the Web Console under Access and require it from every `/v1` and `/mcp` client.

```ts
const open = new OpenConnector({
  baseUrl: "https://connect.internal.example.com",       // the server origin
  runtimeToken: process.env.OOMOL_CONNECT_RUNTIME_TOKEN!, // oct_…; required for a public runtime
  connectionName: "work",                                // optional client-level default connection
});
```

Every field is optional. `timeoutMs`, `maxRetries`, and `fetch` behave exactly as on the hosted client.

### Runtime-specific surface

`OpenConnector` provides these runtime-specific methods:

```ts
await open.health();                                    // { ok, runtime } — connectivity / auth probe
await open.catalog.services();                          // every service id that has actions
await open.catalog.search("top stories", { limit: 3 }); // rank actions by free-text relevance
await open.apps.listByService("github");                // one service's connections
await open.apps.authenticated(["github", "notion"]);    // which have a REAL credential stored
```

Connection selection has two layers: a per-call `connectionName` overrides the client-level default, and omitting both selects the runtime's `"default"` connection.

> **Use the Web Console for management.** Create connections, configure OAuth clients, and mint runtime tokens in the console; the `OpenConnector` SDK calls the configured runtime. See the [self-hosting guide](/docs/openconnector-self-hosting/) for setup. When a service id collides with a member name (`execute` / `executeRaw` / `health` / `proxy` / `catalog` / `apps`), call it through `execute("<service>.<action>", …)`.

Full runnable tour — [`examples/open.ts`](https://github.com/oomol-lab/connector-sdk/blob/main/examples/open.ts).

## Reference

### `Connector` (personal `api_…` key)

```ts
new Connector(config: ClientConfig)

oomol.execute(actionId, input, options?)     // → action output
oomol.executeRaw(actionId, input, options?)  // → { data, executionId, actionId, message }
oomol.<service>.<action>(input, options?)    // namespace sugar for execute
oomol.using(scope)                           // → immutable scoped sub-client
oomol.proxy(service, { endpoint, method, query?, headers?, body? }, options?) // → { status, headers, data }
oomol.catalog.action(actionId, options?)     // → ActionMetadata
oomol.catalog.actions(service, options?)     // → ActionMetadata[]
oomol.catalog.providers(query?, options?)    // → ProviderMetadata[]   query: { service?: string[]; q?: string }
oomol.apps.list(options?)                    // → ConnectedApp[]
```

### `ProjectConnector` (project `oo_proj_…` key)

```ts
new ProjectConnector(config: ProjectConnectorConfig)

project.connect.oauth(externalUserId, input, options?)            // → ConnectionRequest (pending)
project.connect.apiKey(externalUserId, input, options?)           // → ConnectedAccount (synchronous)
project.connect.customCredential(externalUserId, input, options?) // → ConnectedAccount (synchronous)
project.getConnectionRequest(connectionRequestId, options?)       // → ConnectionRequest
project.waitForConnection(requestOrId, options?)                  // → ConnectionRequest   options: { pollIntervalMs?, maxWaitMs?, signal?, timeoutMs? }
project.getUserProfile(connectedAccountId, options?)              // → ConnectedAccountProfile
project.execute(externalUserId, actionId, input, options?)        // → action output
project.executeRaw(externalUserId, actionId, input, options?)     // → { data, executionId, actionId, message }
project.forUser(externalUserId)                                   // → ProjectUser (same methods, id bound)
```

`connect.*` input is `{ service | providerConfigId } & { connectionName?, … }` (exactly one of `service` / `providerConfigId`). `execute` options add `{ providerConfigId?, service?, connectedAccountId?, connectionName? }`.

### `OpenConnector` (self-hosted runtime, optional `oct_…` token)

```ts
new OpenConnector(config?: OpenConnectorConfig)   // every field optional; baseUrl defaults to http://localhost:3000

open.execute(actionId, input, options?)       // → action output
open.executeRaw(actionId, input, options?)    // → { data, executionId, actionId, message }
open.<service>.<action>(input, options?)      // namespace sugar for execute
open.health(options?)                         // → { ok, runtime }
open.proxy(service, { endpoint, method, query?, headers?, body? }, options?) // → { status, headers, data }   (endpoint must be a relative path)
open.catalog.action(actionId, options?)       // → OpenActionMetadata
open.catalog.actions(service, options?)       // → OpenActionMetadata[]
open.catalog.services(options?)               // → string[]   (service ids that have actions)
open.catalog.providers(query?, options?)      // → ProviderMetadata[]
open.catalog.search(q, query?, options?)      // → OpenActionSearchResult[]   query: { service?, limit? }
open.apps.list(options?)                      // → ConnectedApp[]
open.apps.listByService(service, options?)    // → ConnectedApp[]
open.apps.authenticated(services, options?)   // → string[]   (services with a real credential)
```

`options` is `{ connectionName?, signal?, timeoutMs?, retries? }` (no `team`, no `using()`). `config` adds `{ baseUrl?, runtimeToken?, connectionName?, timeoutMs?, maxRetries?, fetch? }`.

### Exports

```ts
import {
  Connector,
  ProjectConnector,
  OpenConnector,
  ConnectorError,
  isRetryable,
} from "@oomol-lab/connector";
import type {
  ClientConfig, CallOptions, ScopeOptions, RawResult,
  ProxyRequest, ProxyResponse, ProxyMethod,
  CatalogApi, ActionMetadata, ProviderMetadata, ProviderQuery,
  AppsApi, ConnectedApp,
  ConnectorErrorCode,
  ProjectConnectorConfig, ProjectCallOptions, ProjectExecuteOptions,
  ConnectionRequest, ConnectedAccount, ProviderSelector,
  ConnectedAccountProfile, ProviderUserProfile, ProviderUserKind,
  OAuthConnectInput, ApiKeyConnectInput, CustomCredentialConnectInput,
  OpenConnectorConfig, OpenConnectorApi, OpenCallOptions, OpenExecuteOptions,
  OpenCatalogApi, OpenAppsApi, OpenHealth,
  OpenActionMetadata, OpenActionFollowUp, OpenActionAsyncLifecycle,
  OpenActionSearchResult, OpenSearchQuery,
} from "@oomol-lab/connector";
```

Runnable, type-checked examples live in the repository's [`examples/`](https://github.com/oomol-lab/connector-sdk/tree/main/examples) directory.

## License

MIT, see the [connector-sdk](https://github.com/oomol-lab/connector-sdk) repository.
