> ## Documentation Index
> Fetch the complete documentation index at: https://docs.engini.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Connections & OAuth

> Create connections programmatically: direct credentials, the OAuth flow, object selection, and defaults.

<Tip>The CLI's `engini connect <app>` wraps this whole page in one guided (and agent-resumable) command - see [CLI commands](/developers/cli/command-reference). Use the SDK when you need the flow inside your own product.</Tip>

## Discover how an application connects

Every application lists its authentication methods - each with an `authentication_id`, a field list, and whether it's OAuth:

<CodeGroup>
  ```python Python theme={null}
  detail = client.applications.get("slack")
  for m in detail.authentication_methods:
      print(m.authentication_id, m.authentication_name, m.requires_o_auth_sign_in)
  ```

  ```typescript TypeScript theme={null}
  const detail = await client.applications.get("slack");
  for (const m of detail.authenticationMethods) {
    console.log(m.authenticationId, m.authenticationName, m.requiresOAuthSignIn);
  }
  ```
</CodeGroup>

## Direct-credential connections (API keys, DB credentials...)

<CodeGroup>
  ```python Python theme={null}
  created = client.connections.create(
      "slack", "Sales workspace", authentication_id=2,
      fields={"token": "xoxb-..."},
  )
  connection_id = created.connection_id
  alive = client.connections.check(connection_id)
  ```

  ```typescript TypeScript theme={null}
  const created = await client.connections.create(
    "slack", "Sales workspace", 2,
    { token: "xoxb-..." },
  );
  const alive = await client.connections.check(created.connectionId);
  ```
</CodeGroup>

## The OAuth flow

Three steps: get a sign-in URL, let the user authorize in a browser, then poll for the captured token and create the connection from it.

<CodeGroup>
  ```python Python theme={null}
  import time, webbrowser

  # 1. Start the sign-in
  resp = client.connections.get_sign_in_url("outlook", authentication_id=0)
  webbrowser.open(resp.sign_in_url)          # or show resp.sign_in_url to your user
  state = resp.state

  # 2. Poll until the user completes the sign-in
  while True:
      token = client.connections.get_access_token(state)
      if token is None:
          raise RuntimeError("Sign-in state could not be resolved - start a new sign-in.")
      status = (token.status or "").lower()
      if status in {"completed", "complete", "success", "succeeded"}:
          break
      if status in {"failed", "error", "cancelled", "canceled", "denied"}:
          raise RuntimeError(token.error_message or "OAuth sign-in failed.")
      time.sleep(2)                           # empty status = still pending - keep polling

  # 3. Create the connection - pass the sign-in's state; the server looks up its own
  #    record of the sign-in and derives the credentials itself, so `fields` carries
  #    nothing here
  created = client.connections.create("outlook", "My Outlook", 0, {}, oauth_state=state)
  ```

  ```typescript TypeScript theme={null}
  // 1. Start the sign-in
  const resp = await client.connections.getSignInUrl("outlook", 0);
  console.log("Authorize here:", resp.signInUrl);
  const state = resp.state;

  // 2. Poll until the user completes the sign-in
  let token;
  for (;;) {
    token = await client.connections.getAccessToken(state);
    if (!token) {
      throw new Error("Sign-in state could not be resolved - start a new sign-in.");
    }
    const status = (token.status ?? "").toLowerCase();
    if (["completed", "complete", "success", "succeeded"].includes(status)) break;
    if (["failed", "error", "cancelled", "canceled", "denied"].includes(status))
      throw new Error(token.errorMessage ?? "OAuth sign-in failed.");
    await new Promise((r) => setTimeout(r, 2000)); // empty status = still pending
  }

  // 3. Create the connection - pass the sign-in's state; the server looks up its own
  //    record of the sign-in and derives the credentials itself, so `fields` carries
  //    nothing here
  const created = await client.connections.create("outlook", "My Outlook", 0, {}, { oauthState: state });
  ```
</CodeGroup>

<Note>
  Pass the sign-in's **`state`** as `oauth_state` (Python) / `opts.oauthState` (TypeScript) on create - the server looks up its own record of that sign-in and derives the connection's credentials from it. Don't copy `connectionData` into `fields`: values you send for server-derived keys (`ApiKey`, `RefreshToken`) are ignored, and `ExternalOrgId` is rejected outright with 400 `INVALID_FIELD`. Omitting `oauth_state` on an OAuth2 method returns 400 `OAUTH_STATE_REQUIRED`; a state the server can't resolve - expired (past 5 minutes), unknown, or another account's - returns 400 `OAUTH_STATE_EXPIRED`, deliberately identical for all three so it's not an existence oracle. This applies to `OAuth2` methods only - `OAuth2WithoutSignIn` (client-credentials) connectors need no state. The token record exists from the moment the sign-in URL is created, so every poll - including while the sign-in is pending - returns a body; while pending, `status` is an empty string and `field_options`/`fieldOptions` is `{}`. Poll on `status`, not on body presence: keep polling until `status` is `success` (or a failure status), not until the body is "no longer empty". A **null** body means the same thing here as it does for `create`: the state can't be resolved. It never turns non-null on a later poll, so treat it as a dead sign-in and start a new one - not as "still pending".
