Workspace-wide file storage, brokered so your application never holds the bytes.

Three scopes, deliberately split so a key gets only what it needs: `drive:write` uploads, `drive:read` returns a signed URL for the original, `drive:preview` returns one for a thumbnail only.

```ts
const file = await connect.drive.upload({ workspaceId, file, parentId });

// Later - URLs are short-lived, so fetch one when you render, not at upload.
const url = await connect.drive.getFileUrl(file.id, { workspaceId });
```

**Signed URLs expire.** Store the `fileId`, never the URL. Caching a URL is how an `<img>` starts 403ing a few minutes after the page was built.

Drive is the *workspace's* storage, not your application's - the customer sees these files in their own Drive, and they are covered by the deletion policy you declared. A key scoped to `drive:preview` alone can render thumbnails without ever being able to fetch an original.

## Endpoints

### POST `/api/v1/application/drive/files`

Upload a file into a workspace's Drive

**Scope** `drive:write` &nbsp;·&nbsp; **Rate limit** 20 requests / min per key

Uploads a file into the target workspace's Drive and hands back a signed URL for it in the same response - no separate call needed right after upload.

#### Implementation

**1. Prerequisite** - an API key scoped to `drive:write`.

**2. Send a `multipart/form-data` request** - `file` is the binary, `workspaceId` is required, `parentId` is an optional Drive folder id (omit, or send empty, to upload into the workspace's Drive root):
```bash
curl -X POST https://connect.cosmoops.com/api/v1/application/drive/files \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
  -F "workspaceId=WORKSPACE_ID" \
  -F "file=@invoice.pdf"
```

**3. Read `url`** in the response - a short-lived signed URL for the original file. Store `id` if you'll need the file again later; re-fetch a fresh URL anytime with `GET /drive/files/{id}/url` below rather than caching this one past its expiry.

#### Errors
`validation_error` (400 - missing `file`/`workspaceId`, or the file exceeds the per-file size cap), `forbidden` (403 - key missing `drive:write`), `not_found` (404 - unknown `workspaceId`), `quota_exceeded` (409 - the workspace's Drive storage is full), `rate_limited` (429).

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | `string` | yes |  |
| `file` | `string` | yes | Binary file content |
| `parentId` | `uuid` | no | Destination Drive folder id - omitted uploads into the workspace's Drive root |

**Responses**

| Status | Meaning |
| --- | --- |
| `201` | File uploaded |
| `400` | validation_error - missing file/workspaceId, or file too large |
| `403` | forbidden - key missing drive:write |
| `404` | not_found - unknown workspaceId |
| `409` | quota_exceeded - workspace's Drive storage is full |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `id` | `uuid` | yes |  |
| `fileName` | `string` | yes |  |
| `mimeType` | `string` | yes |  |
| `size` | `number` | yes | Bytes |
| `url` | `uri` | yes |  |

### GET `/api/v1/application/drive/files/{fileId}/url`

Get a signed URL for a Drive file's original bytes

**Scope** `drive:read` &nbsp;·&nbsp; **Rate limit** 60 requests / min per key

Re-fetches a signed URL for a file already uploaded (via `POST /drive/files` above, or already present in the workspace's Drive) - use this once the URL you were handed earlier has expired.

#### Implementation

**1. Prerequisite** - an API key scoped to `drive:read`.

**2. Send the request:**
```bash
curl "https://connect.cosmoops.com/api/v1/application/drive/files/FILE_ID/url?workspaceId=WORKSPACE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"
```

**3. Read `url`** - short-lived, don't persist it; call this again next time you need it.

#### Errors
`validation_error` (400 - missing `workspaceId`), `forbidden` (403 - key missing `drive:read`), `not_found` (404 - unknown `fileId` for this workspace, or it's been deleted), `rate_limited` (429).

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `fileId` | path | yes |  |
| `workspaceId` | query | yes |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Signed URL |
| `400` | validation_error - missing workspaceId |
| `403` | forbidden - key missing drive:read |
| `404` | not_found - unknown fileId for this workspace |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `url` | `uri` | yes |  |

### GET `/api/v1/application/drive/usage`

Get how much of a workspace's Drive storage is used

**Scope** `drive:read` &nbsp;·&nbsp; **Rate limit** 60 requests / min per key

Returns the bytes the workspace's Drive holds and its limit - the same limit an upload is refused against with `quota_exceeded`, so a meter built on this agrees with the refusal. The limit is the workspace's baseline plus the storage every active plan includes; `limitBytes: null` means unlimited.

#### Implementation

**1. Prerequisite** - an API key scoped to `drive:read`, and the workspace must hold an active subscription to your application.

**2. Send the request:**
```bash
curl "https://connect.cosmoops.com/api/v1/application/drive/usage?workspaceId=WORKSPACE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"
```

#### Errors
`validation_error` (400 - missing `workspaceId`), `forbidden` (403 - key missing `drive:read`, or the workspace does not subscribe to your application), `not_found` (404 - unknown `workspaceId`), `rate_limited` (429).

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | query | yes |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Storage usage |
| `400` | validation_error - missing workspaceId |
| `403` | forbidden - key missing drive:read, or no subscription |
| `404` | not_found - unknown workspaceId |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `usedBytes` | `number` | yes | Bytes |
| `limitBytes` | `any` | yes | Bytes; null means unlimited |

### GET `/api/v1/application/drive/files`

List the files your application uploaded to a workspace's Drive

**Scope** `drive:read` &nbsp;·&nbsp; **Rate limit** 60 requests / min per key

The files **your application** uploaded through `POST /drive/files`, newest first. A workspace's Drive also holds its members' own files and other applications' files; none of those are listed here, and none can be read or deleted through your key.

`limit` defaults to 50 and is capped at 100. There is no cursor: `truncated: true` says more exist. An application that keeps more than a hundred files in one workspace should keep its own index of the ids `POST /drive/files` returned.

#### Errors
`validation_error` (400 - missing `workspaceId`, or a `limit` that is not a positive integer), `forbidden` (403 - key missing `drive:read`), `rate_limited` (429).

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | query | yes |  |
| `limit` | query | no | 1-100, default 50. |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Your application's files |
| `400` | validation_error |
| `403` | forbidden - key missing drive:read |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `files` | `array` | yes |  |
| `truncated` | `boolean` | yes | More files exist than `limit` returned; there is no cursor |

### DELETE `/api/v1/application/drive/files/{fileId}`

Delete a file your application uploaded

**Scope** `drive:write` &nbsp;·&nbsp; **Rate limit** 20 requests / min per key

Removes the file's bytes and thumbnails and gives the space back to the workspace's Drive quota. Only a file **your application** uploaded can be deleted: a member's file, another application's file, an unknown id and an already-deleted file all answer the same `not_found`, so a retry after a timeout can treat 404 as done.

#### Errors
`validation_error` (400 - missing `workspaceId`), `forbidden` (403 - key missing `drive:write`), `not_found` (404), `rate_limited` (429).

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `fileId` | path | yes |  |
| `workspaceId` | query | yes |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `204` | Deleted |
| `400` | validation_error - missing workspaceId |
| `403` | forbidden - key missing drive:write |
| `404` | not_found - not a file your application uploaded |
| `429` | rate_limited |

### GET `/api/v1/application/drive/files/{fileId}/preview-url`

Get a signed URL for a Drive file's thumbnail

**Scope** `drive:preview` &nbsp;·&nbsp; **Rate limit** 60 requests / min per key

Same as `GET /drive/files/{fileId}/url` above, but for the file's thumbnail instead of its original bytes - useful for rendering a preview without downloading a large original.

#### Implementation

**1. Prerequisite** - an API key scoped to `drive:preview`.

**2. Send the request** - `size` defaults to `medium`:
```bash
curl "https://connect.cosmoops.com/api/v1/application/drive/files/FILE_ID/preview-url?workspaceId=WORKSPACE_ID&size=medium" \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"
```

**3. Read `url`** - short-lived, same as the original-file endpoint.

#### Errors
`validation_error` (400 - missing `workspaceId`, or an invalid `size`), `forbidden` (403 - key missing `drive:preview`), `not_found` (404 - unknown `fileId`, or it has no thumbnail), `rate_limited` (429).

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `fileId` | path | yes |  |
| `workspaceId` | query | yes |  |
| `size` | query | no | Defaults to "medium" |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Signed thumbnail URL |
| `400` | validation_error - missing workspaceId, or invalid size |
| `403` | forbidden - key missing drive:preview |
| `404` | not_found - unknown fileId, or it has no thumbnail |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `url` | `uri` | yes |  |