> ## 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](/examples/rest-walkthrough).
</Note>

## 1. Find the OAuth method

Each application exposes one or more authentication methods. You need the `authenticationId` of one where `requiresOAuthSignIn` is true.

<CodeGroup>
  ```python Python theme={null}
  detail = client.applications.get("outlook")
  method = 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.requiresOAuthSignIn)!;
  ```

  ```bash cURL theme={null}
  curl -s "$BASE/applications/outlook" -H "$AUTH" \
    | jq '.authenticationMethods[] | select(.requiresOAuthSignIn) | {authenticationId, authenticationName}'
  ```
</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

Build the credential fields from **`connectionData`** only.

<CodeGroup>
  ```python Python theme={null}
  props = token.connection_data.additional_properties or {}
  fields = {k: v if isinstance(v, str) else str(v) for k, v in props.items() if v is not None}

  created = client.connections.create("outlook", "Alex's Outlook", method.authentication_id, fields)
  client.connections.set_default(created.connection_id)   # optional but recommended
  ```

  ```typescript TypeScript theme={null}
  const fields = Object.fromEntries(
    Object.entries(token.connectionData)
      .filter(([, v]) => v != null)
      .map(([k, v]) => [k, typeof v === "string" ? v : String(v)]),
  );

  const created = await client.connections.create("outlook", "Alex's Outlook", method.authenticationId, fields);
  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,
    "fields": { "...": "values from connectionData" }
  }'
  ```
</CodeGroup>

<Warning>
  Use `connectionData`, **not** `tokenData`. `tokenData` is the raw provider token (`access_token`, `token_type`...) and the API rejects unknown connector fields. `connectionData` is the set Engini derived for this connector.
</Warning>

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](/examples/object-sync) after connecting
* Non-OAuth apps skip steps 2-3 entirely - see [REST walkthrough](/examples/rest-walkthrough)