</Note>

<Note>
  Requires `engini` / `@engini/sdk` **0.21.0 or newer** (0.20.0 and earlier cannot create OAuth2 connections - `oauth_state`/`oauthState` isn't sent).
</Note>

### Field options discovered after sign-in

Some connection fields have no static list of legal values - the org itself has to be asked. Salesforce's `ApiVersionUrl` is the example: it's required, has no default, and the only source for it is the signed-in org's own `/services/data`. The poll that ends the sign-in successfully (`status` is `success`, the same check as the polling loop above) carries a `field_options` / `fieldOptions` map for exactly this case, keyed by the field name:

<CodeGroup>
  ```python Python theme={null}
  fo = token.field_options   # reuse the loop's own poll that saw status "success" - don't poll again
  options = fo["ApiVersionUrl"].to_dict() if "ApiVersionUrl" in fo else None
  # {"/services/data/v60.0": "60.0", "/services/data/v59.0": "59.0", ...}
  ```

  ```typescript TypeScript theme={null}
  const options = token.fieldOptions?.["ApiVersionUrl"]; // reuse the loop's own poll - don't poll again
  // {"/services/data/v60.0": "60.0", "/services/data/v59.0": "59.0", ...}
  ```
</CodeGroup>

<Note>
  **`field_options` is an attrs model, not a dict.** `token.field_options` supports `"ApiVersionUrl" in fo` and `fo["ApiVersionUrl"]`, but has no `.get()` - use the `in` check above, not `.field_options.get(...)`. Each value is an `AccessTokenResponseFieldOptionsAdditionalProperty` object; call `.to_dict()` on it to get the plain `{key: label}` dict shown in the comment.
</Note>

<Warning>
  **The orientation is the load-bearing part.** Each map is `{valueToSave: labelToShow}` - the **key** is what a `create`/`update` `fields` entry must send, the **value** exists only to show a human which one they're picking. `/services/data/v60.0` is the key; `60.0` is only the label. Sending the label instead of the key saves a connection that looks fine and fails on first use - `ApiVersionUrl` is a URL path prefix (`SalesforceService.cs` builds `{ApiVersionUrl}/query?q=…`), never a bare version string like `v60.0` or `60.0`.
</Warning>

`field_options` is populated only on the successful poll (`status` is `success`); on every other poll it's `{}`. On the successful poll, an empty map (`{}`) - for a field, or for the whole response - means there's nothing to offer: either no dynamic options exist for that field, or the lookup couldn't complete. Either way, fall back to `list_values`, if any, or free text; don't read `{}` on a pending poll as a lookup result at all.

<CodeGroup>
  ```python Python theme={null}
  created = client.connections.create(
      "salesforce", "Prod org", authentication_id=0,
      fields={"ApiVersionUrl": "/services/data/v60.0"},   # the key, never the label
      oauth_state=state,
  )
  ```

  ```typescript TypeScript theme={null}
  const created = await client.connections.create(
    "salesforce", "Prod org", 0,
    { ApiVersionUrl: "/services/data/v60.0" },   // the key, never the label
    { oauthState: state },
  );
  ```
</CodeGroup>

<Note>
  Requires `engini` **0.24.0 or newer** / `@engini/sdk` **0.24.0 or newer** (earlier releases don't declare `field_options`/`fieldOptions` on the access-token response at all).
</Note>

## Listing and filtering by kind

Every connection carries a **`kind`** - `Regular`, `Key`, or `MCPClient`. Filter the list to the subset the web UI's **Keys** tab shows (see [Keys vs Integrations](/developers/get-started/platform-model#keys-vs-integrations)):

<CodeGroup>
  ```python Python theme={null}
  keys = client.connections.list(kind="Key")
  for conn in keys:
      print(conn.connection_id, conn.connection_name, conn.kind)
  ```

  ```typescript TypeScript theme={null}
  const keys = await client.connections.list(undefined, "Key");
  for (const conn of keys) {
    console.log(conn.connectionId, conn.connectionName, conn.kind);
  }
  ```
</CodeGroup>

Values are case-insensitive. `MCPClient` is accepted but returns nothing - this endpoint serves only `Regular` and `Key` connections. **`OPA` is rejected** with `EnginiValidationError`: on-prem agents are not connections on this surface. They have their own resource, [`client.opa`](/developers/sdk#the-resources), with their own lifecycle, token, settings, and logs.

## Updating a connection

Change a name, description, credential fields, or metadata. Anything you leave out is omitted from the request, and the server leaves it unchanged.

<CodeGroup>
  ```python Python theme={null}
  client.connections.update(481, name="prod key")
  client.connections.update(481, fields={"apiKey": "sk-new"})
  ```

  ```typescript TypeScript theme={null}
  await client.connections.update(481, { name: "prod key" });
  await client.connections.update(481, { fields: { apiKey: "sk-new" } });
  ```
</CodeGroup>

<Warning>
  `fields` and `metadata` **replace the entire map - they do not merge.** Rotating one credential by passing a single-entry `fields` clears every other field on the connection. Read the current values first and pass the full map.
</Warning>

Omitting a member is not the same as passing `None`/`null`: the SDKs leave an omitted member out of the request body entirely, so it is never mistaken for "clear this field".

### Re-authenticating an OAuth2 connection in place

`update` also takes `oauth_state` / `oauthState` - the `state` from a fresh `get_sign_in_url` sign-in. Passing it **alone**, with no other fields, is a valid edit: the server derives fresh credentials from that sign-in and applies them to the existing connection, so you never mint a new connection id or re-point workflows that reference it.

<CodeGroup>
  ```python Python theme={null}
  resp = client.connections.get_sign_in_url("outlook", authentication_id=0)
  # user authorizes at resp.sign_in_url, then you poll get_access_token(resp.state) as above
  client.connections.update(481, oauth_state=resp.state)
  ```

  ```typescript TypeScript theme={null}
  const resp = await client.connections.getSignInUrl("outlook", 0);
  // user authorizes at resp.signInUrl, then you poll getAccessToken(resp.state) as above
  await client.connections.update(481, { oauthState: resp.state });
  ```
</CodeGroup>

Required only when the sign-in should replace stored OAuth credentials (`ApiKey`, `RefreshToken`); everything else on `update` (renaming, editing other fields) needs no state. `ExternalOrgId` is rejected outright, with or without a state.

## Pointing a workflow at a different connection

<CodeGroup>
  ```python Python theme={null}
  client.connections.replace(
      workflow_id=903, original_connection_id=481, new_connection_id=482
  )
  ```

  ```typescript TypeScript theme={null}
  await client.connections.replace({
    workflowId: 903,
    originalConnectionId: 481,
    newConnectionId: 482,
  });
  ```
</CodeGroup>

This is **workflow-scoped, not a global swap** - only workflow `903`'s activities are rewired, and every other workflow using connection `481` is untouched. The arguments are keyword-only (Python) and an options object (TypeScript) on purpose: three same-typed integers are easy to transpose.

Raises if an activity cannot be remapped, i.e. the target connection doesn't expose the activity or object the workflow needs.

## Refresh & object selection

Applications with `supports_object_selection` expose objects (tables, entities) to choose from. Refresh is **asynchronous** - trigger it, then poll:

<CodeGroup>
  ```python Python theme={null}
  client.connections.refresh(connection_id)                       # fire the async job
  status = client.connections.wait_for_refresh(connection_id)     # polls refresh-status (300s timeout, 2s interval)

  objs = client.connections.objects(connection_id)
  client.connections.select_objects(connection_id, [o.object_id for o in objs[:5]])
  client.connections.wait_for_refresh(connection_id)              # selection triggers another refresh
  ```

  ```typescript TypeScript theme={null}
  await client.connections.refresh(connectionId);
  const status = await client.connections.waitForRefresh(connectionId); // 300s timeout, 2s interval

  const objs = await client.connections.objects(connectionId);
  await client.connections.selectObjects(connectionId, objs.slice(0, 5).map((o) => o.objectId));
  await client.connections.waitForRefresh(connectionId);
  ```
</CodeGroup>

`select_objects` **replaces** the whole selection set. A completed refresh also proves the connection authenticates - it's a stronger health signal than `check` (which can be inconclusive). A refresh that doesn't finish in time raises `EnginiTimeoutError`.

## Defaults

The default connection is what implicit tool execution resolves to:

<CodeGroup>
  ```python Python theme={null}
  client.connections.set_default(connection_id)
  client.connections.defaults()               # one entry per application
  client.connections.clear_default("slack")
  conn_id = client.connections.resolve("slack", "Sales workspace")   # name -> id
  ```

  ```typescript TypeScript theme={null}
  await client.connections.setDefault(connectionId);
  await client.connections.defaults();
  await client.connections.clearDefault("slack");
  const connId = await client.connections.resolve("slack", "Sales workspace");
  ```
</CodeGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.