> ## Documentation Index
> Fetch the complete documentation index at: https://docs.case.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect an Account

> Take a user through hosted OAuth and handle the return safely

Connecting a provider account takes three steps:

1. **Create a pending connection** and get a one-time `connect_url`
2. **Send the user to it**—they consent at the provider
3. **Handle the return** at your own URL, which now carries the connection id

Your server never sees an authorization code, an access token, or a refresh token.

***

## Step 1: Create the connection

```bash title="Endpoint" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
POST /connectors/v1/connections
```

```bash title="cURL" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
curl -X POST https://api.case.dev/connectors/v1/connections \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "clio",
    "return_url": "https://app.example.com/integrations/callback"
  }'
```

```json title="Response" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "connection_id": "conn_abc123",
  "connect_url": "https://api.case.dev/connectors/v1/connect/QTd2…",
  "expires_at": "2026-01-15T10:40:00Z"
}
```

| Field        | Required | Description                                                   |
| ------------ | -------- | ------------------------------------------------------------- |
| `provider`   | yes      | `clio` or `gdrive`                                            |
| `return_url` | yes      | Where the user lands after consent                            |
| `scope_tier` | no       | Provider permission tier. Omit to use the provider's default. |

The connection exists immediately with status `pending`. It becomes `healthy` only after the user finishes consent.

### return\_url rules

The URL is validated when you create the connection, not when the user comes back. A URL that fails these checks returns `400`.

* It must be **absolute**.
* It must use **https**. `localhost` and `127.0.0.1` over http are accepted outside production.
* It must not embed **credentials** (no `user:password@host`).

Case.dev appends a query parameter to whatever you supply, preserving your existing ones.

<Warning>
  Treat `return_url` as a redirect target you own. Do not build it from unvalidated user input, and do not point it at a URL that forwards onward using a parameter an attacker controls.
</Warning>

### Scope tiers

`scope_tier` selects a provider permission level. Both current providers expose a single tier (`clio.us`, `drive`), so omit the field and you get it by default. The tier is signed into the `connect_url`, so a user cannot broaden their own consent by editing the query string before they click through.

## Step 2: Send the user to connect\_url

Redirect the browser to `connect_url`. Case.dev builds the provider authorization URL, attaches HMAC-signed state and a PKCE challenge, and forwards the user to the provider's consent screen.

<Info>
  `connect_url` is **one-time and expires in 10 minutes**. An expired link returns `410`; a link that has already been used returns `404`. Generate it when the user is about to click, not when the page loads.
</Info>

## Step 3: Handle the return

After consent, the provider redirects to Case.dev, which exchanges the code, stores the encrypted tokens, and sends the user to your `return_url`.

```text title="Success" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
https://app.example.com/integrations/callback?connection_id=conn_abc123
```

```text title="Failure" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
https://app.example.com/integrations/callback?error=connector_scope_not_enabled
```

Branch on which parameter is present.

| `error` value                 | Meaning                                                                                           | What to do                                                                |
| ----------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `access_denied`               | The user declined consent at the provider                                                         | Offer to try again                                                        |
| `provider_error`              | The provider returned an error before issuing a code                                              | Retry; escalate if it persists                                            |
| `missing_code`                | The provider redirected without an authorization code                                             | Retry                                                                     |
| `connector_scope_not_enabled` | The provider is not enabled for your organization, or the user granted fewer scopes than required | Contact support, or ask the user to reconnect and accept the full consent |
| `oauth_flow_inactive`         | The pending connection was disconnected, superseded, or did not match the initiating session      | Start a fresh connection                                                  |
| `oauth_exchange_failed`       | The code exchange with the provider failed                                                        | Retry; escalate if it persists                                            |

On success, confirm the connection is usable before you show it as connected:

```bash title="cURL" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
curl https://api.case.dev/connectors/v1/connections/conn_abc123 \
  -H "Authorization: Bearer $CASEDEV_API_KEY"
```

```json title="Response" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "id": "conn_abc123",
  "provider": "clio",
  "status": "healthy",
  "enabled": true,
  "scope_tier": "clio.us",
  "account": {
    "external_id": "3915522",
    "email": "casey@example.com",
    "display_name": "Casey Rivera"
  },
  "created_at": "2026-01-15T10:30:00Z",
  "updated_at": "2026-01-15T10:31:04Z"
}
```

<Warning>
  Do not trust `connection_id` from the query string on its own. Anyone can type it. Fetch the connection server-side with your API key and confirm it belongs to the tenant whose session you are serving.
</Warning>

## Connection statuses

| Status            | Meaning                                      | What to do                                                  |
| ----------------- | -------------------------------------------- | ----------------------------------------------------------- |
| `pending`         | Created, consent not finished                | Nothing. Once `connect_url` expires it cannot be completed. |
| `healthy`         | Usable                                       | Proceed.                                                    |
| `reauth_required` | Provider authorization lapsed beyond refresh | Run the connect flow again.                                 |
| `revoked`         | Unlinked, tokens deleted                     | Create a new connection.                                    |
| `throttled`       | Provider is rate limiting this account       | Back off. It clears on its own.                             |

Subscribe to [`connection.reauth_required` and `connection.revoked`](/events/event-types) instead of polling.

## Reconnecting

Reconnection is the same flow. Create a new connection for the same provider and account, and send the user through `connect_url` again. When the account matches an existing connection, the callback returns you to `return_url` carrying that **existing** `connection_id`—so links built on it keep working, with their ledgers and cursors intact.

This is what you should offer when a connection goes `reauth_required`. Nothing re-imports.

## Disconnecting

```bash title="Endpoint" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
DELETE /connectors/v1/connections/{id}
```

```bash title="cURL" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
curl -X DELETE https://api.case.dev/connectors/v1/connections/conn_abc123 \
  -H "Authorization: Bearer $CASEDEV_API_KEY"
```

Tokens are revoked at the provider and deleted. Documents already imported into vaults stay where they are.

To remove those documents too, add `?purge=true`. The purge runs as a durable background workflow and returns `202` rather than `204`.

<Warning>
  `purge=true` deletes vault documents that this connection's import links brought in. It is not reversible—confirm with the user before you call it.
</Warning>

## Turning a provider off without disconnecting

```bash title="Endpoint" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
PATCH /connectors/v1/connections
```

This stops new runs and scheduled syncs for one provider across the whole organization or installation. Credentials, links, and imported files are preserved, and runs already in flight are not interrupted. Because it is organization-wide, the body must include `confirm_organization_wide: true`.

## Next steps

<CardGroup>
  <Card title="Import & Export" href="/connectors/transfers">
    Browse the connected account and move files into a vault
  </Card>

  <Card title="Installations" href="/connectors/installations">
    Scope connections per customer tenant
  </Card>

  <Card title="Errors & Recovery" href="/connectors/errors">
    What each failure code means and which ones to retry
  </Card>
</CardGroup>
