Developer

Attachments APIs

What are Attachments?

Attachments allow the user to pair together two elements so that they act as a single, interactable object.

In a Bluescape Workspace, a user has the ability to manually drag an element A on top of an element B in order to form a link between the two, to group them. Now whenever the user selects or moves one element, the other will be selected and moved as well. The Attachments APIs allow for a programmatic way to perform these attachment operations.

Coordinates can be specified in order to determine where the element to attach will be placed on the base element. It is important to note that when providing coordinates, these inputs will be relative to the base element they are being attached to. For example, if the user specifies the element to be attached at [0,0] this will place the attachment at the top left corner of the base element, and not at the center of the workspace.

When executing an Attachments API, the user will see the source element be moved onto the base element specified, and a yellow border will appear around the newly attached objects. When deleting an Attachment (or detaching), the user will see a flashing blue border around the element that was detached, this to signify the operation is complete.
NOTE: The object to which an element is attached is referred as the surface in this page.

Please review the page for attachment APIs in v2.

Table of Contents:

Allowed Elements and Surfaces for Attachments
How to Implement Attachments APIs: REST APIs
1. Create: attachment an element to another element, REST
2. Get: list all attachments of an element, REST
3. Detach: remove attachment of an element, REST
How to Implement Attachments APIs: GraphQL APIs
A. Create: attachment an element to another element, GraphQL
B. Get: list all attachments of an element, GraphQL
C. Detach: remove attachment of an element, GraphQL
Use Case Example

Allowed Elements and Surfaces for Attachments

The elements to which you can attach other elements are in the following list:

Attachable objects Surfaces (elements to which attachable object can attach to)
  • Text
  • LegacyNote
  • Image
  • Document
  • Browser
  • Video
  • Stroke
  • Shape
  • Line
  • Window
  • Whiteboard
  • LinkedDocument
  • Browser
  • Document
  • Image
  • Note
  • Shape
  • Video
  • Window

How to Implement Attachments APIs: REST APIs

Create: attach one element to another, REST

Endpoint /v3/workspaces/<workspace_uid>/elements/<surface_id>/attachments
Method POST
Comments Specify in the payload the Id of the element you want to attach to, the element indicated with <surface_id>.

Sample Request:

var request = require('request');

const token = <SET_TOKEN>;
const portal = '';
const workspace_uid = <SET_WORKSPACE_UID>;
const api_version = 'v3';
var api_endpoint = '/' + api_version + '/workspaces/' + workspace_uid + '/elements/';

function runRequest(portal,api_endpoint,method,data_load) {
    return new Promise((resolve, reject) => {
        var request_values = {
            uri : portal + api_endpoint ,
            method : method ,
            headers : {
                'Authorization': "Bearer " + token,
                'Content-Type' : 'application/json'    
            },
            body : data_load,
            json: true
        };

        request(request_values, function (error, response, body) {
            if (!error && response.statusCode == 200) {
                // Print out the result
                console.log("Successful request. Body response: " + JSON.stringify(body,null,"     ")); 
                } else {
                reject('Invalid status code <' + response.statusCode + '>');
                }
                resolve(body);
        })

    });
}

async function runAPIRequests() {
    api_endpoint = '/v3' + '/workspaces/' + workspace_uid + '/elements/<SURFACE_ID>/attachments'

    data_load = {
        'sourceId': "<SET_SOURCE_ID>"
    }

    method = 'POST';

    try {
        var uploadResponse = await runRequest(portal,api_endpoint,method,data_load);
    } catch (error) {
        console.error('ERROR  attaching ');
        console.error(error);
    }
}
runAPIRequests();
    
import requests
from requests.exceptions import HTTPError
import pprint

token = '<SET_TOKEN>'
portal = ''

workspace_uid = '<SET_WORKSPACE_UID>'
api_version = '/v3/'

if __name__ == "__main__":

    API_endpoint = API_endpoint = portal + api_version + 'workspaces/' + workspace_uid + '/elements/<SURFACE_ID>/attachments'

    data_load = {
        'sourceId': '<SET_SOURCE_ID>',     # Example showing how to use the filter_by field
        'x' : 10,
        'y' : 10
    }

    the_request = requests.post(
            API_endpoint,
            headers={"Authorization": "Bearer " + token,
                        "Content-Type": "application/json"
                    },
            json = data_load
        )

    json_response = the_request.json()
    pprint.pprint(json_response)
