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

# Import & Export

> Browse a provider, create a link, and follow its progress

Once an account is [connected](/connectors/oauth), moving files takes three calls:

1. **Browse** to let the user choose a folder
2. **Transfer** to create the link and start a run
3. **Poll or subscribe** to follow progress

***

## Step 1: Browse the provider

Browse returns one level at a time. With no parameters you get the top-level containers—matters for Clio, My Drive and shared drives for Google Drive.

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

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

```json title="Response" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "items": [
    {
      "id": "1a2b3c",
      "name": "Smith v. Hospital",
      "kind": "matter",
      "browse_ref": { "kind": "folder", "folder_id": "1a2b3c" },
      "import_ref": { "folder_id": "1a2b3c" }
    }
  ],
  "cursor": null
}
```

To descend, pass back the `browse_ref` fields from the item the user picked:

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

| Parameter   | Description                                                 |
| ----------- | ----------------------------------------------------------- |
| `container` | Container id—a shared drive or a matter                     |
| `parent`    | Folder id to list inside                                    |
| `query`     | Provider-supported search text, scoped to the current level |
| `cursor`    | Continuation from a previous page                           |
| `page_size` | Up to 1000                                                  |

Item `kind` is one of `my_drive`, `shared_drive`, `matter`, `folder`, or `file`. The enum also reserves `site` and `document_library` for providers that group drives under sites; neither current provider returns them, and the `site` query parameter is likewise unused.

<Warning>
  `browse_ref` and `import_ref` are **opaque references**. Pass them back exactly as received.

  Never construct one from a provider id supplied by a browser, and never accept one as user input in your own API. These values are how the service decides what a caller is allowed to reach.
</Warning>

Some items are browsable but not importable, and some are the reverse. When an item carries an `import_ref` that differs from its `browse_ref`, use `import_ref` as the link's `remote`.

<Info>
  Browsing needs a connection scope that permits server-side listing. If it does not, you get `403 provider_scope_insufficient`—the user consented at a narrower tier than browsing requires.
</Info>

## Step 2: Create the link and run it

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

```bash title="cURL" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
curl -X POST https://api.case.dev/connectors/v1/transfer \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "direction": "import",
    "connection_id": "conn_abc123",
    "remote": { "folder_id": "1a2b3c", "path": "Smith v. Hospital" },
    "vault_id": "vault_abc123",
    "policy": {
      "deletes": "preserve",
      "filters": { "max_size_bytes": 209715200 }
    }
  }'
```

```json title="Response (202)" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "links": [{ "id": "link_55aa", "direction": "import", "mode": "once", "state": "running" }],
  "run": { "started": true }
}
```

A link's identity is `(connection_id, direction, remote, vault_id)`. The first call creates it and runs a full backfill. Every later call with the same four values runs an **incremental diff** against the ledger—so "Re-import" in your UI is this exact request again.

If a run is already active, the existing run is returned instead of an error. Double-clicking is safe.

| Field                  | Description                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------- |
| `direction`            | `import`, `export`, or `both`                                                             |
| `remote.folder_id`     | Required. From the browse `import_ref` or `browse_ref`.                                   |
| `remote.resource_type` | Provider-declared collection. Omit for the provider default.                              |
| `vault_id`             | Destination vault. Must be granted if you use [installations](/connectors/installations). |
| `matter_id`            | Optional association with a [matter](/matters)                                            |
| `run_mode`             | `auto` (default) or `full_reconcile` to force a complete re-scan                          |
| `export_destination`   | For `direction: "both"`. Defaults to a `CaseMark Output` subfolder under `remote`.        |

### When a run does not start

`run.started` can be `false` with a `reason`. This is not an error:

| Reason               | Meaning                                                           |
| -------------------- | ----------------------------------------------------------------- |
| `already_running`    | A run for this link is in flight                                  |
| `stale_run_recovery` | A previous run is being reconciled first                          |
| `ingestion_backlog`  | The vault ingestion queue is saturated; `backlog` gives the depth |

The link is created either way. Poll it and the run will begin.

## Keeping a folder current

