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

# Connect a customer's app (OAuth)

> Run the full OAuth handshake from your own product, so your users connect their apps without leaving it.

<Info>**Uses:** Python · TypeScript · CLI · REST API</Info>

If you're embedding Engini in your own product, this is the flow you have to implement: your user clicks "Connect Gmail", authorizes in their browser, and comes back with a working connection - never seeing Engini.

Four steps: discover the auth method, get a sign-in URL, poll for the captured token, create the connection.

<Note>
  cURL samples assume `BASE=https://api.engini.io/v1` and `AUTH="x-api-key: $ENGINI_API_KEY"` - the setup from the [REST walkthrough](/developers/cookbook/rest-api-walkthrough).
</Note>

## 1. Find the OAuth method

Each application exposes one or more authentication methods. Prefer the one flagged `isDefault` - exactly one method is marked default, and it's the one used when a connection-create request omits `authenticationId` - rather than picking "the first one with `requiresOAuthSignIn`". Methods also carry a `description` and, when set, a `helpUrl` (a per-method install-guide link); show both if you're building your own picker.

<CodeGroup>
  ```python Python theme={null}
  detail = client.applications.get("outlook")
  method = next(
      (m for m in detail.authentication_methods if m.is_default),
      next(m for m in detail.authentication_methods if m.requires_o_auth_sign_in),
  )
  ```

  ```typescript TypeScript theme={null}
  const detail = await client.applications.get("outlook");
  const method =
    detail.authenticationMethods.find((m) => m.isDefault) ??
    detail.authenticationMethods.find((m) => m.requiresOAuthSignIn)!;
  ```

  ```bash cURL theme={null}
  curl -s "$BASE/applications/outlook" -H "$AUTH" \
    | jq '.authenticationMethods[] | {authenticationId, authenticationName, isDefault, description, helpUrl}'
  # pick the entry with isDefault: true, or requiresOAuthSignIn: true if none is flagged
  ```
</CodeGroup>

<Tip>
  Some applications require values *before* sign-in (a Salesforce sandbox flag, a region). Those are the `connectionFields` entries with `isRequiredOnSignin: true` - collect them from your user first and pass them as `fields` in the next step.
</Tip>

## 2. Get a sign-in URL

<CodeGroup>
  ```python Python theme={null}
  resp = client.connections.get_sign_in_url("outlook", method.authentication_id)
  sign_in_url, state = resp.sign_in_url, resp.state
  ```

  ```typescript TypeScript theme={null}
  const { signInUrl, state } = await client.connections.getSignInUrl("outlook", method.authenticationId);
  ```

  ```bash cURL theme={null}
  curl -s -X POST "$BASE/connections/get-sign-in-url" -H "$AUTH" -H "Content-Type: application/json" -d '{
    "applicationSlug": "outlook",
    "authenticationId": 0
  }'
  # -> { "signInUrl": "https://login.microsoftonline.com/...", "state": "3f2a..." }
  ```
</CodeGroup>

Send your user to `signInUrl` (redirect, popup, or new tab). **Keep the `state`** - it's how you collect the result. Store it against the user's session.

## 3. Poll until they finish

Engini captures the provider's callback for you. Poll with the `state` until a token appears.

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

  deadline = time.time() + 300
  while time.time() < deadline:
      token = client.connections.get_access_token(state)
      status = ((token and 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 "The user did not complete sign-in.")
      time.sleep(2)
  else:
      raise TimeoutError("Sign-in was not completed in time.")
  ```

  ```typescript TypeScript theme={null}
  const deadline = Date.now() + 300_000;
  let token;
  for (;;) {
    token = await client.connections.getAccessToken(state);
    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?.error_message ?? "The user did not complete sign-in.");
    if (Date.now() > deadline) throw new Error("Sign-in was not completed in time.");
    await new Promise((r) => setTimeout(r, 2000));
  }
  ```

  ```bash cURL theme={null}
  # poll every 2s; an empty body means "still waiting"
  until curl -s "$BASE/connections/access-token/$STATE" -H "$AUTH" | jq -e '.status' >/dev/null 2>&1; do
    sleep 2
  done
  curl -s "$BASE/connections/access-token/$STATE" -H "$AUTH" | jq '{status, connectionData}'
  ```
</CodeGroup>

<Warning>
  While the user is still authorizing, this endpoint returns **200 with an empty body** - not a 404 and not an error. Treat "no body" as "keep polling", and always bound the loop with a timeout so a user who abandons the tab doesn't hang your request.
</Warning>

## 4. Create the connection

Pass the sign-in's `state` as `oauthState` - the server looks up its own record of the sign-in and derives the connection's credentials itself.

<CodeGroup>
  ```python Python theme={null}
  created = client.connections.create(
      "outlook", "Alex's Outlook", method.authentication_id, {}, oauth_state=state,
  )
  client.connections.set_default(created.connection_id)   # optional but recommended
  ```

  ```typescript TypeScript theme={null}
  const created = await client.connections.create(
    "outlook", "Alex's Outlook", method.authenticationId, {}, { oauthState: state },
  );
  await client.connections.setDefault(created.connectionId);   // optional but recommended
  ```

  ```bash cURL theme={null}
  curl -s -X POST "$BASE/connections" -H "$AUTH" -H "Content-Type: application/json" -d '{
    "applicationSlug": "outlook",
    "connectionName": "Alex'\''s Outlook",
    "authenticationId": 0,
    "oauthState": "'"$STATE"'",
    "fields": {}
  }'
  ```
</CodeGroup>

<Warning>
  Don't build `fields` from `connectionData` - values you send for server-derived keys (`ApiKey`, `RefreshToken`) are ignored, and `ExternalOrgId` is rejected outright with 400 `INVALID_FIELD`. Omitting `oauthState` 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 isn't an existence oracle. This applies to `OAuth2` methods only; `OAuth2WithoutSignIn` (client-credentials) connectors need no state.
</Warning>

<Note>
  Requires `engini` / `@engini/sdk` / `@engini/cli` **0.21.0 or newer** (0.20.0 and earlier cannot create OAuth2 connections).
</Note>

Setting a default matters more than it looks: with one, tool calls can omit `connectionId` entirely and still resolve. Without one - and with more than one connection for that app - execution fails with `409 NO_DEFAULT_CONNECTION`.

## Doing it without writing any of this

The CLI implements this whole flow, including the agent-resumable variant:

```bash theme={null}
engini connect outlook                       # interactive, opens the browser
engini connect outlook --json                # agent: returns sign_in_url + a resume command
```

## Next

* Databases and similar apps need [object selection](/developers/cookbook/sync-database-objects) after connecting
* Non-OAuth apps skip steps 2-3 entirely - see [REST walkthrough](/developers/cookbook/rest-api-walkthrough)


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