This endpoint connects one of your customer's own content sources: their mailbox, their Drive, their YouTube channel, another website. It is driven from your product, over the API. There is no Context Link screen for your customer to visit.

Sources are addressed by a **uid you choose**, not by type, because one customer can hold several of the same kind (two mailboxes, three channels). The uid is your own word for the source. The write verb is a `PUT` upsert, so a retried request is always safe.

```bash
curl -X PUT "https://context-link.ai/api/v1/api_accounts/tf-cust-8841/connections/acme-yt-main" \
  -H "Authorization: Bearer $PROVISIONING_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "connection_type": "youtube", "handle": "@acmecorp" }'
```

```json
{
  "connection": {
    "id": 9013,
    "uid": "acme-gdrive",
    "connection_type": "google_doc",
    "name": "GoogleDoc",
    "status": "redirect_required",
    "auth_status": "pending_auth",
    "sync_status": "unsynced",
    "redirect_url": "https://accounts.google.com/o/oauth2/v2/auth?...",
    "expires_at": "2026-09-13T12:15:00Z"
  }
}
```

## Request fields

| Field | Type | Description |
|---|---|---|
| `uid` | string | Required, in the path. The customer id you provisioned the account with. An unknown uid is a `404`. |
| `connection_uid` | string | Required, in the path. The uid you choose for this source: `a-z`, `0-9`, hyphen, underscore or dot, 1 to 255 characters, unique per customer. It is the address for every later call on this connection. |
| `connection_type` | string | Required, in the body. Which kind of source this is. The accepted values and the setup fields each one needs are in the table below. |

