---
title: Attach a document
description: Upload a file as base64 and attach it to an account, in two calls through CloudObjects or in one through CloudObjectRelations, in REST or GraphQL.
canonical: https://developers.coevera.com/recipes/attach-document/
section: recipes
verification: behaviour
lastVerified: 2026-10-15
publisher: Coevera
---

# Attach a document

**Answer:** Send the file as base64 content with a filename and type 1 (S3File). Create it in CloudObjects and link it with a CloudObjectRelations row, or nest it in the relation create to do both in one call. The limit is the space's Attachment Size Limit, 30 MB by default and up to 200 MB, which the API cannot read.

Attach two files to **Fabrikam Robotics**: a pilot scope, uploaded first and linked second, and
a site survey, uploaded and linked in one call. You end with both files on the account, read
back with the size and type the API recorded.

## Before you start

|                    |                                                                                                                                                                                                        |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Permissions        | **Full Access** to **Documents** on the **Misc** tab, and **Read Only Access** to **Accounts** on the **Records** tab.                                                                                 |
| `validation-level` | The default (`0`, full validation).                                                                                                                                                                    |
| Batch behaviour    | No batches: every write is one record.                                                                                                                                                                 |
| File size          | The space's **Attachment Size Limit**: 30 MB by default, and an admin can raise it up to 200 MB. The API does not expose the value. Base64 adds about a third, so a 30 MB file is about 40 MB of JSON. |
| Context            | Region and space ID come from the context bar. Credentials come from `$COEVERA_API_KEY` and `$COEVERA_API_SECRET`.                                                                                     |

A file is a `CloudObjects` record (in the app, a document). It appears on a record through a
`CloudObjectRelations` row that names the file and the record: `account_id` here, or
`contact_id`, `lead_oppty_id`, `project_id`, `quote_id`, `custom_entity_id` and the other parent
ids the relation takes.

The programs need `curl`, `jq` and `base64`, Node.js 18 or later, or Python 3.9 or later with
`requests`. [Authentication](/get-started/authentication/) shows where the credentials come from, and
[Permissions and scopes](/get-started/permissions-and-scopes/) where to set the permissions.