curl -X POST /v3/workspaces/<workspace_uid>/elements/<surface_id>/attachments \
-H 'Authorization: Bearer <SET_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
    "attachment" : "<SET_ID_OF_ELEMENT_TO_ATTACH_TO_SURFACE_ID>",
    "transform" : {
        "x": 10,
        "y": 10
    }

}'


Sample Response Body:

{
    "data": {
        "id": "<ID_OF_ATTACHED_ELEMENT>"
    }
}

Get: list all attachments of an element

Endpoint /v3/workspaces/<workspace_uid>/elements/<surface_id>/attachments
Method GET
Comments

Sample Request:

var request = require('request');

const token = <SET_TOKEN>;
const portal = '';
const workspace_uid = <SET_WORKSPACE_UID>;
const api_version = 'v3';
var api_endpoint = '/' + api_version + '/workspaces/' + workspace_uid + '/elements/attachments';

function runRequest(portal,api_endpoint,method,data_load) {
    return new Promise((resolve, reject) => {
        var request_values = {
            uri : portal + api_endpoint ,
            method : method ,
            headers : {
                'Authorization': "Bearer " + token,
                'Content-Type' : 'application/json'    
            },
            body : data_load,
            json: true
        };

        request(request_values, function (error, response, body) {
            if (!error && response.statusCode == 200) {
                // Print out the result
                console.log("Successful request. Body response: " + JSON.stringify(body,null,"     ")); 
                } else {
                    reject('Invalid status code <' + response.statusCode + '>'); 
                }
                resolve(body);
        })

    });
}

async function runAPIRequests() {
    api_endpoint = '/v3' + '/workspaces/' + workspace_uid + '/elements/<SURFACE_ID>/attachments'

    method = 'GET';

    try {
        var uploadResponse = await runRequest(portal,api_endpoint,method);
    } catch (error) {
        console.error('ERROR getting attachments ');
        console.error(error);
    }
}
runAPIRequests();  
import requests
from requests.exceptions import HTTPError
import pprint

token = '<SET_TOKEN>'
portal = ''
workspace_uid = '<SET_WORKSPACE_UID>'
api_version = '/v3/'


if __name__ == "__main__":
    API_endpoint = portal + api_version + 'workspaces/' + workspace_uid + '/elements/<SURFACE_ID>/attachments'

    the_request = requests.get(
            API_endpoint,
            headers={"Authorization": "Bearer " + token
                    }
        )

    json_response = the_request.json()
    pprint.pprint(json_response)
curl -X GET /v3/workspaces/<workspace_uid>/elements/<surface_id>/attachments \
-H 'Authorization: Bearer <SET_TOKEN>' \
-H 'Content-Type: application/json' 

Sample Response Body with the list of attachments an object has:
  • This is the list of elements attached to the element <surface_id>:
{
    "data": [
        {
            "id": "<ID_OF_ATTACHED_ELEMENT>",
            "zIndex": 1357,
            "transform": {
                "x": 10,
                "y": 10,
                "scaleX": 1,
                "scaleY": 1
            },
            "pinned": false,
            "comments": [],
            "surface": "<surface_id>",
            "type": "Text",
            "text": "Text to Attach",
            "style": {
                ...
            },
            "blocks": [
                ...
            ]
        }
    ]
}

Detach: remove attachment of an element, REST

To detach an element (remove it from being attached to a surface), you will need to use the following API:

Endpoint /v3/workspaces/<workspace_uid>/elements/<surface_id>/attachments/<element_id>
Method DELETE
Comments Here you will remove the element with Id element_id from the surface with Id surface_id.

Sample Request:

var request = require('request');

const token = <SET_TOKEN>;
const portal = '';
const workspace_uid = <SET_WORKSPACE_UID>;
const api_version = 'v3';
var api_endpoint = '/' + api_version + '/workspaces/' + workspace_uid + '/elements/attachments';

