Developers
Set your API contextPick a region and space ID once. Every sample fills in your base URL.
Where do I find these?

In Coevera: open your space's API application and choose Show Access. It shows the space ID and the base URL, whose host starts with your region.

From the API: if you know the space ID, the discovery endpoint GET /api/discovery/spaces/{space_id} returns its region and base URLs, without credentials.

Only the region and space ID are kept, in this browser. Never paste a key or password here.

More in the quickstart

RecipesAttach a document

Attach a document

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.

  • 10 min
  • REST and GraphQL
  • cURL · TypeScript · Python
  • Verified in the docs sandbox on 2026-10-15

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 shows where the credentials come from, and Permissions and scopes where to set the permissions.

The operations this recipe calls, in the API reference: REST GET /entities/Accounts, POST /entities/CloudObjects, POST /entities/CloudObjectRelations, GET /entities/CloudObjectRelations; GraphQL AccountRepository.getByCriteria, createCloudObject, createCloudObjectRelation, CloudObjectRelationRepository.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:

Shell
export COEVERA_API_KEY="<application user name>"
export COEVERA_API_SECRET="<application password>"   # never commit it
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'; }
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");
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")
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'; }
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");
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.

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")
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);
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)
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")
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);
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"])
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "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"
    }
  ]
}
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "data": {
    "entities": {
      "account": {
        "getByCriteria": {
          "edges": [
            {
              "node": {
                "id": "0a000000-0000-4000-a000-000000000001",
                "name": "Fabrikam Robotics"
              }
            }
          ]
        }
      }
    }
  }
}

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.

# 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")
// 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);
# 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)
# 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")
// 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);
# 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)
201 CreatedRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "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
  }
}
Show all 28 linesShow less
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "data": {
    "createCloudObject": {
      "cloudObject": {
        "id": "16000000-0000-4000-a000-000000000001",
        "filename": "pilot-scope.txt",
        "size": 74,
        "mimeType": "text/plain"
      }
    }
  }
}

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.

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

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."
await coevera("/entities/CloudObjectRelations", {
  method: "POST",
  body: { cloud_object_id: fileId, account_id: accountId },
});
console.log("Linked to the account.");
coevera("POST", "/entities/CloudObjectRelations", json={"cloud_object_id": file_id, "account_id": account_id})
print("Linked to the account.")
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
await gql(
  `mutation LinkFile($input: CreateCloudObjectRelationInput!) {
    createCloudObjectRelation(input: $input) { cloudObjectRelation { id } }
  }`,
  { input: { cloudObjectId: fileId, accountId: account.id } },
);
console.log("Linked to the account.");
gql(
    """
    mutation LinkFile($input: CreateCloudObjectRelationInput!) {
      createCloudObjectRelation(input: $input) { cloudObjectRelation { id } }
    }
    """,
    {"input": {"cloudObjectId": file_id, "accountId": account["id"]}},
)
print("Linked to the account.")
201 CreatedRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "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
  }
}
Show all 58 linesShow less
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "data": {
    "createCloudObjectRelation": {
      "cloudObjectRelation": {
        "id": "17000000-0000-4000-a000-000000000001"
      }
    }
  }
}

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.

# 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")
// 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);
# 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)
# 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")
// 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);
# 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)
201 CreatedRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "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
  }
}
Show all 58 linesShow less
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "data": {
    "createCloudObjectRelation": {
      "cloudObjectRelation": {
        "id": "17000000-0000-4000-a000-000000000002",
        "cloudObject": {
          "id": "16000000-0000-4000-a000-000000000002",
          "filename": "site-survey.txt"
        }
      }
    }
  }
}

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.

# 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"
// 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));
# 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))
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
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));
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))
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "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
      }
    }
  ]
}
Show all 30 linesShow less
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "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"
                }
              }
            }
          ]
        }
      }
    }
  }
}
Show all 35 linesShow less

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.