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

### 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. Google consent screen. Indexes the whole of the customer's My Drive. There is no folder to choose, because you never see inside it. |
| `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`, `pending` or `connected`. This is the value to branch on, and it is covered below. |
| `connection.auth_status` | string | `pending_auth`, `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 **three** values. Branching on "is `redirect_url` present" mishandles the third.

| `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`. |
| `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` with `?connection_status=connected` or `?connection_status=failed` appended. Your existing query string is preserved.
4. The authoritative record is the [`connection.authorised` or `connection.auth_failed` webhook](/docs/white-label/webhooks), which fires whatever the browser does.

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

> **Provider verification is yours to obtain.** The consent screen shows your brand because it is your OAuth client. That also means the provider's unverified-app caps and review processes apply to you: Google restricts Gmail and Drive scopes behind an annual third-party security assessment, and an unverified client is limited to a small number of users. Start that process before you launch the type to your fleet.

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

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