function runRequest(portal,api_endpoint,method,data_load) {
    return new Promise((resolve, reject) => {
        var request_values = {
            uri : portal + api_endpoint ,
            method : method ,
            headers : {
                'Authorization': "Bearer " + token,
                'Content-Type' : 'application/json'    
            },
            json: true
        };

        request(request_values, function (error, response, body) {
            if (!error && response.statusCode == 200) {
                // Print out the result
                console.log("Successful request. Body response: " + JSON.stringify(body,null,"     ")); 
                } else {
                reject('Invalid status code <' + response.statusCode + '>'); 
                }
                resolve(body);
        })

    });
}
async function runAPIRequests() {
    api_endpoint = '/v3' + '/workspaces/' + workspace_uid + '/elements/<SURFACE_ID>/attachments/<ATTACHMENT_ID>'

    method = 'DELETE';

    try {
        var uploadResponse = await runRequest(portal,api_endpoint,method);
    } catch (error) {
        console.error('ERROR deleting attachment ');
        console.error(error);
    }
}
runAPIRequests();
import requests
from requests.exceptions import HTTPError
import pprint

token = '<SET_TOKEN>'
portal = ''
workspace_uid = '<SET_WORKSPACE_UID>'
api_version = '/v3/'


if __name__ == "__main__":
    API_endpoint = portal + api_version + 'workspaces/' + workspace_uid + '/elements/<SURFACE_ID>/attachments/<ATTACHMENT_ID>'

    the_request = requests.delete(
            API_endpoint,
            headers={"Authorization": "Bearer " + token
                    }
        )

    json_response = the_request.json()
    pprint.pprint(json_response)
curl -X DELETE /v3/workspaces/<workspace_uid>/elements/<surface_id>/attachments/<attachment_id> \
-H 'Authorization: Bearer <SET_TOKEN>' \
-H 'Content-Type: application/json' 


Sample Response Body after deleting the attachments in an element:

{
  "message": "Attachment {source_id} has been removed from the surface {surface_id}"
}  

Create: attach one element to another, GraphQL

In graphQL, you perform the attachment of an element to another element (surface) by setting the ID of the surface element in the surface field when creating or updating an element.
The example below shows how to attach a Text element to another element on creation, using the createText mutation, or later by updating it with the updateText mutation.

mutation createText or updateText
Method POST
Comments
  • Specify in the payload the Id of the element you want to attach to, the element indicated with <surface_id>.
  • The positioning of attachment is relative to top-left corner of the surface

Here is the example for an attachment when creating a Text element:

mutation createTextExample($workspaceId: String!, $input: CreateTextInput!){
    createText(workspaceId: $workspaceId, input: $input) {
        id
    }
}

using these variables:

{
    "workspaceId" : "<SET_WORKSPACE_UID>",
    "input" : {       
        "transform" : { "x": 10, "y":10 },
        "text" : "This text is attached",
        "surface": "<SET_SURFACE_ID>"
    }
}

Sample Request:

const axios = require('axios');

const url = "/v3/graphql"
const token = "<SET_TOKEN>"
const workspaceId = "<SET_WORKSPACEID>"

const textMutation = 
    `mutation createTextExample($workspaceId: String!, $input: CreateTextInput!){
            createText(workspaceId: $workspaceId, input: $input) {
            __typename
            id
            text
            transform { x y }
            }
        }`
const variables = {   
    "workspaceId": workspaceId,
    "input": {
        "text": "This text is attached",
        "surface": "<SET_SURFACE_ID>"
        "transform": {
            "x": 10,
            "y": 10
        }
    }
}

const attachText = async () => {
    try {
        const response = await axios.post(url,
            {
                query: textMutation,
                variables: variables
            }, 
            {headers: {"Authorization": "Bearer " + token}})
        
        console.log(response.data);
        return response
    }
    catch(error) {
        console.error(error)
    }
}

attachText();
    
import requests
from requests.exceptions import HTTPError
import pprint

token = '<SET_TOKEN>'
portal = '/v3/graphql'

workspace_uid = '<SET_WORKSPACE_UID>'

