# Create connection

> Source: https://elaichi.ai/docs/api-reference/connections/connection/createconnection/

`POST /connection`

Resource: **Connection** · API: **Connections**

## Request body

- **`connector_slug`** _(string)_
  From `GET /connector` — a slug, not an id.
- **`redirect_uri`** _(string)_
  App-relative path to return the user to after the connect flow. Absolute URLs are ignored.
- **`name`** _(string)_
  Defaults to a connector-derived name.
- **`shares`** _(array<object>)_
  Grants to create alongside the connection. Omit, or send `[]`, for a private connection.
  - **`grantee_type`** _(string)_
    `user` and `team` grants target one `grantee_id`; `org` applies to every member of the organization and takes no id.
    Allowed: `user`, `team`, `org`
  - **`grantee_id`** _(string,null)_
    User id (`usr_…`) or team id (`team_…`). Omit or send null for `grantee_type: "org"`.
  - **`level`** _(string)_
    Same ladder for every shareable resource, low to high. `view` — see it exists, read its metadata/config; cannot exercise it. `use` — `view` + exercise it, resource-specific: run tools through a connection; execute a toolbox's tools through its bound connections (this DELEGATES — the caller runs through each entry's pinning editor's own authority, not necessarily their own); stamp a new toolbox by copying a template's entries; create connections from a connector. `edit` — `use` + change its settings, entries and its own grants.
    Allowed: `view`, `use`, `edit`

## Response body

