> For the complete documentation index, see [llms.txt](https://docs.molecule.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.molecule.xyz/api-reference/labs-api/files.md).

# Files

Working with files in a Lab dataroom: the three-step upload flow (initiate → upload → finish), plus metadata updates, deletion, storage limits, and client-side encryption. Creating the Lab itself is covered in [Lab Management](/api-reference/labs-api/lab-management.md).

> **Looking for a runnable walkthrough?** This page is the per-operation reference. For a first upload with expected responses and failure handling at every step, use [Create a lab and upload a public file](/api-reference/getting-started/create-lab-and-upload-file.md) or [Upload an encrypted file](/api-reference/getting-started/upload-encrypted-file.md) (encrypted, with a decrypt round trip).

> **Note**: Every mutation on this page returns its failure in-band: the result carries `error: ApiError`, and success means `error` is `null`. Branch on `error.code` — never on `message` text — and quote `requestId` when reporting a problem. See [Errors](/api-reference/errors.md#labs-api-errors) for the `ApiError` shape, how to read `details`, and the list of error codes.

## Step 1: Initiate File Upload

Starts a file or file-version upload to a lab's data room, returning a pre-signed `uploadUrl` and the `uploadToken` for the finish step. PUT the bytes to that URL, then call `finishCreateOrUpdateFile` with the token.

Returns [`InitiateFileUploadResult!`](/api-reference/types.md#initiatefileuploadresult).

| Name            | Type      | Description                                                    |
| --------------- | --------- | -------------------------------------------------------------- |
| `oclId`         | `String!` | 32-byte OCL id of the lab, `0x` plus 64 hex, case-insensitive. |
| `contentType`   | `String!` | MIME type to store with the file.                              |
| `contentLength` | `Int!`    | Size of the upload in bytes.                                   |

**GraphQL Mutation:**

```graphql
mutation InitiateFileUpload(
  $oclId: String!
  $contentType: String!
  $contentLength: Int!
) {
  initiateCreateOrUpdateFile(
    oclId: $oclId
    contentType: $contentType
    contentLength: $contentLength
  ) {
    uploadToken
    uploadUrl
    uploadUrlExpiry
    method
    headers {
      key
      value
    }
    error {
      code
      message
      requestId
      retryable
      details
    }
  }
}
```

**Example Request (curl):**

```bash
curl -X POST https://production.graphql.api.molecule.xyz/graphql \
  -H 'Content-Type: application/json' \
  -H 'Authorization: YOUR_CONSUMER_CREDENTIAL' \
  -H 'X-Service-Token: YOUR_SERVICE_TOKEN' \
  -d '{
    "query": "mutation InitiateFileUpload($oclId: String!, $contentType: String!, $contentLength: Int!) { initiateCreateOrUpdateFile(oclId: $oclId, contentType: $contentType, contentLength: $contentLength) { uploadToken uploadUrl uploadUrlExpiry method headers { key value } error { code message requestId retryable details } } }",
    "variables": {
      "oclId": "0x0101000000000000000000000000000000000000000000000000000000000042",
      "contentType": "application/pdf",
      "contentLength": 381846
    }
  }'
```

**Success Response:**

```json
{
  "data": {
    "initiateCreateOrUpdateFile": {
      "uploadToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "uploadUrl": "https://s3.amazonaws.com/bucket/path?signature=...",
      "uploadUrlExpiry": "2024-01-15T10:45:00.000Z",
      "method": "PUT",
      "headers": [
        {
          "key": "Content-Type",
          "value": "application/pdf"
        }
      ],
      "error": null
    }
  }
}
```

On failure `error` is set and the upload fields (`uploadToken`, `uploadUrl`, `uploadUrlExpiry`, `method`, `headers`) are `null`; do not proceed to Step 2 unless `error` is `null`.

## Step 2: Upload File to Storage

Upload the file directly to the presigned URL returned in Step 1.

**Example Request (curl):**

```bash
curl -X PUT "UPLOAD_URL_FROM_STEP_1" \
  -H "Content-Type: application/pdf" \
  --data-binary @your-file.pdf
```

**Example Request (JavaScript):**

```javascript
const uploadHeaders = {};
headers.forEach((h) => {
  uploadHeaders[h.key] = h.value;
});

const uploadResponse = await fetch(uploadUrl, {
  method: "PUT",
  headers: uploadHeaders,
  body: fileBuffer,
});

if (!uploadResponse.ok) {
  throw new Error(`Upload failed: ${uploadResponse.statusText}`);
}
```

This `PUT` goes to storage, not to the API, so a failure here is a plain HTTP error with no `ApiError`. If it fails:

* Send the body as binary (`--data-binary` in curl)
* Send exactly the headers returned in Step 1
* Check that the presigned URL hasn't expired (it expires after \~15 minutes); if it has, start again from Step 1

## Step 3: Finish File Upload

Completes an upload once the bytes have been PUT to the `uploadUrl` from `initiateCreateOrUpdateFile`. Provide `path` for a new file or `ref` for a new version of an existing one, never both.

Returns [`FinishFileUploadResult!`](/api-reference/types.md#finishfileuploadresult).

| Name                 | Type                                                                         | Description                                                                                                                                                                              |
| -------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `oclId`              | `String!`                                                                    | 32-byte OCL id of the lab, `0x` plus 64 hex, case-insensitive.                                                                                                                           |
| `uploadToken`        | `String!`                                                                    | Value of `uploadToken` returned by `initiateCreateOrUpdateFile`.                                                                                                                         |
| `path`               | `String`                                                                     | Path for a new file, e.g. `foo.txt`. Underscores are not allowed.                                                                                                                        |
| `ref`                | `String`                                                                     | Dataset ref of an existing file, to store a new version of it.                                                                                                                           |
| `accessLevel`        | `String!`                                                                    | Access level of the file, `PUBLIC` or `ADMIN`, with `HOLDERS` accepted but unenforced. `ADMIN` restricts reading only for content encrypted at upload; the label alone protects nothing. |
| `changeBy`           | `String`                                                                     | Deprecated and ignored: the change is attributed to the authenticated caller. Removed after 2027-01-01.                                                                                  |
| `description`        | `String`                                                                     | Description of the file or version.                                                                                                                                                      |
| `tags`               | `[String!]`                                                                  | Tags for the file or version.                                                                                                                                                            |
| `contentText`        | `String`                                                                     | Searchable text, stored and returned unencrypted even on `ADMIN` files, so it should hold no more than the title.                                                                        |
| `categories`         | `[String!]`                                                                  | Categories for the file or version.                                                                                                                                                      |
| `encryptionMetadata` | [`EncryptionMetadataInput`](/api-reference/types.md#encryptionmetadatainput) | Encryption metadata for encrypted files (KMS, BLS, or legacy).                                                                                                                           |

**GraphQL Mutation:**

```graphql
mutation FinishFileUpload(
  $oclId: String!
  $uploadToken: String!
  $path: String
  $ref: String
  $accessLevel: String!
  $changeBy: String!
  $description: String
  $tags: [String!]
  $categories: [String!]
  $contentText: String
) {
  finishCreateOrUpdateFile(
    oclId: $oclId
    uploadToken: $uploadToken
    path: $path
    ref: $ref
    accessLevel: $accessLevel
    changeBy: $changeBy
    description: $description
    tags: $tags
    categories: $categories
    contentText: $contentText
  ) {
    datasetId
    contentHash
    version
    message
    error {
      code
      message
      requestId
      retryable
      details
    }
  }
}
```

*\*Use `path` for new files OR `ref` for versions - not both*

**Example Request (curl):**

```bash
curl -X POST https://production.graphql.api.molecule.xyz/graphql \
  -H 'Content-Type: application/json' \
  -H 'Authorization: YOUR_CONSUMER_CREDENTIAL' \
  -H 'X-Service-Token: YOUR_SERVICE_TOKEN' \
  -d '{
    "query": "mutation FinishFileUpload($oclId: String!, $uploadToken: String!, $path: String, $accessLevel: String!, $changeBy: String!, $description: String, $tags: [String!], $categories: [String!], $contentText: String) { finishCreateOrUpdateFile(oclId: $oclId, uploadToken: $uploadToken, path: $path, accessLevel: $accessLevel, changeBy: $changeBy, description: $description, tags: $tags, categories: $categories, contentText: $contentText) { datasetId contentHash version message error { code message requestId retryable details } } }",
    "variables": {
      "oclId": "0x0101000000000000000000000000000000000000000000000000000000000042",
      "uploadToken": "TOKEN_FROM_STEP_1",
      "path": "research-results.pdf",
      "accessLevel": "PUBLIC",
      "changeBy": "0x1234567890123456789012345678901234567890",
      "description": "Q4 2024 Research Results",
      "tags": ["research", "results", "2024"],
      "categories": ["data"],
      "contentText": "Quarterly research findings and experimental data"
    }
  }'
```

**Success Response:**

```json
{
  "data": {
    "finishCreateOrUpdateFile": {
      "datasetId": "did:kamu:...",
      "contentHash": "sha256:abc123...",
      "version": 1,
      "message": "File uploaded successfully",
      "error": null
    }
  }
}
```

**Failure Response** (HTTP 200, no top-level `errors[]` — `message` mirrors `error.message`):

```json
{
  "data": {
    "finishCreateOrUpdateFile": {
      "datasetId": null,
      "contentHash": null,
      "version": null,
      "message": "You are not allowed to perform this operation.",
      "error": {
        "code": "UNAUTHORIZED",
        "message": "You are not allowed to perform this operation.",
        "requestId": "8f1e4c9a-2b7d-4e10-9c3a-5d6f7a8b9c0d",
        "retryable": false,
        "details": "{\"reason\":\"UNAUTHORIZED\"}"
      }
    }
  }
}
```

***

## Complete Example

Here's a complete Node.js example demonstrating the full 3-step workflow:

```javascript
#!/usr/bin/env node

const fs = require("fs");
const fetch = require("node-fetch");

async function uploadFileToLabs(filePath, oclId, serviceToken) {
  const apiUrl = "https://production.graphql.api.molecule.xyz/graphql";
  const fileBuffer = fs.readFileSync(filePath);
  const filename = require("path").basename(filePath);
  const fileSize = fs.statSync(filePath).size;

  try {
    // Step 1: Initiate upload
    console.log("Step 1: Initiating file upload...");
    const initiateResponse = await fetch(apiUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": process.env.CONSUMER_CREDENTIAL,
        "X-Service-Token": serviceToken,
      },
      body: JSON.stringify({
        query: `
          mutation InitiateFileUpload($oclId: String!, $contentType: String!, $contentLength: Int!) {
            initiateCreateOrUpdateFile(
              oclId: $oclId
              contentType: $contentType
              contentLength: $contentLength
            ) {
              uploadToken
              uploadUrl
              method
              headers { key value }
              error { code message requestId retryable details }
            }
          }
        `,
        variables: {
          oclId,
          contentType: "application/octet-stream",
          contentLength: fileSize,
        },
      }),
    });

    const initiateResult = await initiateResponse.json();
    if (initiateResult.errors?.length) {
      // Top-level errors on a mutation mean a transport or request-validation failure
      const e = initiateResult.errors[0];
      throw new Error(
        `${e.errorType ?? "Request failed"}: ${e.message}${
          e.errorInfo?.requestId ? ` (requestId ${e.errorInfo.requestId})` : ""
        }`,
      );
    }
    const initiateError = initiateResult.data.initiateCreateOrUpdateFile.error;
    if (initiateError) {
      throw new Error(
        `${initiateError.code}: ${initiateError.message} (requestId ${initiateError.requestId})`,
      );
    }

    const { uploadToken, uploadUrl, headers } =
      initiateResult.data.initiateCreateOrUpdateFile;
    console.log("✅ Upload initiated");

    // Step 2: Upload to presigned URL
    console.log("Step 2: Uploading file to storage...");
    const uploadHeaders = {};
    headers?.forEach((h) => {
      uploadHeaders[h.key] = h.value;
    });

    const uploadResponse = await fetch(uploadUrl, {
      method: "PUT",
      headers: uploadHeaders,
      body: fileBuffer,
    });

    if (!uploadResponse.ok) {
      throw new Error(`Upload failed: ${uploadResponse.statusText}`);
    }
    console.log("✅ File uploaded to storage");

    // Step 3: Finish upload
    console.log("Step 3: Finalizing upload...");
    const finishResponse = await fetch(apiUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": process.env.CONSUMER_CREDENTIAL,
        "X-Service-Token": serviceToken,
      },
      body: JSON.stringify({
        query: `
          mutation FinishFileUpload(
            $oclId: String!
            $uploadToken: String!
            $path: String!
            $accessLevel: String!
            $changeBy: String!
          ) {
            finishCreateOrUpdateFile(
              oclId: $oclId
              uploadToken: $uploadToken
              path: $path
              accessLevel: $accessLevel
              changeBy: $changeBy
            ) {
              datasetId
              message
              error { code message requestId retryable details }
            }
          }
        `,
        variables: {
          oclId,
          uploadToken,
          path: filename,
          accessLevel: "PUBLIC",
          changeBy: process.env.WALLET_ADDRESS,
        },
      }),
    });

    const finishResult = await finishResponse.json();
    if (finishResult.errors?.length) {
      const e = finishResult.errors[0];
      throw new Error(
        `${e.errorType ?? "Request failed"}: ${e.message}${
          e.errorInfo?.requestId ? ` (requestId ${e.errorInfo.requestId})` : ""
        }`,
      );
    }
    const finishError = finishResult.data.finishCreateOrUpdateFile.error;
    if (finishError) {
      throw new Error(
        `${finishError.code}: ${finishError.message} (requestId ${finishError.requestId})`,
      );
    }

    console.log("🎉 File upload completed successfully!");
    console.log(
      "Dataset ID:",
      finishResult.data.finishCreateOrUpdateFile.datasetId,
    );

    return {
      success: true,
      datasetId: finishResult.data.finishCreateOrUpdateFile.datasetId,
    };
  } catch (error) {
    console.error("❌ Upload failed:", error.message);
    throw error;
  }
}

// Usage
if (require.main === module) {
  const filePath = process.argv[2];
  const oclId = process.argv[3];
  const serviceToken = process.env.SERVICE_TOKEN;

  if (!filePath || !oclId || !serviceToken) {
    console.error(
      'Usage: SERVICE_TOKEN="token" WALLET_ADDRESS="0x..." node upload.js <file> <ocl-id>',
    );
    process.exit(1);
  }

  uploadFileToLabs(filePath, oclId, serviceToken);
}

module.exports = { uploadFileToLabs };
```

**Usage:**

```bash
CONSUMER_CREDENTIAL="mol_your-consumer-id_your-secret" SERVICE_TOKEN="your-service-token" WALLET_ADDRESS="0x..." node upload.js data.pdf 0x0101000000000000000000000000000000000000000000000000000000000042
```

***

## Update File Metadata

Updates a file's metadata, recorded as a new version with unchanged content. The change is attributed to the authenticated caller.

Returns [`UpdateFileMetadataResult!`](/api-reference/types.md#updatefilemetadataresult).

| Name          | Type        | Description                                                                                                                                                                              |
| ------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `oclId`       | `String!`   | 32-byte OCL id of the lab, `0x` plus 64 hex, case-insensitive.                                                                                                                           |
| `ref`         | `String!`   | Reference (DID) of the file to update.                                                                                                                                                   |
| `accessLevel` | `String!`   | Access level of the file, `PUBLIC` or `ADMIN`, with `HOLDERS` accepted but unenforced. `ADMIN` restricts reading only for content encrypted at upload; the label alone protects nothing. |
| `description` | `String`    | Description to store for the file.                                                                                                                                                       |
| `tags`        | `[String!]` | Tags to store on the file.                                                                                                                                                               |
| `categories`  | `[String!]` | Categories to store on the file.                                                                                                                                                         |
| `contentText` | `String`    | Searchable text, stored and returned unencrypted even on `ADMIN` files, so it should hold no more than the title.                                                                        |

**GraphQL Mutation:**

```graphql
mutation UpdateFileMetadata(
  $oclId: String!
  $ref: String!
  $accessLevel: String!
  $description: String
  $tags: [String!]
  $categories: [String!]
  $contentText: String
) {
  updateFileMetadata(
    oclId: $oclId
    ref: $ref
    accessLevel: $accessLevel
    description: $description
    tags: $tags
    categories: $categories
    contentText: $contentText
  ) {
    ref
    message
    error {
      code
      message
      requestId
      retryable
      details
    }
  }
}
```

> **Note**: The `changeBy` field (wallet address) is automatically derived from your authentication and does not need to be provided as a parameter.

**Example Request:**

```bash
curl -X POST https://production.graphql.api.molecule.xyz/graphql \
  -H 'Content-Type: application/json' \
  -H 'Authorization: YOUR_CONSUMER_CREDENTIAL' \
  -H 'X-Service-Token: YOUR_SERVICE_TOKEN' \
  -d '{
    "query": "mutation UpdateFileMetadata($oclId: String!, $ref: String!, $accessLevel: String!, $description: String, $tags: [String!], $categories: [String!], $contentText: String) { updateFileMetadata(oclId: $oclId, ref: $ref, accessLevel: $accessLevel, description: $description, tags: $tags, categories: $categories, contentText: $contentText) { ref message error { code message requestId retryable details } } }",
    "variables": {
      "oclId": "0x0101000000000000000000000000000000000000000000000000000000000042",
      "ref": "did:kamu:fed01...",
      "accessLevel": "PUBLIC",
      "description": "Updated research findings with peer review",
      "tags": ["research", "peer-reviewed", "2024"],
      "categories": ["data", "validated"],
      "contentText": "Enhanced searchable content with key findings"
    }
  }'
```

***

## Delete File

Deletes a file from the lab data room.

Returns [`DeleteDataRoomFileResult`](/api-reference/types.md#deletedataroomfileresult).

| Name       | Type      | Description                                                                                                               |
| ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------- |
| `oclId`    | `String!` | 32-byte OCL id of the lab, `0x` plus 64 hex, case-insensitive.                                                            |
| `path`     | `String!` | Path within the data room. Paths under `agreements/` are reserved for signed legal agreements and cannot be deleted here. |
| `changeBy` | `String`  | Deprecated and ignored: the change is attributed to the authenticated caller. Removed after 2027-01-01.                   |

**GraphQL Mutation:**

```graphql
mutation DeleteFile($oclId: String!, $path: String!, $changeBy: String!) {
  deleteDataRoomFile(oclId: $oclId, path: $path, changeBy: $changeBy) {
    oclId
    filePath
    error {
      code
      message
      requestId
      retryable
      details
    }
  }
}
```

> **Warning**: This is a destructive operation. The file will be permanently deleted from the dataroom and cannot be recovered.

**Example Request:**

```bash
curl -X POST https://production.graphql.api.molecule.xyz/graphql \
  -H 'Content-Type: application/json' \
  -H 'Authorization: YOUR_CONSUMER_CREDENTIAL' \
  -H 'X-Service-Token: YOUR_SERVICE_TOKEN' \
  -d '{
    "query": "mutation DeleteFile($oclId: String!, $path: String!, $changeBy: String!) { deleteDataRoomFile(oclId: $oclId, path: $path, changeBy: $changeBy) { oclId filePath error { code message requestId retryable details } } }",
    "variables": {
      "oclId": "0x0101000000000000000000000000000000000000000000000000000000000042",
      "path": "old-data.pdf",
      "changeBy": "0x1234567890123456789012345678901234567890"
    }
  }'
```

***

## Get File by Path

Retrieve a specific file using the lab's `oclId` and the file path.

**GraphQL Query:**

```graphql
query GetFile($oclId: String!, $path: String!) {
  dataRoomFile(oclId: $oclId, path: $path) {
    did
    path
    version
    contentType
    accessLevel
    description
    tags
    categories
    contentText
    downloadUrl
    downloadUrlExpiry
  }
}
```

**Example Request:**

```bash
curl -X POST https://production.graphql.api.molecule.xyz/graphql \
  -H 'Content-Type: application/json' \
  -H 'Authorization: YOUR_CONSUMER_CREDENTIAL' \
  -d '{
    "query": "query GetFile($oclId: String!, $path: String!) { dataRoomFile(oclId: $oclId, path: $path) { did path contentType accessLevel downloadUrl } }",
    "variables": {
      "oclId": "0x0101000000000000000000000000000000000000000000000000000000000042",
      "path": "research-data.pdf"
    }
  }'
```

***

## File Categories & Tags

### Get File Categories and Tags

Return the valid file categories and their tags from the CMS. Use these when tagging or categorizing files. **Public query** — no authentication required.

```graphql
query FileCategoriesAndTags {
  fileCategoriesAndTags {
    data {
      name
      tags
    }
  }
}
```

Each entry in `data` is a `FileCategory` with a `name` and its list of allowed `tags`. Failures arrive as top-level GraphQL `errors[]` entries with `errorType` set to the catalogue code (e.g. `UPSTREAM_UNAVAILABLE`); the result type has no `error` field.

***

## File Requirements & Limits

### Storage Limits

* **Default Limit**: 5GB per lab/project
* **Custom Limits**: Can be increased upon request - contact the Molecule team
* **Note**: the Labs web app additionally caps individual uploads at 100 MB per file; API uploads are not subject to that app-side cap

### Supported File Types

* All file types are supported
* Common types: PDF, PNG, JPEG, CSV, JSON, ZIP, etc.

### Optional Metadata

Enhance file discoverability with optional metadata:

* **description**: Human-readable description of the file
* **tags**: Array of tags for categorization (e.g., `["research", "q4-2024"]`)
* **categories**: Array of categories for organization (e.g., `["data", "results"]`)
* **contentText**: Searchable text content for full-text search

***

## Advanced: Encrypted File Upload

> **Step-by-step version:** [Upload an encrypted file](/api-reference/getting-started/upload-encrypted-file.md), including both access-condition recipes (owner-only, and owner/contributor/viewer) and a decrypt round trip that verifies the gate actually works.

For files requiring client-side encryption, obtain a data encryption key via the `generateDataEncryptionKey` mutation, encrypt locally, upload as normal, and include an `encryptionMetadata` object on `finishCreateOrUpdateFile`. The full end-to-end model — key wrapping, onchain access conditions, and condition-gated decryption — is documented on the [Data Privacy & Access](/technical-deep-dive/data/data-privacy-and-access.md) page.

### Obtain a DEK, then encrypt locally

`generateDataEncryptionKey` (no arguments) returns `plaintextDEK`, `encryptedDek`, and `encryptionSystem`. The client uses `plaintextDEK` to AES-256-GCM encrypt the file locally (Web Crypto `SubtleCrypto`), then wipes it from memory. The upload itself uses the standard `initiateCreateOrUpdateFile` → PUT → `finishCreateOrUpdateFile` flow, with the encrypted bytes uploaded to the presigned URL.

### Encryption Metadata Parameter (Onchain-Verified Envelope Encryption, current default)

```graphql
$encryptionMetadata: EncryptionMetadataInput
```

```json
{
  "encryptionMetadata": {
    "encryptionSystem": "<echo value returned by generateDataEncryptionKey>",
    "encryptedDek": "BASE64_WRAPPED_DEK",
    "iv": "BASE64_AES_GCM_IV",
    "contentHash": "sha256-...",
    "accessControlConditions": "[{...}]",
    "encryptedBy": "0x1234567890123456789012345678901234567890",
    "encryptedAt": "2026-01-15T10:30:00.000Z"
  }
}
```

`encryptionSystem` is **backend-set** — clients must echo the value returned by `generateDataEncryptionKey` rather than hardcode it. This keeps the roadmap rollover to BLS threshold key custody transparent to existing integrations.

#### `accessControlConditions` — gating decryption by role

`accessControlConditions` is a JSON-stringified array of `EvmContractCondition` predicates joined by `BooleanCondition` separators (`and` / `or`). The backend evaluates each predicate against live chain state at decrypt time via viem `readContract`, short-circuits booleans, and fails closed on RPC error. To gate decryption on *LabNFT owner OR active Contributor OR active Viewer*, OR `AccessResolver.isAuthorizedSignerForTba(:userAddress, tba)` against `AccessResolver.hasRole(oclId, :userAddress, ROLE_VIEWER)` — the role-hierarchy collapses Contributor + Viewer into one check on the canonical chain (Base).

The placeholder `:userAddress` in `functionParams` is substituted with the authenticated caller's wallet at evaluate time. The full `EvmContractCondition` JSON shape, the worked OR-composite example, and condition-evaluator semantics are documented on the [Data Privacy & Access](/technical-deep-dive/data/data-privacy-and-access.md#worked-example-encrypt-for-owner-or-contributor-or-viewer) page.

#### Role Management (onchain, off this API surface)

Role grants are **onchain transactions on the `AccessResolver` contract**, not Labs API mutations. Lab owners (and active Contributors, for the Viewer slot) call `grantRole(oclId, account, role, expiry, isAgent)` / `revokeRole(oclId, account)` directly via viem / ethers / Safe. The Labs API only *consumes* role state at decrypt time through the `accessControlConditions` evaluator. See [Roles & Permissions](/technical-deep-dive/roles-and-permissions.md) for the capability matrix, grant lifecycle (expiry, `isAgent`), and the [`AccessResolver` reference](/networks-and-contracts/contracts/accessresolver.md) for the onchain interface.

**When to Use Encryption:**

* Sensitive research data requiring access control
* Compliance requirements for data protection
* Conditional access based on token ownership or lab role

***

## Data Encryption Keys

### Generate a Data Encryption Key

Generates a standalone data encryption key for client-side encryption, returning both the plaintext key and its wrapped form. Requires an authenticated caller.

Returns [`GenerateDataEncryptionKeyResult!`](/api-reference/types.md#generatedataencryptionkeyresult).

This operation takes no arguments.

The plaintext DEK encrypts data locally and is then wiped; the encrypted DEK is stored alongside the ciphertext. See [Advanced: Encrypted File Upload](#advanced-encrypted-file-upload) for the file-upload encryption path.

```graphql
mutation GenerateDataEncryptionKey {
  generateDataEncryptionKey {
    plaintextDEK
    encryptedDek
    encryptionSystem
    error {
      code
      message
      requestId
      retryable
      details
    }
  }
}
```

***

## Related

* [Create a lab and upload a public file](/api-reference/getting-started/create-lab-and-upload-file.md) — a first upload, step by step
* [Upload an encrypted file](/api-reference/getting-started/upload-encrypted-file.md) — encrypted upload with a decrypt round trip
* [Data Privacy & Access](/technical-deep-dive/data/data-privacy-and-access.md) — how access conditions are evaluated
* [Browse & Search](/api-reference/labs-api/browse-and-search.md) — read and search files across labs

{% hint style="info" %}
**Need help?**

Ask the team on Discord. Reporting an API error? Include the `requestId` from the failing response so we can trace it.

<a href="https://t.co/L0VEiy4Bjk" class="button primary" data-icon="discord">Ask on Discord</a>

*Page wrong, unclear or missing something? Let us know with **Was this helpful?** on this page.*
{% endhint %}