if __name__ == "__main__":

    API_endpoint = portal

    textMutation = """
    mutation createTextExample($workspaceId: String!, $input: CreateTextInput!){
        createText(workspaceId: $workspaceId, input: $input) {
            __typename
            id
            text
            transform { x y }
        }
    }
    """

    graphqlVariables = {
        "workspaceId": workspaceId,
        "input": {
            "text": "This text is attached",
            "surface": "<SET_SURFACE_ID>",
            "transform": {
                "x": 10,
                "y": 10
            }
    }

    the_request = requests.post(
            API_endpoint,
            headers={"Authorization": "Bearer " + token,
                        "Content-Type": "application/json"
                    },
            json = json={
            'query': textMutation,
            'variables': graphqlVariables
        }
        )

    json_response = the_request.json()
    pprint.pprint(json_response)
curl -X POST /v3/graphql
-H 'Authorization: Bearer <SET_TOKEN>' \
-H 'Content-Type: application/json' \
--data-raw '{"query":"mutation createTextExample($workspaceId: String!, $input: CreateTextInput!){\n    createText(workspaceId: $workspaceId, input: $input) {\n        id\n    }\n}    ","variables":{"workspaceId":"<SET_WORKSPACE_UID>","input":{"transform":{"x":10,"y":10},"text":"This text is attached","surface":"<SET_SURFACE_ID>"}}}''

Sample Response Body:

{
    "data": {
        "id": "<ID_OF_ATTACHED_ELEMENT>"
    }
}

How to Implement Attachments APIs: GRAPHQL APIs

Get: list all attachments of an element, GraphQL

query elements
Method POST
Comments

Here is the example a query to get the details of the attachments for an element. For this example, we added the settings to list the attachment details for an Image, but you can extend it to any specific element you want:

query getDetailsOfAttachments($workspaceId: String! $elementId: String!){
    elements(workspaceId: $workspaceId  id: $elementId) {
        type: __typename        
        id
        transform {
            x
            y
        }

        ... on Image {
          attachments {
            id
            type: __typename
            transform {x y}
            ... on Text {
              text
            }
          }
        }
    }
}

using these variables:

{
    "workspaceId" : "<SET_WORKSPACE_UID>",
    "elementId" : "<SET_SURFACE_ID>"
}

Detach: remove an element's attachments, GraphQL

In graphQL, to detach or remove an element's attachment, you need to set the surface field to the value of the Id of the workspace where you are working. This will need to be done using an update mutation.
The example below shows how to detach a Text element from the surface it is attached to. We will use the updateText mutation.

mutation updateText
Method POST
Comments You will need to know the Id of the Text object to detach

Here is the example a query to get the details of the attachments for an element. For this example, we added the settings to list the attachment details for an Image, but you can extend it to any specific element you want:

mutation detachTextElement($workspaceId: String!  $textElementId: String!) {
  updateText(workspaceId: $workspaceId , id: $textElementId, input: {
    surface: $workspaceId
  })
  {
    id
  }
  
}

using these variables:

{
    "workspaceId" : "<SET_WORKSPACE_UID>",
    "textElementId" : "<SET_TEXT_ELEMENT_ID_TO_DETACH>"
}

Use Case Example

Below is a simple, end-to-end example of how someone might use the Attachments APIs. This example is one in which a user uploads content from a local directory into a Bluescape workspace, generates text fields corresponding to the names of the uploaded files, and attaches the text fields to the files. An e-mail is then sent to the specified recipient notifying them of the changes that were made within the workspace.

Select Node.js or Python
// Use Case Example (Node.js)

var request = require('request');
var path = require('path');
var fs = require('fs');
var nodemailer = require('nodemailer');

/*
How to run:
node this_script_name.js 

Requires the following modules: fs, nodemailer, request, path
Ex: npm install <module_name>

This script:
1) Creates a canvas
2) Uploads the content from a specific folder into the canvas: documents and images
3) Create a text field for each file, within the Bluescape workspace, containing the file's name
4) Attach the filename text fields to their corresponding files 
5) Send an e-mail to the specified recipient, notifying them of the changes that were made 

*/

const token = <SET_TOKEN>;
const portal = '';
const workspace_uid = <SET_WORKSPACE_UID>;
const api_version = 'v3';
var api_endpoint = '/' + api_version + '/workspaces/' + workspace_uid + '/elements/images';
var method = '';

const canvas_x = 0;
const canvas_y = 0;

const canvas_width = 3000;
const canvas_height = 6000;

// Canvas coordinates are from its top left corner, relative from there
var xx = 100;
var yy = 200;

function runUploadRequest(portal,api_endpoint,method,data_load){
    return new Promise((resolve, reject) => {
        var request_values = {
            uri : portal + api_endpoint ,
            method : method ,
            headers : {
                'Authorization': "Bearer " + token,
                'Content-Type' : 'multipart/form-data'    
            },
            formData : data_load,
            json: true
        };

        request(request_values, function (error, response, body) {
            if (!error && response.statusCode == 200) {
                // Print out the result
                console.log("Successful request. Body response: " + JSON.stringify(body,null,"     ")); 
                } else {
                reject('Invalid status code <' + response.statusCode + '>'); 
                }
                resolve(body);
        })
    });
}

function runRequest(portal,api_endpoint,method,data_load) {
    return new Promise((resolve, reject) => {
        var request_values = {
            uri : portal + api_endpoint ,
            method : method ,
            headers : {
                'Authorization': "Bearer " + token,
                'Content-Type' : 'application/json'    
            },
            body : data_load,
            json: true
        };

        request(request_values, function (error, response, body) {
            if (!error && response.statusCode == 200) {
                // Print out the result
                console.log("Successful request. Body response: " + JSON.stringify(body,null,"     ")); 
                } else {
                reject('Invalid status code <' + response.statusCode + '>'); 
                }
                resolve(body);
        })

    });
}

async function asyncForEach(array, callback) {
    for (let index = 0; index < array.length; index++) {
        await callback(array[index], index, array);
    }
}

async function runAPIRequests() {

    /*****************************
    **    1) Create a canvas:   ** 
    *****************************/

    try {           
        api_endpoint = '/v3/workspaces/' + workspace_uid + '/elements/canvas';
        method = 'POST';
        var currtime = new Date().toISOString().slice(0,16).replace('T',' ');

        var data_load = {
            'x': canvas_x,
            'y': canvas_y,
            'width': canvas_width,
            'height': canvas_height,
            'name': "New Canvas - Creation time: " + currtime ,
            'borderColor': 'Red'
        }
        
        const canvasResponse = await runRequest(portal,api_endpoint,method,data_load);

        const canvas_id = canvasResponse['canvas']['id'];

        /****************************************
        **    2) Upload content into canvas:   ** 
        ****************************************/

        // Supported: png, jpeg, gif, tiff, pdf, docx, pptx, xlsx, doc, ppt, xls

        var allowedDocExtensions = new RegExp(/.*\.(pdf|docx|pptx|xlsx|doc|ppt|xls)$/i);
        var allowedImageExtensions = new RegExp(/.*\.(jpg|png|jpeg|tiff)$/i)

        const pathSource = <SET_FILE_PATH>
        var myPath = path.dirname(pathSource +'*'); // Absolute path
        // passsing myPath and callback function
        fs.readdir(myPath, function (errorIssue, files) {
            //handling error
            if (errorIssue) {
                return console.log('Unable to read directory: ' + errorIssue);
            } 
                
            asyncForEach(files, async(filename) => {

                var api_endpoint = '';
                var file_type_key = '';

                var is_supported_extension = true;

                // Check if the file is a supported one for uploading.
                if (allowedDocExtensions.test(filename) ){
                    api_endpoint = '/v3/workspaces/' + workspace_uid + '/elements/canvas/' + canvas_id + '/documents';
                    file_type_key = 'document';
                } else if (allowedImageExtensions.test(filename)) {
                    api_endpoint = '/v3/workspaces/' + workspace_uid + '/elements/canvas/' + canvas_id + '/images';
                    file_type_key = 'image';
                } else {
                    is_supported_extension = false;
                }

                if (is_supported_extension) {
                    console.log("Going to update: " + filename);
                    console.log("File type: " + file_type_key)

                    var data_load = {
                        'x': xx,
                        'y': yy,
                        'title': filename,
                        'scale': 1,
                    };

                    // Cannot add values for the keys using a variable name, it uses the name of the variable instead of its value.
                    data_load[file_type_key] = fs.createReadStream( pathSource + filename );

                    method = 'POST';

                    try {
                        var uploadResponse = await runUploadRequest(portal,api_endpoint,method,data_load);
                    } catch (error) {
                        console.error('ERROR  when uploading content for file '+ filename);
                        console.error(error);
                    }
                    
                    var file_id = uploadResponse[file_type_key]['id'];
                    var file_name = uploadResponse[file_type_key]['title'];

                    /****************************************************
                    **    3) Create text field containing file_name:   ** 
                    ****************************************************/

                    api_endpoint = '/v3/workspaces/' + workspace_uid + '/elements/canvas/' + canvas_id + '/text';
                    
                    data_load = {
                        'text': file_name,
                        'fontFamily': 'Aleo'
                    }

                    method = 'POST';

                    try {
                        var uploadResponse = await runRequest(portal,api_endpoint,method,data_load);
                    } catch (error) {
                        console.error('ERROR  creating text for '+ filename);
                        console.error(error);
                    }

                    var text_id = uploadResponse['text']['id'];

                    const sleep = (waitTime) => new Promise(resolve => setTimeout(resolve, waitTime));
                    await sleep(3000);
                    
                    /*******************************************
                    **    4) Attach new text field to file:   ** 
                    ********************************************/

                    api_endpoint = '/v3' + '/workspaces/' + workspace_uid + '/elements/'+file_type_key+'s/'+file_id+'/attachments'

                    data_load = {
                        'sourceId': text_id
                    }

                    method = 'POST';

                    try {
                        var uploadResponse = await runRequest(portal,api_endpoint,method,data_load);
                    } catch (error) {
                        console.error('ERROR  attaching ');
                        console.error(error);
                    }
                    

                    /*************************
                    **    5) Send e-mail:   ** 
                    *************************/

                    var transporter = nodemailer.createTransport({
                    service: 'gmail',
                    auth: {
                        user: '',
                        pass: ''
                    }
                    });
                    var mailOptions = {
                    from: '<INSERT_YOUR_EMAIL>',
                    to: '<INSERT_DESTINATION_EMAIL>',
                    subject: 'Attachment Canvas is Ready for Review',
                    text: 'An attachment canvas is ready for review. You can reach this workspace here:' + '' + '/' + workspace_uid
                    };
                    transporter.sendMail(mailOptions, function(error, info){
                    if (error) {
                        console.log(error);
                    } else {
                        console.log('Email sent: ' + info.response);
                    }
                    });

                    delta = 1600;
                    yy += delta;

                    //  Check if the object's position is outside the canvas. If so, start in new column
                    if (yy > canvas_height){
                        yy = 200;
                        xx += delta;
                    }                      
                }
            } ); 
        });


    } catch (error) {
        console.error('ERROR:');
        console.error(error);
    }
}

// Run the requests
runAPIRequests();       
# Use Case Example (Python)

import requests
from requests.exceptions import HTTPError
import datetime
from os import listdir
from os.path import isfile, join
import pprint
import time

'''
This script:
1) Creates a canvas
2) Uploads the content from a specific folder into the canvas: documents and images
3) Create a text field for each file, within the Bluescape workspace, containing the file's name
4) Attach the filename text fields to their corresponding files 
5) Send an e-mail to the specified recipient, notifying them of the changes that were made 

Required modules:
    requests 2.22.0
'''

token = '<SET_TOKEN>'

#############################
#    1) Create a canvas:    #
#############################

if __name__ == "__main__":
    portal = ''
    workspace_uid = '<SET_WORKSPACE_UID>'
    user_uid = ''
    API_version = 'v3'
    API_endpoint = '/' + API_version + '/workspaces/' + workspace_uid + '/elements/canvas'
    parameters = ''
    id_dict = {}

    canvas_x = 0
    canvas_y = 0

    canvas_width = 4000
    canvas_height = 4000

    date_time = datetime.datetime.now()

    data_load = {
        'x': canvas_x,
        'y': canvas_y,
        'width': canvas_width,
        'height': canvas_height,
        'name': "Data Load on " + str(date_time),
        'borderColor': 'Yellow'
    }

    # IMPORTANT: canvas has to be bigger than document/object coming inside, or the object does NOT stick to the canvas

    the_request = requests.post(
        portal + API_endpoint,
        headers={"Authorization": "Bearer " + token,
                 "Content-Type": "application/json"
                 },
        json=data_load,
        params=parameters
    )

    json_response = the_request.json()

    pprint.pprint(json_response)

    canvas_id = json_response['canvas']['id']
    
    ###########################################
    #    2) Upload content into the canvas:   #
    ###########################################
    
    mypath = '<SET_FILE_PATH>'
    onlyfiles = [f for f in listdir(mypath) if isfile(join(mypath, f))]

    API_endpoint = '/' + API_version + '/workspaces/' + workspace_uid + '/elements/canvas/' + canvas_id + '/documents'

    # Canvas coordinates are from its top left corner, relative from there
    xx = 100
    yy = 200


    # Supported: png, jpeg, gif, tiff, pdf, docx, pptx, xlsx, doc, ppt, xls

    document_extensions = ['pdf', 'docx', 'pptx', 'xlsx', 'doc', 'ppt', 'xls']
    image_extensions = ['jpg', 'png', 'jpeg', 'tiff']
    not_supported_extensions = ['txt']

    for this_file in onlyfiles:

        print("\nFile: ", this_file)

        file_extension = this_file.split('.')[1]

        full_path_file = mypath + this_file

        file_type_key = ''

        is_supported_extension = True

        if file_extension in document_extensions:
            API_endpoint = '/' + API_version + '/workspaces/' + workspace_uid + '/elements/canvas/' + canvas_id + '/documents'
            file_type_key = 'document'
        elif file_extension in image_extensions:
            API_endpoint = '/' + API_version + '/workspaces/' + workspace_uid + '/elements/canvas/' + canvas_id + '/images'
            file_type_key = 'image'
        else:
            is_supported_extension = False

        if is_supported_extension:
            the_params = {
                'x': xx,
                'y': yy,
                'scale': 1,
                'title' : this_file 
            }

            print("Data_load:", str(the_params))
            print(portal + API_endpoint)

            # NOTE for this library: do not use "Content-Type" in headers, does not need to be set to 'multipart/form-data', it is set automatically
            the_request = requests.post(
                portal + API_endpoint,
                headers={"Authorization": "Bearer " + token
                         },
                params=the_params,
                files={file_type_key: (this_file, open(full_path_file, "rb"))}
            )

            json_response = the_request.json()

            #pprint.pprint(json_response)  # uncomment if you want to see response
            
            if the_request.status_code == 200:
                print("object successfully loaded.")
            else:
                print("[[ERROR]] Could not upload object. Status code: " + str(the_request.status_code))

            file_id = json_response[file_type_key]['id']
            file_name = json_response[file_type_key]['title']

            ###################################################
            #   3) Create text field containing file_name:    #
            ###################################################

            API_endpoint = '/v3/workspaces/' + workspace_uid + '/elements/canvas/' + canvas_id + '/text'
            params = ''

            data_load = {
                'text': file_name,
                'fontFamily': 'Aleo'
            }

            the_request = requests.post(
                portal + API_endpoint,
                headers={"Authorization": "Bearer " + token,
                         "Content-Type": "application/json"
                         },
                json=data_load
            )

            json_response = the_request.json()
            pprint.pprint(json_response)
            text_id = json_response['text']['id']

            time.sleep(2)

            ##########################################
            #   4) Attach new text field to file:    #
            ##########################################

            API_endpoint = '/' + API_version + '/workspaces/' + workspace_uid + '/elements/'+file_type_key+'s/'+file_id+'/attachments'

            data_load = {
                'sourceId': text_id
            }

            # IMPORTANT: canvas has to be bigger than document/object coming inside, or the object does NOT stick to the canvas

            the_request = requests.post(
                portal + API_endpoint,
                headers={"Authorization": "Bearer " + token,
                         "Content-Type": "application/json"
                         },
                json=data_load
            )

            json_response = the_request.json()
            pprint.pprint(json_response)
 
            ########################
            #   5) Send e-mail:    #
            ########################

            s = smtplib.SMTP(host='smtp.gmail.com', port=587)
            s.starttls()
            s.login('<INSERT_YOUR_EMAIL>', '<INSERT_EMAIL_PASSWORD>')
            msg = MIMEMultipart()
            message = "An attachment canvas is ready for review. You can reach this workspace here: " + "" + "/" + workspace_uid
            msg['From'] = '<INSERT_YOUR_EMAIL>'
            msg['To'] = '<INSERT_DESTINATION_EMAIL>'
            msg['Subject'] = 'Attachment Canvas is Ready for Review'
            msg.attach(MIMEText(message, 'plain'))
            s.send_message(msg)
            del msg
            s.quit()

            delta = 1300
            if the_request.status_code == 200:
                yy += delta

            # Check if the objects's position is outside the canvas. If so, start in new column
            if yy > canvas_height:
                yy = 200
                xx += delta

If you have any questions or comments, please contact us at Bluescape support.