- **`connection`** _(object)_
  - **`id`** _(string)_
    Connection id (`conn_…`).
  - **`restricted_inherited_from`** _(object,null, required)_
    Set when this connector is restricted for the caller ONLY because the connector it was forked from is — a fork inherits its upstream's restrictions on purpose (forking a blocked connector is not a bypass), but nothing was set on the fork itself, so this says where the block came from. Null when not restricted, when a rule names this connector directly, or when the lineage could not be walked. Asking for access still targets THIS connector: approving the fork's own request lifts the inherited block for that fork only. Sibling of `restricted_by` (which stays the precedence layer); computed server-side in the same batch as the restriction evaluation — clients must not derive it from lineage.
  - **`connector_slug`** _(string)_
    Which connector this account is on.
  - **`name`** _(string)_
  - **`owner_user_id`** _(string)_
    Owning member (`usr_…`); calls run as this identity. Owner is implicit `edit`, plus delete and transfer — the two things no share confers. Move it with `POST /connection/{id}/transfer`. This is who is ACCOUNTABLE for the connection now, which after a transfer is not who authenticated it — see `connected_by_user_id`.
  - **`connected_by_user_id`** _(string,null)_
    Who AUTHENTICATED this account (`usr_…`) — the member whose third-party credential the vault actually holds. Written once at creation and never rewritten. Distinct from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and deliberately does NOT touch the vault account, so after a transfer these are two different people and only this one may be rendered as "Connected by". **Null means NOT RECORDED, never "nobody"** — every connection created before this field existed is null, because nothing server-side could tell a never-transferred row (where `owner_user_id` would have been the honest answer) from a transferred one, and a fabricated attribution is worse than an absent one. Render nothing for a null.
  - **`connected_by_label`** _(string,null)_
    Display name for `connected_by_user_id`, resolved server-side one batch per page (name, else email). Present on every branch that returns a connection, commands included. A null HERE beside a NON-null `connected_by_user_id` is a fact about the person, not a failed lookup: they have left the organization, and clients render "Former member" — the same word the `GET /audit-log` actor summary uses. Compare the two fields, never one alone: a null id means "not recorded", a null label means "departed".
  - **`connector`** _(object, required)_
    The connector's display data, resolved server-side in one batch per page so a client needs no `GET /connector/{slug}` per distinct connector. On `GET /connection` rows and `GET /connection/{id}` (which adds `can_view_connector`, `available` and `auth_mode`, and always states `single_redirect_auth`); absent from the command responses, which cannot change a connection's connector.
    - **`label`** _(string)_
      Catalog display name; the slug itself when the catalog cannot resolve it.
    - **`logo`** _(string,null)_
      Logo URL; null when the connector has none.
    - **`upstream_slug`** _(string)_
      Present only on a fork (an org-owned connector with a lineage): the connector it was forked from. Omitted when the caller has no reach on the fork or on a private upstream (naming it would disclose its slug), and when the lineage cannot be read.
    - **`single_redirect_auth`** _(boolean)_
      The connector-row hint of the same name: connecting is most likely a single provider sign-in, so a client reserves a popup inside the Reconnect click. Always stated on `GET /connection/{id}` (false when `can_view_connector` is). On a list row it is stated only where the caller could open the connector, and ABSENT otherwise, which reads as unknown, never false.
    - **`available`** _(boolean)_
      False only when the catalog positively does not have the connector (staff unpublished or deleted a platform connector): the connection is kept but nothing can work until it returns. On `GET /connection/{id}` it is `true` when the connector resolves; on a `GET /connection` row it is never `true`, only `false` or ABSENT (one batched lookup per page). ABSENT too when the catalog could not answer, so a blip never reads as an unpublished connector.
    - **`auth_mode`** _(string)_
      `GET /connection/{id}` only, remote MCP connectors only, and only when `can_view_connector`: how the connector signs in, so a screen can say "No sign-in" instead of "API key".
      Allowed: `oauth_auto`, `oauth_client`, `custom`, `none`
    - **`can_view_connector`** _(boolean)_
      `GET /connection/{id}` only. Whether `GET /connector/{slug}` would answer this caller: false for a connector the catalog no longer resolves, without `connector:view`, and for an org-owned connector nobody shared with them (it 404s). Link to the connector only when true.
  - **`saffron_account_id`** _(string,null)_
    Vault account holding the credential. Null until the connect flow completes.
  - **`status`** _(string)_
    `pending` = created but not yet authenticated (open `connect_url`); `active` = usable; `needs_reauth` = the credential expired or was revoked upstream, call reconnect; `disconnected` = no longer usable. Refreshed lazily against the vault when connections are read.
    Allowed: `pending`, `active`, `needs_reauth`, `disconnected`
  - **`last_error`** _(string,null)_
  - **`access`** _(string)_
    The caller's relationship to this connection: `owner`, or a granted `view`/`use`/`edit` level. There is no oversight fallback for connections — this is never `"oversight"`; a connection the caller neither owns nor holds a grant on is not visible to them at all, whatever organization permission they hold.
  - **`owner`** _(object, required)_
    Who `owner_user_id` is, resolved server-side in the same batch as `connected_by_label` (one per page). Same audience as a toolbox's `owner`: present on a connection the caller does NOT own, absent on their own. On every branch that returns a connection — list, detail and the command responses, so a transfer names the new owner without a reload. `name`/`email` are absent for an owner who has left the organization ("Former member").
    - **`id`** _(string)_
      User id (`usr_…`) — same value as `owner_user_id`.
    - **`name`** _(string)_
      Display name, else the email.
    - **`email`** _(string)_
    - **`avatar_url`** _(string,null)_
      The person's picture — the same one the console's user menu draws: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived server-side from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load.
  - **`shares`** _(array<object>)_
    Present only when the caller may see the ACL: owner or `edit` access. A `view`/`use` recipient is never told who else the connection is shared with. Absent (not empty) when the caller may not see it.
    - **`id`** _(string)_
      ACL entry id — the `:aclId` a `DELETE .../share/{aclId}` call takes.
    - **`resource_type`** _(string)_
      Allowed: `connection`, `toolbox`, `template`, `connector`, `file`
    - **`resource_id`** _(string)_
      The shared resource's id (a connector's slug, for that type).
    - **`grantee_type`** _(string)_
      `user` and `team` grants target one `grantee_id`; `org` applies to every member of the organization and takes no id.
      Allowed: `user`, `team`, `org`
    - **`grantee_id`** _(string,null)_
      User id (`usr_…`) or team id (`team_…`). Null for a `grantee_type: "org"` grant.
    - **`level`** _(string)_
      Same ladder for every shareable resource, low to high. `view` — see it exists, read its metadata/config; cannot exercise it. `use` — `view` + exercise it, resource-specific: run tools through a connection; execute a toolbox's tools through its bound connections (this DELEGATES — the caller runs through each entry's pinning editor's own authority, not necessarily their own); stamp a new toolbox by copying a template's entries; create connections from a connector. `edit` — `use` + change its settings, entries and its own grants.
      Allowed: `view`, `use`, `edit`
    - **`created_at`** _(string)_
    - **`updated_at`** _(string)_
  - **`created_at`** _(string)_
  - **`updated_at`** _(string)_
  - **`can_share`** _(boolean)_
    Whether the caller may share this connection — owner, `edit` access, or a team admin on a team-granted one. No `connection:manage` fallback. Computed server-side (CLAUDE.md's API-first rule); the console reads this rather than re-deriving it from `owner_user_id`/permissions.
  - **`can_manage`** _(boolean)_
    Present on every list and detail row. Whether the caller may edit this connection's own settings — owner, or `edit` access. No `connection:manage` fallback. Computed server-side; same `can_manage` name used by toolbox and connector for the identical capability.
  - **`can_transfer`** _(boolean)_
    Present on every list and detail row. Whether the caller may transfer or delete this connection — owner, full stop. No `connection:manage` fallback. Deliberately narrower than `can_manage`: an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it.
  - **`can_revoke_share`** _(boolean)_
    `can_share`, verbatim — a THIRD formula, deliberately not an alias for `can_manage`: an `edit` grantee whose role omits `connection:share` (Auditor, Guest, any custom role) can reconfigure or reconnect the connection but was never meant to grant or revoke someone else's access to it, so `can_manage: true` on that row must not imply this control too. Mirrors `DELETE /connection/{id}/share/{aclId}`'s own gate exactly. Present on every list and detail row.
  - **`access_summary`** _(object)_
    Present only when `can_share` is true. Bounded by design — page `GET /connection/{id}/share` for the grants themselves.
    - **`org_level`** _(string,null)_
      Allowed: `view`, `use`, `null`
    - **`team_count`** _(integer)_
    - **`user_count`** _(integer)_
    - **`total`** _(integer)_
      Every grant, the org-wide one included.
    - **`preview`** _(array<object>)_
      At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.
      - **`grantee_type`** _(string)_
        Allowed: `user`, `team`, `org`
      - **`grantee_id`** _(string,null)_
      - **`level`** _(string)_
        Allowed: `view`, `use`
      - **`name`** _(string,null)_
        Display name; null for org grants and for grantees no longer in the org.
  - **`access_via`** _(string)_
    How the CALLER reaches this connection — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the connection reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means "no source to name", never "not permitted", and never an implied `org`.
    Allowed: `owner`, `direct`, `team`, `org`
  - **`access_via_team`** _(object)_
    Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this connection through — one of their own teams, never a disclosure about anybody else.
    - **`id`** _(string)_
      Team id (`team_…`).
    - **`name`** _(string,null)_
      Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries.
- **`connect_url`** _(string)_
  Where the USER authenticates with the provider. Hand it to them as a link — Elaichi never accepts a password or API key for a third-party system through this API. Single-use and short-lived; mint another with reconnect. On completion the provider returns to `GET /connection/{id}/callback` and the status becomes `active`.
- **`direct_connect_url`** _(string,null)_
  The same one-time session as `connect_url`, pointed at Saffron's sign-in start route (`/connect/<slug>?connect_session&installed_integration_id&authentication_method`) instead of the hosted connect page. That route redirects to the provider, so the person skips the page. Non-null only when the connector has exactly one auth method, that method is a redirect (OAuth), and it has no form fields or `permissions_text` — the cases where the hosted page would show nothing but a Connect button. Open it top-level in a popup or a new tab, never in an iframe. `null` means use `connect_url`; so does a lookup that failed or took longer than 1.5 seconds.

## Code examples

### curl

```bash
curl -X POST 'https://api.elaichi.ai/connection' \
  -H 'Authorization: Bearer $ELAICHI_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"connector_slug":"your_connector_slug","redirect_uri":"your_redirect_uri","name":"your_name","shares":[]}'
```

### JavaScript

```javascript
const body = {
  "connector_slug": "your_connector_slug",
  "redirect_uri": "your_redirect_uri",
  "name": "your_name",
  "shares": []
};

const response = await fetch('https://api.elaichi.ai/connection', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + process.env.ELAICHI_API_TOKEN,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(body),
});

const data = await response.json();
console.log(data);
```

### Python

```python
import os
import requests

url = "https://api.elaichi.ai/connection"
headers = {
    "Authorization": f"Bearer {os.environ['ELAICHI_API_TOKEN']}",
    "Content-Type": "application/json",
}
payload = {
    "connector_slug": "your_connector_slug",
    "redirect_uri": "your_redirect_uri",
    "name": "your_name",
    "shares": []
}

response = requests.post(url, headers=headers, json=payload)
print(response.json())
```