You choose which types your organisation can offer on the [Connection types screen](/docs/white-label/customer-sources). A type you have not turned on is a `403 connection_type_not_activated`. An OAuth type turned on without its complete partner configuration is a `409 partner_credentials_required`, on this call and on [re-authorising](#re-authorising-a-dead-connection), until you add it there.

### Setup fields by type

Every type takes `connection_type` plus the fields below.

| `connection_type` | Required fields | Authorisation |
|---|---|---|
| `site` | `site_domain` (e.g. `https://acme.com`) | None. Connects directly, one website per customer. |
| `email` | `email_address`, `imap_password`, `imap_server`; optional `imap_port` (defaults to 993) | None. Credentials are checked on the first sync. |
| `youtube` | `handle` (e.g. `@acmecorp`) | None. The handle is resolved to a channel before the connection is created. |
| `monday` | `api_token` (pasted by the customer); optional `workspace_id` | None. The token is resolved to a workspace before the connection is created. If it can see more than one workspace, send `workspace_id`. |
| `webmention` | `brand_name`, `web_domain` | None. Web Mentions of the brand across search and news. |
| `google_doc` | `return_url` | Redirect to Google consent, then `selection_required`. Your UI opens Google Picker and commits the files the customer selected. |
| `one_drive` | `return_url` | Redirect. Microsoft consent screen. |
| `notion` | `return_url` | Redirect. Notion consent screen. |
| `basecamp` | `return_url`, `project_url` (e.g. `https://3.basecamp.com/1234567/projects/9876543`) | Redirect. Basecamp consent screen. One project per connection. The project has to belong to the account the customer authorises, or the connection comes back `auth_failed`. |

A missing or invalid field is a `422` carrying a per-field `errors` object, so you can put the message next to the input the customer filled in:

```json
{
  "error": "Setup fields are invalid.",
  "error_code": "invalid_setup_fields",
  "errors": { "handle": "is required" }
}
```

Mail connections default to the `INBOX` folder. Everything a customer pushes through the [Custom Connections API](/docs/white-label/create-connection) is unaffected by any of this.

## Response fields

Every successful call returns a single `connection` object.

| Field | Type | Description |
|---|---|---|
| `connection` | object | The connection that was created or replayed. |
| `connection.id` | integer | Context Link's own id for the connection. You address it by the uid you chose, so you never need to store this. |
| `connection.uid` | string | The uid you chose for this source, echoed back. |
| `connection.connection_type` | string | The type, in the same form you sent it (`google_doc`, `youtube`). |
| `connection.name` | string | The type's own display name, such as `GoogleDoc` or `Youtube`. Never something you supply. |
| `connection.status` | string | Where the connection has got to: `redirect_required`, `selection_required`, `pending` or `connected`. This is the value to branch on, and it is covered below. |
| `connection.auth_status` | string | `pending_auth`, `pending_selection`, `authorised`, `auth_failed` or `disconnected`. A type that needs no consent screen is `authorised` from the moment it is created. |
| `connection.sync_status` | string | `unsynced`, `syncing`, `synced` or `error_syncing`. A connection that has not yet attempted a sync is `unsynced`. |
| `connection.redirect_url` | string | The consent-screen URL to send the customer to. Present **only** when `status` is `redirect_required`. On any other status the key is left out of the response rather than returned as `null`. |
| `connection.expires_at` | timestamp | When that `redirect_url` stops working, 15 minutes after it was minted. Present only alongside `redirect_url`, on the same terms. |

Two mailboxes on one customer are told apart by their `uid`. That is your own label, and the only one your customer will recognise.

## Branch on `status`, not on `redirect_url`

Every response carries one `status` discriminator with **four** values. Branching on "is `redirect_url` present" loses the distinction between file selection and syncing.

| `status` | What it means | What to do |
|---|---|---|
| `redirect_required` | The type needs the customer's consent at the provider. `redirect_url` and `expires_at` are present. | Send the customer's browser to `redirect_url` before `expires_at`. |
| `selection_required` | Google consent succeeded, but no Drive files have been committed yet. | Fetch the Picker configuration from your backend, open Google Picker in the customer's browser, then commit the selected file ids. |
| `pending` | Connected and authorised, first sync still running. | Nothing. Wait for the [`connection.authorised` webhook](/docs/white-label/webhooks) or poll. |
| `connected` | Authorised and synced at least once. | Query it. |

`201` on create, `200` on a replay of the same uid. A replay ignores body drift, exactly like [`POST /api_accounts`](/docs/white-label/create-account). There is one exception. Changing the `connection_type` of a live uid is a `409 connection_type_immutable`, because the content already indexed under it would be meaningless as another type. Delete and recreate instead.

A replay of a connection still in `pending_auth` re-mints a **fresh** `redirect_url`, because the usual reason you are retrying is that the customer abandoned the first consent screen.

## The redirect flow

For the four OAuth types the customer has to say yes at the provider, so the flow is a browser round trip that starts and ends in **your** product:

1. You `PUT` the connection with a `return_url`. You get back `status: "redirect_required"` and a `redirect_url`.
2. You send the customer's browser to `redirect_url`. They consent at Google / Microsoft / Notion / Basecamp, under **your** brand and using your own OAuth client.
3. The provider returns them to Context Link, which exchanges the code and redirects the browser to your `return_url`. Non-Google providers append `?connection_status=connected`; Google Drive appends `?connection_status=selection_required`; failures append `?connection_status=failed`. Your existing query string is preserved.
4. For Google Drive, complete the Picker flow below. The authoritative record is the [`connection.authorised` or `connection.auth_failed` webhook](/docs/white-label/webhooks). Google does not emit `connection.authorised` until the selected files have been committed.

`return_url` is required for these types, and it must be a publicly routable `http(s)` URL. The same host rules apply as for a [webhook target](/docs/white-label/webhooks#ssrf-constraints-on-the-webhook-url). A private or loopback address is a `422` at request time.

The `redirect_url` is signed and expires **15 minutes** after it is minted. An expired or tampered link shows the customer a plain "this link has expired, start again" page rather than bouncing them anywhere. At that point nothing proves where they came from.

> **Google Drive uses the non-sensitive `drive.file` scope.** Picker grants access only to files the customer chooses, so Drive file access does not trigger Google's restricted-scope verification or annual security assessment. You still own the Google Cloud consent-screen branding and publishing configuration for your app.

## Complete Google Drive with Picker

When the return URL receives `connection_status=selection_required`, your backend fetches the short-lived Picker configuration with the provisioning token. Do not call this endpoint directly from the browser and do not log or cache its response: it contains the customer's Google access token.

```bash
curl "https://context-link.ai/api/v1/api_accounts/tf-cust-8841/connections/acme-gdrive/picker" \
  -H "Authorization: Bearer $PROVISIONING_TOKEN"
```

The response contains `access_token`, `developer_key`, `app_id`, `max_items` and the current `selected_files`. Pass the first three to Google Picker in the authenticated customer's browser, enable multi-select, and restrict the view to the supported Google Docs, Sheets, PDF, text and spreadsheet file types.

After Picker returns, send only its file ids back through your backend:

```bash
curl -X PATCH "https://context-link.ai/api/v1/api_accounts/tf-cust-8841/connections/acme-gdrive/selected_files" \
  -H "Authorization: Bearer $PROVISIONING_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "file_ids": ["1abc...", "2def..."] }'
```

Context Link verifies that the OAuth grant can read every id and that every file has a supported type. A valid commit replaces the previous selection, removes indexed documents that were deselected, sets `auth_status: "authorised"`, starts a sync and returns `status: "pending"`. Cancelling Picker changes nothing: keep the connection at `selection_required` and offer **Choose files** again.

## Re-authorising a dead connection

Refresh tokens die: the customer changes their password, revokes access at the provider, or the grant simply expires. That sets `auth_status: "auth_failed"` and fires `connection.auth_failed`. **Nothing is deleted.** The indexed content stays, because the usual cause is transient. Re-authorising should not mean re-crawling from nothing.

```bash
curl -X POST "https://context-link.ai/api/v1/api_accounts/tf-cust-8841/connections/acme-gdrive/reauthorise" \
  -H "Authorization: Bearer $PROVISIONING_TOKEN"
```

You get a fresh `redirect_url` to send the customer to. Pass a `return_url` to override the one stored at create time. On a type that has no authorisation to renew this is a `422 connection_type_not_reauthorisable`.

Re-authorising Google Drive returns to `selection_required`, even if the connection previously used the legacy whole-Drive integration. The customer must choose files before syncing resumes.

## Limits

| Limit | Value |
|---|---|
| Connections per customer, all types | 10, and the memory connection is exempt |
| Connections of one type per customer | 5, counting toward the 10 |
| Websites per customer | 1, counting toward the 10 |
| Custom connections per customer | 6, counting toward the 10 |
| Posts per sync per connection | 500 by default; lower where a sync is expensive: 30 for YouTube, 50 for Web Mentions |
| Write rate limit | 5 per 10s per token **and** per organisation |

**10 is the binding ceiling.** The per-type numbers are maxima within it, not slices that add up to it. A customer can hold 6 custom connections or 5 mailboxes, but not both, because the total still has to come in at 10.

A customer's memory connection is the one exception: it never counts toward the 10, and it has no cap of its own to worry about.
