For Developers

Download, Modify, and Upload a File

Just want the code?

This example was built and tested on an integration using Canvas as the underlying source. Other LMS providers may differ slightly in what is supported.

Goal

Download a student's submission file, use AI to edit it, and re-upload it to Edlink.

Why

Interacting with Assignments, Submissions, and Files can be tricky. This guide exists to demonstrate a common use-case using these Edlink Data types.

How

Before this script will work, you'll need a few things:

  1. Your valid Graph API token
  2. A valid set of class + assignment + submission + file UUIDs
  3. A valid Claude API key (if you want to actually call out to a remote AI model)

This script will use that information to:

  1. Call the Download Submission File endpoint, saving the response file-bytes in memory.
  2. Call out to an AI, asking it to modify the file with your fancy prompt.
  3. Call the Update Submission endpoint, passing in the updated version of the file.

The code

Save this to markup-file.js.

// IMPORTANT TODO - you must replace these ids with valid ids for your integration
const CLASS_ID = '00000000-0000-0000-0000-000000000000';
const ASSIGNMENT_ID = '00000000-0000-0000-0000-000000000000';
const SUBMISSION_ID = '00000000-0000-0000-0000-000000000000';
const FILE_ID = '00000000-0000-0000-0000-000000000000';
// IMPORTANT TODO - you must also replace this with a valid key if you want to actually make an AI call
const CLAUDE_KEY = '';

const EDLINK_BASE_URL = 'https://ed.link/api/v2/graph';

const download_url = `/classes/${CLASS_ID}/assignments/${ASSIGNMENT_ID}/submissions/${SUBMISSION_ID}/files/${FILE_ID}/download`;

const access_token = process.argv[2];
if (!access_token) {
    console.error('access token must be provided as command line argument');
    process.exit(1);
}

// helper function for Edlink API calls
const fetchWithToken = async (url) => {
    if (url.startsWith('/')) {
        url = `${EDLINK_BASE_URL}${url}`;
    }

    const response = await fetch(url, {
        headers: {
            Authorization: `Bearer ${access_token}`
        }
    });

    if (!response.ok) {
        console.error(url, response);
        throw new Error(`HTTP error on ${url} Status: ${response.status}`);
    }
    return await response.text();
};

// IMPORTANT TODO: here is where you would integrate your ai model and prompt
const askAIToImproveTheFile = async (fileText) => {
    if (CLAUDE_KEY === '') {
        return `improved: ${fileText}`;
    } else {
        let claude_prompt = 'Modify the following text document to be a standard pretty markdown document:\n```';
        claude_prompt += fileText;
        claude_prompt += '\n```';
        const claude_response = await fetch('https://api.anthropic.com/v1/messages', {
            headers: {
                'Content-Type': 'application/json',
                'x-api-key': CLAUDE_KEY,
                'anthropic-version': '2023-06-01'
            },
            method: 'POST',
            body: JSON.stringify({
                model: 'claude-3-7-sonnet-20250219',
                max_tokens: 2048,
                messages: [{ role: 'user', content: claude_prompt }]
            })
        });
        return (await claude_response.json()).content[0].text;
    }
};

/// Usage Example ///
// 1. Download the file from Edlink, which is returned as raw bytes
let file = await fetchWithToken(download_url);
// 2. Modify the file to suit your application's purpose
file = await askAIToImproveTheFile(file);
// 3. Transform the file into the correct format
const uint8Array = new TextEncoder().encode(file);
// 4. Create the proper PATCH payload for modifying the Edlink Submission
const payload = JSON.stringify({
    attachments: [
        {
            type: 'file',
            title: 'overwritten.txt',
            data: Array.from(uint8Array)
        }
    ]
});
// 5. Send it to Edlink
const final_response = await fetch(`${EDLINK_BASE_URL}/classes/${CLASS_ID}/assignments/${ASSIGNMENT_ID}/submissions/${SUBMISSION_ID}`, {
    method: 'PATCH',
    headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${access_token}`
    },
    body: payload
});
// and... we're done
if (final_response.status === 200) {
    const body = await final_response.json();
    console.log('new file ID:', body['$data'].attachments[0].id);
    console.log('available at:', body['$data'].attachments[0].url);
} else {
    console.error('something went wrong:', final_response.status, await final_response.text());
}

Running node markup-file.js MY_INTEGRATION_ACCESS_TOKEN will run the script and cause the specified file submission to be downloaded, modified, and re-uploaded.