For a standing promise instead of a one-shot, use `/sync-link`. Same body, minus `run_mode`.

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

It backfills now, then stays current on a sweep cycle. Due links run incrementally about every 15 minutes. Once a day the sweep runs a full reconcile instead, which catches deletions and moved folders that per-file change feeds cannot report.

An existing `once` link is upgraded in place, keeping its ledger and cursor. Nothing re-imports.

To step back down or pause:

```bash title="cURL" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
curl -X PATCH https://api.case.dev/connectors/v1/links/link_55aa \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "once" }'
```

`PATCH` also accepts `state` (`paused` or `ready`) and a replacement `policy`.

## Step 3: Follow progress

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

```json title="Response" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "id": "link_55aa",
  "direction": "import",
  "mode": "once",
  "automation_status": "manual_only",
  "state": "running",
  "vault_id": "vault_abc123",
  "counts": {
    "synced": 118,
    "pending": 12,
    "queued": 9,
    "processing": 3,
    "failed": 1,
    "skipped": 4,
    "tombstoned": 0
  },
  "active_run": {
    "id": "run_77cd",
    "trigger": "manual",
    "mode": "backfill",
    "status": "running",
    "stats": { "listed": 135, "added": 118, "updated": 0, "deleted": 0, "skipped": 4, "failed": 1, "bytes": 884213504 },
    "started_at": "2026-01-15T10:32:00Z",
    "finished_at": null,
    "error": null
  },
  "last_run": null
}
```

### Link states

| State      | Meaning                                                           |
| ---------- | ----------------------------------------------------------------- |
| `ready`    | Idle, will run when asked or swept                                |
| `running`  | A run is executing                                                |
| `active`   | Synced and current                                                |
| `paused`   | Suspended—by you, or by a lost grant                              |
| `orphaned` | Its provider folder or vault is gone. `orphan_reason` says which. |
| `error`    | The last run failed in a way that needs attention                 |

### Run statuses

`running`, `completed`, `completed_with_errors`, or `failed`. `completed_with_errors` means the run finished and some files failed—check the ledger for which.

<Info>
  `counts.pending` is the umbrella for everything not yet settled. `queued` and `processing` are subsets of it, so do not add them together.
</Info>

## Per-file results

Run stats tell you how many. The ledger tells you which.

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

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

Each row carries the provider item, the vault object it became, its path, its content version, its state, and an error when one applies.

| State          | Meaning                                               |
| -------------- | ----------------------------------------------------- |
| `pending`      | Recorded, not started                                 |
| `transferring` | Bytes in flight                                       |
| `ingesting`    | In the vault, being OCR'd and indexed                 |
| `synced`       | Done                                                  |
| `skipped`      | Excluded by policy—size or MIME filter                |
| `failed`       | Did not transfer. The row carries the reason.         |
| `tombstoned`   | Source file is gone and the delete policy was applied |

Filter by `state` and page with `cursor`.

## Events instead of polling

For anything longer than a few seconds, subscribe rather than poll. These [events](/events/event-types) carry connector progress:

| Event                        | Fires when                                                    |
| ---------------------------- | ------------------------------------------------------------- |
| `run.completed`              | A run finished, with full stats—possibly with per-file errors |
| `run.failed`                 | A run failed before completing                                |
| `link.objects_changed`       | A run changed the set of synced objects                       |
| `link.orphaned`              | The provider folder, provider access, or vault went away      |
| `connection.reauth_required` | The account needs reconnecting                                |

All connector events require the `connectors` scope on your webhook endpoint.

## Deleting a link

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

Deletes the link and its ledger. Vault documents are kept by default.

Add `?vault_docs=delete` to remove the documents an import link brought in. That cleanup runs as a durable background workflow and returns `202`.

<Warning>
  `vault_docs=delete` is not reversible. Confirm with the user before calling it.
</Warning>

## Next steps

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

  <Card title="Providers" href="/connectors/providers">
    How Clio and Google Drive differ in hierarchy, change detection, and rate budget
  </Card>

  <Card title="Vault Search" href="/vault/search">
    Search the documents once they have been ingested
  </Card>
</CardGroup>