The operations this recipe calls, in the API reference: REST
[`GET /entities/Accounts`](/reference/rest/accounts/#list),
[`POST /entities/CloudObjects`](/reference/rest/cloud-objects/#create),
[`POST /entities/CloudObjectRelations`](/reference/rest/cloud-object-relations/#create),
[`GET /entities/CloudObjectRelations`](/reference/rest/cloud-object-relations/#list); GraphQL
[`AccountRepository.getByCriteria`](/reference/graphql/account/#q-getByCriteria),
[`createCloudObject`](/reference/graphql/cloud-object/#m-createCloudObject),
[`createCloudObjectRelation`](/reference/graphql/cloud-object-relation/#m-createCloudObjectRelation),
[`CloudObjectRelationRepository.getByCriteria`](/reference/graphql/cloud-object-relation/#q-getByCriteria).

## Set up a helper

Every step below uses this setup, and a helper that base64-encodes the file. The programs attach
short texts; in your integration, encode the file's bytes. Export the credentials first:

```bash
export COEVERA_API_KEY="<application user name>"
export COEVERA_API_SECRET="<application password>"   # never commit it
```

**REST · cURL**

```bash
COEVERA_BASE="https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"

# b64: the file goes up as base64 in JSON, on one line. In your integration, encode the file
# itself: base64 < pilot-scope.pdf | tr -d '\n'
b64() { printf '%s\n' "$1" | base64 | tr -d '\n'; }
```

**REST · TypeScript**

```ts
const base = `https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}`;
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");

async function coevera(path: string, init: { method?: string; body?: unknown } = {}) {
  const res = await fetch(`${base}${path}`, {
    method: init.method ?? "GET",
    headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
    body: init.body === undefined ? undefined : JSON.stringify(init.body),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`HTTP ${res.status} ${body.name}: ${String(body.message).trim()}`);
  return body;
}

// The file goes up as base64 in JSON. Here it is a short text; in your integration, read the
// file's bytes, for example with fs.readFile.
const base64 = (text: string) => Buffer.from(text, "utf8").toString("base64");
```

**REST · Python**

```python
BASE = "https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"
session = requests.Session()
session.auth = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])


def coevera(method, path, **kwargs):
    response = session.request(method, f"{BASE}{path}", timeout=30, **kwargs)
    body = response.json()
    if not response.ok:
        sys.exit(f"HTTP {response.status_code} {body.get('name')}: {str(body.get('message')).strip()}")
    return body


def b64(text):
    """The file goes up as base64 in JSON. In your integration, read the file's bytes instead."""
    return base64.b64encode(text.encode("utf-8")).decode("ascii")
```

**GraphQL · cURL**

```bash
COEVERA_GRAPHQL="https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public"

# gql: POST the GraphQL request read from stdin. GraphQL reports errors in the body, often
# with HTTP 200, so fail on those too.
gql() {
  local response
  response=$(curl -sS --fail-with-body "$COEVERA_GRAPHQL" \
    -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
    -H "Content-Type: application/json" \
    -d @-)
  if jq -e '.errors' <<<"$response" >/dev/null; then
    echo "$response" >&2
    return 1
  fi
  echo "$response"
}

# b64: the file goes up as base64, on one line. In your integration, encode the file itself:
# base64 < pilot-scope.pdf | tr -d '\n'
b64() { printf '%s\n' "$1" | base64 | tr -d '\n'; }
```

**GraphQL · TypeScript**

```ts
const endpoint = `https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public`;
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");

// gql: one GraphQL request. GraphQL reports errors in the body, often with HTTP 200.
async function gql(query: string, variables: object = {}) {
  const res = await fetch(endpoint, {
    method: "POST",
    headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  const body = await res.json();
  if (!res.ok || body.errors) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body.errors ?? body)}`);
  return body.data;
}

// The file goes up as base64 (the Base64 scalar). Here it is a short text; in your
// integration, read the file's bytes, for example with fs.readFile.
const base64 = (text: string) => Buffer.from(text, "utf8").toString("base64");
```

**GraphQL · Python**

```python
ENDPOINT = (
    "https://{region}.api.coevera.com/api/v100/app/space/"
    "{space_id}/graphql/public"
)
session = requests.Session()
session.auth = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])


def gql(query, variables=None):
    """One GraphQL request. GraphQL reports errors in the body, often with HTTP 200."""
    response = session.post(ENDPOINT, json={"query": query, "variables": variables or {}}, timeout=30)
    body = response.json()
    if not response.ok or body.get("errors"):
        sys.exit(f"HTTP {response.status_code}: {json.dumps(body.get('errors', body))}")
    return body["data"]


def b64(text):
    """The file goes up as base64 (the Base64 scalar). In your integration, read the file's bytes."""
    return base64.b64encode(text.encode("utf-8")).decode("ascii")
```

## 1. Find the account

Look the account up by its e-mail address.

**REST · cURL**

```bash
account=$(curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Accounts" \
  --data-urlencode "filter[email1]=fabrikam@example.com" \
  -d first=1 -d load-only=id,name \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET")
echo "$account"
ACCOUNT_ID=$(jq -er '.data[0].id' <<<"$account")
```

**REST · TypeScript**

```ts
const accounts = await coevera(
  `/entities/Accounts?${new URLSearchParams({ "filter[email1]": "fabrikam@example.com", first: "1", "load-only": "id,name" })}`,
);
const accountId: string | undefined = accounts.data[0]?.id;
if (!accountId) throw new Error("No account with that e-mail.");
console.log("Account:", accounts.data[0].name, accountId);
```

**REST · Python**

```python
accounts = coevera(
    "GET",
    "/entities/Accounts",
    params={"filter[email1]": "fabrikam@example.com", "first": 1, "load-only": "id,name"},
)
if not accounts["data"]:
    sys.exit("No account with that e-mail.")
account_id = accounts["data"][0]["id"]
print("Account:", accounts["data"][0]["name"], account_id)
```

**GraphQL · cURL**

```bash
account=$(jq -n '{
  query: "{ entities { account { getByCriteria(first: 1, filter: { email1: { eq: [\"fabrikam@example.com\"] } }) { edges { node { id name } } } } } }"
}' | gql)
echo "$account"
ACCOUNT_ID=$(jq -er '.data.entities.account.getByCriteria.edges[0].node.id' <<<"$account")
```

**GraphQL · TypeScript**

```ts
const accounts = await gql(`{
  entities {
    account {
      getByCriteria(first: 1, filter: { email1: { eq: ["fabrikam@example.com"] } }) {
        edges { node { id name } }
      }
    }
  }
}`);
const account = accounts.entities.account.getByCriteria.edges[0]?.node;
if (!account) throw new Error("No account with that e-mail.");
console.log("Account:", account.name, account.id);
```

**GraphQL · Python**

```python
accounts = gql("""
{
  entities {
    account {
      getByCriteria(first: 1, filter: { email1: { eq: ["fabrikam@example.com"] } }) {
        edges { node { id name } }
      }
    }
  }
}
""")
edges = accounts["entities"]["account"]["getByCriteria"]["edges"]
if not edges:
    sys.exit("No account with that e-mail.")
account = edges[0]["node"]
print("Account:", account["name"], account["id"])
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 1,
  "page_info": {
    "start_cursor": "WyIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "id": "0a000000-0000-4000-a000-000000000001",
      "name": "Fabrikam Robotics"
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "account": {
        "getByCriteria": {
          "edges": [
            {
              "node": {
                "id": "0a000000-0000-4000-a000-000000000001",
                "name": "Fabrikam Robotics"
              }
            }
          ]
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

## 2. Upload a file

Create the file in `CloudObjects` with `filename`, `type` and `content`, the file's bytes in
base64. The API stores it, measures its `size` and detects its `mime_type`. Its `url` is a signed
download link that expires after about 6 hours. The recording shows it redacted.

**REST · cURL**

```bash
# type 1 (S3File) stores the content in Coevera's file storage.
file=$(jq -n --arg content "$(b64 'Fabrikam Robotics pilot scope: telemetry from 12 assembly lines, 6 weeks.')" \
  '{ filename: "pilot-scope.txt", type: 1, content: $content }' |
  curl -sS --fail-with-body "$COEVERA_BASE/entities/CloudObjects" \
    -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
    -H "Content-Type: application/json" \
    -d @-)
echo "$file"
FILE_ID=$(jq -er '.data.id' <<<"$file")
```

**REST · TypeScript**

```ts
// type 1 (S3File) stores the content in Coevera's file storage.
const uploaded = await coevera("/entities/CloudObjects", {
  method: "POST",
  body: {
    filename: "pilot-scope.txt",
    type: 1,
    content: base64("Fabrikam Robotics pilot scope: telemetry from 12 assembly lines, 6 weeks.\n"),
  },
});
const fileId: string = uploaded.data.id;
console.log("File:", uploaded.data.filename, fileId);
```

**REST · Python**

```python
# type 1 (S3File) stores the content in Coevera's file storage.
uploaded = coevera(
    "POST",
    "/entities/CloudObjects",
    json={
        "filename": "pilot-scope.txt",
        "type": 1,
        "content": b64("Fabrikam Robotics pilot scope: telemetry from 12 assembly lines, 6 weeks.\n"),
    },
)
file_id = uploaded["data"]["id"]
print("File:", uploaded["data"]["filename"], file_id)
```

**GraphQL · cURL**

```bash
# S3File stores the content in Coevera's file storage.
file=$(jq -n --arg content "$(b64 'Fabrikam Robotics pilot scope: telemetry from 12 assembly lines, 6 weeks.')" '{
  query: "mutation UploadFile($input: CreateCloudObjectInput!) { createCloudObject(input: $input) { cloudObject { id filename size mimeType } } }",
  variables: { input: { filename: "pilot-scope.txt", type: "S3File", content: $content } }
}' | gql)
echo "$file"
FILE_ID=$(jq -er '.data.createCloudObject.cloudObject.id' <<<"$file")
```

**GraphQL · TypeScript**

```ts
// S3File stores the content in Coevera's file storage.
const uploaded = await gql(
  `mutation UploadFile($input: CreateCloudObjectInput!) {
    createCloudObject(input: $input) { cloudObject { id filename size mimeType } }
  }`,
  {
    input: {
      filename: "pilot-scope.txt",
      type: "S3File",
      content: base64("Fabrikam Robotics pilot scope: telemetry from 12 assembly lines, 6 weeks.\n"),
    },
  },
);
const fileId: string = uploaded.createCloudObject.cloudObject.id;
console.log("File:", uploaded.createCloudObject.cloudObject.filename, fileId);
```

**GraphQL · Python**

```python
# S3File stores the content in Coevera's file storage.
uploaded = gql(
    """
    mutation UploadFile($input: CreateCloudObjectInput!) {
      createCloudObject(input: $input) { cloudObject { id filename size mimeType } }
    }
    """,
    {
        "input": {
            "filename": "pilot-scope.txt",
            "type": "S3File",
            "content": b64("Fabrikam Robotics pilot scope: telemetry from 12 assembly lines, 6 weeks.\n"),
        }
    },
)["createCloudObject"]["cloudObject"]
file_id = uploaded["id"]
print("File:", uploaded["filename"], file_id)
```

**Recorded response · REST · 201 Created**

```json
{
  "success": true,
  "data": {
    "is_delete_protected": false,
    "id": "16000000-0000-4000-a000-000000000001",
    "is_deleted": false,
    "modified": "2026-10-15 09:00:00.000000+00:00",
    "created": "2026-10-15 09:00:00.000041+00:00",
    "application": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Applications/18000000-0000-4000-a000-000000000001",
    "company_cloud_object_folder": null,
    "creator": null,
    "application_id": "18000000-0000-4000-a000-000000000001",
    "company_cloud_object_folder_id": null,
    "creator_id": null,
    "filename": "pilot-scope.txt",
    "is_public": false,
    "mime_type": "text/plain",
    "params": {},
    "size": 74,
    "type": 1,
    "upload_source": null,
    "url": "https://storage.example.com/files/redacted.txt?Expires=redacted&Signature=redacted",
    "revision": 880,
    "dimensions": null,
    "duration": null,
    "public_url": null
  }
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "createCloudObject": {
      "cloudObject": {
        "id": "16000000-0000-4000-a000-000000000001",
        "filename": "pilot-scope.txt",
        "size": 74,
        "mimeType": "text/plain"
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

`type` says where the file lives. Use `1` (GraphQL `S3File`) to upload content:

| `type`     | GraphQL                                                                       | What it is                                                                                                                  |
| ---------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `-1`       | `S3FileUpload`                                                                | A large-file upload through a pre-signed URL, which the public API does not issue: without a `url`, the create answers 422. |
| `1`        | `S3File`                                                                      | A file stored in Coevera, sent as base64 `content`.                                                                         |
| `2`        | `S3Image`                                                                     | An image stored in Coevera.                                                                                                 |
| `3` to `7` | `GoogleDriveFile`, `OneDriveFile`, `BoxFile`, `DropboxFile`, `SharepointFile` | A file in a connected cloud drive.                                                                                          |
| `8`        | `ExternalURL`                                                                 | A link to an external URL.                                                                                                  |
| `9`        | `AlfrescoFile`                                                                | A file in Alfresco.                                                                                                         |
| `10`       | `UrlFile`                                                                     | A file referenced by URL.                                                                                                   |

## 3. Link it to the account

A file on its own belongs to no record. Create a `CloudObjectRelations` row with its
`cloud_object_id` and the `account_id`.

**REST · cURL**

```bash
curl -sS --fail-with-body "$COEVERA_BASE/entities/CloudObjectRelations" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d "{ \"cloud_object_id\": \"$FILE_ID\", \"account_id\": \"$ACCOUNT_ID\" }" >/dev/null
echo "Linked to the account."
```

**REST · TypeScript**

```ts
await coevera("/entities/CloudObjectRelations", {
  method: "POST",
  body: { cloud_object_id: fileId, account_id: accountId },
});
console.log("Linked to the account.");
```

**REST · Python**

```python
coevera("POST", "/entities/CloudObjectRelations", json={"cloud_object_id": file_id, "account_id": account_id})
print("Linked to the account.")
```

**GraphQL · cURL**

```bash
jq -n --arg file "$FILE_ID" --arg account "$ACCOUNT_ID" '{
  query: "mutation LinkFile($input: CreateCloudObjectRelationInput!) { createCloudObjectRelation(input: $input) { cloudObjectRelation { id } } }",
  variables: { input: { cloudObjectId: $file, accountId: $account } }
}' | gql
```

**GraphQL · TypeScript**

```ts
await gql(
  `mutation LinkFile($input: CreateCloudObjectRelationInput!) {
    createCloudObjectRelation(input: $input) { cloudObjectRelation { id } }
  }`,
  { input: { cloudObjectId: fileId, accountId: account.id } },
);
console.log("Linked to the account.");
```

**GraphQL · Python**

```python
gql(
    """
    mutation LinkFile($input: CreateCloudObjectRelationInput!) {
      createCloudObjectRelation(input: $input) { cloudObjectRelation { id } }
    }
    """,
    {"input": {"cloudObjectId": file_id, "accountId": account["id"]}},
)
print("Linked to the account.")
```

**Recorded response · REST · 201 Created**

```json
{
  "success": true,
  "data": {
    "is_delete_protected": false,
    "id": "17000000-0000-4000-a000-000000000001",
    "is_deleted": false,
    "modified": "2026-10-15 09:00:00.368091+00:00",
    "created": "2026-10-15 09:00:00.368141+00:00",
    "account": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Accounts/0a000000-0000-4000-a000-000000000001",
    "appointment": null,
    "task": null,
    "approval_process": null,
    "call": null,
    "cloud_object_folder": null,
    "cloud_object": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/CloudObjects/16000000-0000-4000-a000-000000000001",
    "contact": null,
    "custom_entity": null,
    "email_sequence": null,
    "email_template": null,
    "field_mapping": null,
    "lead": null,
    "oppty": null,
    "media": null,
    "email": null,
    "memo": null,
    "online_form_field": null,
    "online_form": null,
    "process": null,
    "process_template": null,
    "project": null,
    "quote": null,
    "report_schedule": null,
    "text_message": null,
    "account_id": "0a000000-0000-4000-a000-000000000001",
    "activity_id": null,
    "approval_process_id": null,
    "call_id": null,
    "cloud_object_folder_id": null,
    "cloud_object_id": "16000000-0000-4000-a000-000000000001",
    "contact_id": null,
    "custom_entity_id": null,
    "email_sequence_id": null,
    "email_template_id": null,
    "field_mapping_id": null,
    "lead_oppty_id": null,
    "media_id": null,
    "message_id": null,
    "online_form_field_id": null,
    "online_form_id": null,
    "process_id": null,
    "process_template_id": null,
    "project_id": null,
    "quote_id": null,
    "report_schedule_id": null,
    "text_message_id": null,
    "revision": 881
  }
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "createCloudObjectRelation": {
      "cloudObjectRelation": {
        "id": "17000000-0000-4000-a000-000000000001"
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

## 4. Upload and link in one call

Nest the file in `cloud_object` (GraphQL `cloudObject`) of the relation create: the API stores the
file and links it in one call, and returns the new file's id in `cloud_object_id`. Use the
two-call path of steps 2 and 3 when one file goes on several records.

**REST · cURL**

```bash
# The file nested in the relation: uploaded and linked in one call.
attached=$(jq -n --arg account "$ACCOUNT_ID" \
  --arg content "$(b64 'Fabrikam Robotics site survey: Vancouver plant, 2026-11-03.')" \
  '{ account_id: $account, cloud_object: { filename: "site-survey.txt", type: 1, content: $content } }' |
  curl -sS --fail-with-body "$COEVERA_BASE/entities/CloudObjectRelations" \
    -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
    -H "Content-Type: application/json" \
    -d @-)
echo "$attached"
SECOND_FILE_ID=$(jq -er '.data.cloud_object_id' <<<"$attached")
```

**REST · TypeScript**

```ts
// The file nested in the relation: uploaded and linked in one call.
const attached = await coevera("/entities/CloudObjectRelations", {
  method: "POST",
  body: {
    account_id: accountId,
    cloud_object: {
      filename: "site-survey.txt",
      type: 1,
      content: base64("Fabrikam Robotics site survey: Vancouver plant, 2026-11-03.\n"),
    },
  },
});
const secondFileId: string = attached.data.cloud_object_id;
console.log("Second file:", secondFileId);
```

**REST · Python**

```python
# The file nested in the relation: uploaded and linked in one call.
attached = coevera(
    "POST",
    "/entities/CloudObjectRelations",
    json={
        "account_id": account_id,
        "cloud_object": {
            "filename": "site-survey.txt",
            "type": 1,
            "content": b64("Fabrikam Robotics site survey: Vancouver plant, 2026-11-03.\n"),
        },
    },
)
second_file_id = attached["data"]["cloud_object_id"]
print("Second file:", second_file_id)
```

**GraphQL · cURL**

```bash
# The file nested in the relation: uploaded and linked in one call.
attached=$(jq -n --arg account "$ACCOUNT_ID" \
  --arg content "$(b64 'Fabrikam Robotics site survey: Vancouver plant, 2026-11-03.')" '{
  query: "mutation AttachFile($input: CreateCloudObjectRelationInput!) { createCloudObjectRelation(input: $input) { cloudObjectRelation { id cloudObject { id filename } } } }",
  variables: { input: { accountId: $account, cloudObject: { filename: "site-survey.txt", type: "S3File", content: $content } } }
}' | gql)
echo "$attached"
SECOND_FILE_ID=$(jq -er '.data.createCloudObjectRelation.cloudObjectRelation.cloudObject.id' <<<"$attached")
```

**GraphQL · TypeScript**

```ts
// The file nested in the relation: uploaded and linked in one call.
const attached = await gql(
  `mutation AttachFile($input: CreateCloudObjectRelationInput!) {
    createCloudObjectRelation(input: $input) { cloudObjectRelation { id cloudObject { id filename } } }
  }`,
  {
    input: {
      accountId: account.id,
      cloudObject: {
        filename: "site-survey.txt",
        type: "S3File",
        content: base64("Fabrikam Robotics site survey: Vancouver plant, 2026-11-03.\n"),
      },
    },
  },
);
const secondFileId: string = attached.createCloudObjectRelation.cloudObjectRelation.cloudObject.id;
console.log("Second file:", secondFileId);
```

**GraphQL · Python**

```python
# The file nested in the relation: uploaded and linked in one call.
attached = gql(
    """
    mutation AttachFile($input: CreateCloudObjectRelationInput!) {
      createCloudObjectRelation(input: $input) { cloudObjectRelation { id cloudObject { id filename } } }
    }
    """,
    {
        "input": {
            "accountId": account["id"],
            "cloudObject": {
                "filename": "site-survey.txt",
                "type": "S3File",
                "content": b64("Fabrikam Robotics site survey: Vancouver plant, 2026-11-03.\n"),
            },
        }
    },
)
second_file_id = attached["createCloudObjectRelation"]["cloudObjectRelation"]["cloudObject"]["id"]
print("Second file:", second_file_id)
```

**Recorded response · REST · 201 Created**

```json
{
  "success": true,
  "data": {
    "is_delete_protected": false,
    "id": "17000000-0000-4000-a000-000000000002",
    "is_deleted": false,
    "modified": "2026-10-15 09:00:02.066531+00:00",
    "created": "2026-10-15 09:00:02.066601+00:00",
    "account": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Accounts/0a000000-0000-4000-a000-000000000001",
    "appointment": null,
    "task": null,
    "approval_process": null,
    "call": null,
    "cloud_object_folder": null,
    "cloud_object": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/CloudObjects/16000000-0000-4000-a000-000000000002",
    "contact": null,
    "custom_entity": null,
    "email_sequence": null,
    "email_template": null,
    "field_mapping": null,
    "lead": null,
    "oppty": null,
    "media": null,
    "email": null,
    "memo": null,
    "online_form_field": null,
    "online_form": null,
    "process": null,
    "process_template": null,
    "project": null,
    "quote": null,
    "report_schedule": null,
    "text_message": null,
    "account_id": "0a000000-0000-4000-a000-000000000001",
    "activity_id": null,
    "approval_process_id": null,
    "call_id": null,
    "cloud_object_folder_id": null,
    "cloud_object_id": "16000000-0000-4000-a000-000000000002",
    "contact_id": null,
    "custom_entity_id": null,
    "email_sequence_id": null,
    "email_template_id": null,
    "field_mapping_id": null,
    "lead_oppty_id": null,
    "media_id": null,
    "message_id": null,
    "online_form_field_id": null,
    "online_form_id": null,
    "process_id": null,
    "process_template_id": null,
    "project_id": null,
    "quote_id": null,
    "report_schedule_id": null,
    "text_message_id": null,
    "revision": 882
  }
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "createCloudObjectRelation": {
      "cloudObjectRelation": {
        "id": "17000000-0000-4000-a000-000000000002",
        "cloudObject": {
          "id": "16000000-0000-4000-a000-000000000002",
          "filename": "site-survey.txt"
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

## 5. Read them back

Read the account's relations for the two files, with the files expanded. In REST, a raw comma
between the two ids in `filter[cloud_object_id]` is an OR; an encoded `%2C` would be a literal
comma and match nothing; see [Querying](/concepts/querying/#filter).

**REST · cURL**

```bash
# A raw comma separates the two ids: an OR filter.
curl -sS --fail-with-body -G "$COEVERA_BASE/entities/CloudObjectRelations" \
  -d "filter[account_id]=$ACCOUNT_ID" \
  -d "filter[cloud_object_id]=$FILE_ID,$SECOND_FILE_ID" \
  -d expand=cloud_object \
  -d load-only=id,cloud_object.filename,cloud_object.size,cloud_object.mime_type,cloud_object.type \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
```

**REST · TypeScript**

```ts
// A raw comma separates the two ids: an OR filter.
const documents = await coevera(
  `/entities/CloudObjectRelations?filter[account_id]=${accountId}&filter[cloud_object_id]=${fileId},${secondFileId}` +
    "&expand=cloud_object&load-only=id,cloud_object.filename,cloud_object.size,cloud_object.mime_type,cloud_object.type",
);
console.log(documents.total, "documents:", JSON.stringify(documents.data, null, 2));
```

**REST · Python**

```python
# requests would send the comma as %2C, a literal comma: build the query string by hand so the
# raw comma separates the two ids (an OR filter).
documents = coevera(
    "GET",
    f"/entities/CloudObjectRelations?filter[account_id]={account_id}"
    f"&filter[cloud_object_id]={file_id},{second_file_id}"
    "&expand=cloud_object&load-only=id,cloud_object.filename,cloud_object.size,cloud_object.mime_type,cloud_object.type",
)
print(documents.get("total"), "documents:", json.dumps(documents["data"], indent=2))
```

**GraphQL · cURL**

```bash
jq -n --arg account "$ACCOUNT_ID" --arg first "$FILE_ID" --arg second "$SECOND_FILE_ID" '{
  query: "query ReadBack($account: String!, $files: [String]!) { entities { cloudObjectRelation { getByCriteria(filter: { accountId: { eq: [$account] }, cloudObjectId: { eq: $files } }) { totalCount edges { node { id cloudObject { filename size mimeType type } } } } } } }",
  variables: { account: $account, files: [$first, $second] }
}' | gql
```

**GraphQL · TypeScript**

```ts
const documents = await gql(
  `query ReadBack($accountId: String!, $fileIds: [String]!) {
    entities {
      cloudObjectRelation {
        getByCriteria(filter: { accountId: { eq: [$accountId] }, cloudObjectId: { eq: $fileIds } }) {
          totalCount
          edges { node { id cloudObject { filename size mimeType type } } }
        }
      }
    }
  }`,
  { accountId: account.id, fileIds: [fileId, secondFileId] },
);
const relations = documents.entities.cloudObjectRelation.getByCriteria;
console.log(relations.totalCount, "documents:", JSON.stringify(relations.edges, null, 2));
```

**GraphQL · Python**

```python
documents = gql(
    """
    query ReadBack($accountId: String!, $fileIds: [String]!) {
      entities {
        cloudObjectRelation {
          getByCriteria(filter: { accountId: { eq: [$accountId] }, cloudObjectId: { eq: $fileIds } }) {
            totalCount
            edges { node { id cloudObject { filename size mimeType type } } }
          }
        }
      }
    }
    """,
    {"accountId": account["id"], "fileIds": [file_id, second_file_id]},
)
relations = documents["entities"]["cloudObjectRelation"]["getByCriteria"]
print(relations["totalCount"], "documents:", json.dumps(relations["edges"], indent=2))
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 2,
  "page_info": {
    "start_cursor": "WyIxNzAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIxNzAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDIiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "id": "17000000-0000-4000-a000-000000000001",
      "cloud_object": {
        "filename": "pilot-scope.txt",
        "mime_type": "text/plain",
        "size": 74,
        "type": 1
      }
    },
    {
      "id": "17000000-0000-4000-a000-000000000002",
      "cloud_object": {
        "filename": "site-survey.txt",
        "mime_type": "text/plain",
        "size": 60,
        "type": 1
      }
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "cloudObjectRelation": {
        "getByCriteria": {
          "totalCount": 2,
          "edges": [
            {
              "node": {
                "id": "17000000-0000-4000-a000-000000000001",
                "cloudObject": {
                  "filename": "pilot-scope.txt",
                  "size": 74,
                  "mimeType": "text/plain",
                  "type": "S3File"
                }
              }
            },
            {
              "node": {
                "id": "17000000-0000-4000-a000-000000000002",
                "cloudObject": {
                  "filename": "site-survey.txt",
                  "size": 60,
                  "mimeType": "text/plain",
                  "type": "S3File"
                }
              }
            }
          ]
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

## What to check

- Steps 2 to 4 answer **201** (GraphQL: 200).
- Step 2 returns the `size` in bytes of what you sent and the detected `mime_type`.
- Step 5 returns `total: 2` (GraphQL `totalCount: 2`), with each file's `filename`, `size` and
  `type`.
- Run the program twice and the account has four documents: the API does not refuse two files
  with the same name. To make it safe to repeat, list the account's documents by `filename`
  first.

## Clean up

The program leaves two files and two relations in your space. Delete the relations with
`POST /entities/CloudObjectRelations/batch-delete`, then the files with
`POST /entities/CloudObjects/batch-delete`: deleting a relation leaves its file in place. Deletes
are soft.

## Limits

- The largest file is the space's Attachment Size Limit, which the API cannot read. A larger
  upload is refused with HTTP 400, code 117.
- The content travels base64-encoded in one JSON request; the public API has no chunked or
  resumable upload.
- The program does not download files. A file's `url` is a signed link that expires.
