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.
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_ and $COEVERA_. |
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:
export COEVERA_API_KEY="<application user name>"
export COEVERA_API_SECRET="<application password>" # never commit itCOEVERA_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"]){
"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"
}
]
}{
"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){
"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
{
"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. |
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.
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 } }
}' | gqlawait 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."){
"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
{
"data": {
"createCloudObjectRelation": {
"cloudObjectRelation": {
"id": "17000000-0000-4000-a000-000000000001"
}
}
}
}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.
# 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){
"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
{
"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] }
}' | gqlconst 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)){
"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
{
"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
sizein bytes of what you sent and the detectedmime_type. - Step 5 returns
total: 2(GraphQLtotalCount: 2), with each file'sfilename,sizeandtype. - 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
filenamefirst.
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
urlis a signed link that expires.