Community Champion

Like many of you out there, here at Utah State University, we are struggling to move all of our classes online in an absurdly short period of time in response to COVID-19.

To help speed up the process, I have created some JavaScript to facilitate rapidly pulling a template course into an empty course shell and I am sharing it in the hopes that it can alleviate some of the headaches for other institutions.

I am providing the code for two contexts, uploading JavaScript to Canvas using the Canvas Theme Editor and creating a browser bookmarklet for those who do not have access to account-level JavaScript in Canvas.

A Note About Permissions

The code that follows will run as the logged-in user and will only work if the user has permission to import course content in Canvas.

The code is also scoped to only show on the front-page of an empty course so it should not be visible to students.

Account Level JavaScript

Canvas allows for adding CSS and JavaScript at an account level using the Canvas Theme Editor. For institutions that want to provide this type of functionality for an entire account, here are two options:

Single Template Course

If you have a single template course that you would like to provide an easy way to copy, the following code will create an Insert Base Template button in the right Canvas sidebar:

Insert Base Template Button

Here is the code that you would upload to Canvas. Update the templateCourse id on line 36 to your template course:

// Import Base Template button for the course Home Page
// This function will keep checking the progress url until process is complete or fails
function checkImportProgress(progressUrl) {
$.get(progressUrl, function(data) {
// Update the button to show we are checking again
$('.kl-import-template').html('Checking Progress');
// Four possible options
// 'queued', 'running', 'completed', 'failed'
let completed = false;
switch(data.workflow_state) {
case 'completed':
completed = true;
case 'failed':
alert('Import failed');
// For 'queued' or 'running'
setTimeout(function(){ checkImportProgress(progressUrl); }, 5000);
if (completed) {
// Reload the course home page
} else {
// Wait a bit and try again
setTimeout(function(){ checkImportProgress(progressUrl); }, 5000);
// Provide feedback of the current progress
$('.kl-import-template').html('Template Progress: ' + data.workflow_state);
}, 1000);
$(document).ready(function() {
// The Canvas course id for your template
let templateCourse = '593';
// Only add the button to the home page of courses without any other content in place
if (ENV.COURSE !== undefined && !== null && window.location.pathname === '/courses/' + && $('.ic-EmptyStateList:visible').length > 0) {
// Add the import button
let importButton = '<button class="btn btn-primary button-sidebar-wide kl-import-template"><i class="icon-download"></i> Import Base Template</button>';
// Bind action
$('.kl-import-template').unbind('click').on('click', function () {
// Prompt user so they have a chance to cancel
let confirmMessage = confirm("This will copy content into your course. Click OK to proceed.");
if (confirmMessage == true) {
$(this).html('Importing Base Template');
// Send import request to the Canvas API
$.post('/api/v1/courses/' + + '/content_migrations', {'migration_type': 'course_copy_importer', 'settings[source_course_id]': templateCourse}, function(data, textStatus, xhr) {
// Begin checking the progress url to see when import is complete

Multiple Template Courses

This next variation is for institutions with multiple templates. Instead of a single button, this will add a list of courses with the option to preview and a brief description:

Template list

Here is the code that you would upload to Canvas. Update the templateList that begins on line 4 to include a name, id, and description for each of your template courses:

// Create a list of templates on the home page of a blank course 
$(document).ready(function() {
// This is the list of your templates (Duplicate as needed)
let templateList = [
name: 'Basic Online Template',
id: '593',
description: 'Basic 8 week course with readings, assignments, and quizzes'
name: 'Another Template',
id: '1234',
description: 'This is a good course template'
// Function to check the Canvas progress url to see if the course has finished importing
function checkImportProgress(progressUrl, courseID) {
$.get(progressUrl, function(data) {
// Give a visual cue that we are going to check again
$('#kl-import-progress').html('Checking Progress');
// 'queued', 'running', 'completed', 'failed'
let completed = false;
switch(data.workflow_state) {
case 'completed':
completed = true;
$('#kl-import-progress').attr('class', 'alert alert-success');
case 'failed':
$('#kl-import-progress').attr('class', 'alert alert-error').html('Import failed');
// for 'queued' or 'running'
setTimeout(function(){ checkImportProgress(progressUrl, courseID); }, 5000);
if (completed) {
// Change from institution visibility to course
// If you set the template visibility to 'institution', users can preview before they copy
let parms = {
'course[is_public_to_auth_users]' : false,
'course[is_public]' : false
'url' : '/api/v1/courses/' + courseID,
'type' : 'PUT',
'data' : parms
// Import is complete, reload the page
} else {
// Import isn't finished, wait a bit and try again
setTimeout(function(){ checkImportProgress(progressUrl, courseID); }, 5000);
$('#kl-import-progress').html('Template Progress: ' + data.workflow_state);
}, 1000);
// Only add the button to the home page of courses without any other content in place (shows import option)
if (ENV.COURSE !== undefined && !== null && window.location.pathname === '/courses/' + && $('.ic-EmptyStateList:visible').length > 0) {
// Add a placeholder to the sidebar
$('.course-options').prepend('<div id="kl-template-list"></div><div id="kl-import-progress"></div>');
// Add each template to the list
$.each(templateList, function(index, val) {
let itemInfo = `<p>
<a href="/courses/${}" target="_blank" data-tooltip="left" title="${val.description}" class="Button Button--secondary kl-preview-template" style="padding: 2px 6px;"><i class="icon-eye"></i><span class="screenreader-only">View ${}</span></a>
<button class="Button Button--secondary kl-import-template" data-tooltip="top" title="Import ${}" data-courseid="${}" style="padding: 2px 6px;"><i class="icon-download"></i><span class="screenreader-only">Import ${}</span></button>
<span class="kl-template-name-${}">${}</span>
// When template import is clicked
$('.kl-import-template').unbind('click').on('click', function () {
// Give user a chance to cancel
let confirmMessage = confirm("This will copy content into your course. Click OK to proceed.");
if (confirmMessage == true) {
let templateCourse = $(this).attr('data-courseid');
let templateName = $(`.kl-template-name-${templateCourse}`).text();
$('#kl-import-progress').addClass('alert alert-info').html(`Importing ${templateName}`);
// Send import request to the Canvas API
$.post('/api/v1/courses/' + + '/content_migrations', {'migration_type': 'course_copy_importer', 'settings[source_course_id]': templateCourse}, function(data, textStatus, xhr) {
// Begin checking the progress url to see when import is complete

JavaScript Bookmarklets (No access to add JavaScript to Canvas)

Next, let's take a look at some options for those who do not have access to add JavaScript to Canvas using the Theme Editor at an account level.

What is a Bookmarklet?

A bookmarklet is similar to create a bookmark in your browser to take you to a webpage. The difference is that instead of opening a webpage, a bookmarklet will run some JavaScript.

How do I create a Bookmarklet?

  1. Create a bookmark in your browser (the same way you would create any bookmark).
  2. Edit the bookmark.
  3. Give it a name to make it easy for you to find.
  4. In the URL field, you are going to add some JavaScript (keep reading to learn what this will look like).

Hard-Coded Template Course

If you would like to add a bookmarklet that will always import the same course, this is the JavaScript code we will use (replace the template_course_id on line 2 with your template course):

// The id of our template course
let template_course_id = '593';
// Only run on the home page of courses without any other content in place (shows import option)
if (ENV.COURSE !== undefined && !== null && window.location.pathname === '/courses/' + && $('.ic-EmptyStateList:visible').length > 0) {
// Give user a chance to cancel
let confirmMessage = confirm("This will copy content into your course. Click OK to proceed.");
if (confirmMessage == true) {
// Add a div for feedback
$('#modules_homepage_user_create').prepend('<div id="kl-import-progress" class="alert alert-info"></div>');
// Send request to Canvas
$.post('/api/v1/courses/' + + '/content_migrations', {'migration_type': 'course_copy_importer', 'settings[source_course_id]': template_course_id}, function(data, textStatus, xhr) {
// Write a response with a link to the course migration page
$('#kl-import-progress').html('Request submitted. Course copy will take a few minutes. Reload this page periodically or <a href="''/content_migrations">view import progress in Canvas</a>');
} else {
// Will only work on the course front page
alert('Run this from the course home page of an empty course');

In order for this to work as a bookmark, we have to convert it. I like to use MrColes Bookmarklet Creator to convert the code above into what we will add to a bookmark. After converting the code, it will look more like this:


 It is a lot harder to read but it will do the job. To make things easier for you to update, find the ##### string and replace it with your template course id. Once you update the course id, this is the code that you will paste in as the URL in your bookmark.

Prompt for Course ID

If you want a little more flexibility for what course you want to copy, this option will ask the user to provide a course ID.

Course ID Prompt

Here is the JavaScript we will use for this one (you don't have to update this one):

// Only run on the home page of courses without any other content in place (shows import option)
if (ENV.COURSE !== undefined && !== null && window.location.pathname === '/courses/' + && $('.ic-EmptyStateList:visible').length > 0) {
// Prompt user for Canvas course ID
let course = prompt("Please enter the Canvas course ID from which to pull content");
// If they give a response, use it
if (course != null) {
// Placeholder for feedback
$('#modules_homepage_user_create').prepend('<div id="kl-import-progress" class="alert alert-info"></div>');
// Remove any spaces
course = course.trim();
// Send request to Canvas
$.post('/api/v1/courses/' + + '/content_migrations', {'migration_type': 'course_copy_importer', 'settings[source_course_id]': course}, function(data, textStatus, xhr) {
// Write a response with a link to the course migration page
$('#kl-import-progress').html('Request submitted. Course copy will take a few minutes. Reload this page periodically or <a href="''/content_migrations">view import progress in Canvas</a>');
} else {
// Will only work on the course front page
alert('Run this from the course home page of an empty course');

And here is that code converted to use for a bookmarklet:


Wrap Up

Anyway, I hope that this will be useful to some of you during the present chaos and any future chaos. Feel free to modify, adapt, change, or share that code.

6 0 2,132
Community Champion

In one of the courses that I have been responsible for there is a final oral presentation with formal opponents who have previously written an opposition report. During the final oral presentation, I have found it very useful to have a spreadsheet with the presentations sorted into time order - and with columns ready to take notes on the presentation, on the oral oppositions, etc. Now that another teacher is going to take over the responsibility for this course I have been showing other teachers how to organize things to make it easier for them in this course. I realized when showing my spreadsheet to the new person that will take over the course that I have been stupid and should have made more of this directly in the gradebook. The result is that I made a program to add the desired custom columns to the gradebook for the teacher to write their notes during the presentation and to populate the columns with the material that is known (in this case the scheduled date and time for the presentation and the names of the opponent(s) for each student (these were peer reviewers of a draft of the report). I have separately added a column that contains the name of the project group that each student is in. The resulting program can be found at:

Still to be done is to extract the title of the project from the draft report that was submitted by the project group and possibly a compressed version of the grading and feedback on the written opposition.

In any case, the program shows what can be done with the API to get the users in a course, the groups in a course, the members of each group, calendar events for the course, etc.

1 0 872
Community Contributor

I love it when asking a question prompts me to think about and reasearch the actual answer; since I had already typed most of this posting when the solution came to me, I decided to complete it anyway. The topic above is answered herewithin!

When managing user enrollment using the API, I have scripts set up to deal with student "adds" and "deletes/withdrawls" by our SIS. These work well until you bring crosslisted sections into the picture.

Active Enrollment Endpoint: POST /api/v1/sections/:section_id/enrollments
This endpoint works for crosslisted sections because it is based on the Section_ID which is associated with another Course_ID in the crosslist process.

Ended Enrollment Endpoint: DELETE /api/v1/courses/:course_id/enrollments/:id

For crosslisted sections, this endpoint fails because the section in which the student is enrolled is no longer tied to its original Course_ID. Indeed, the course still exists, but the student isn't currently enrolled in that course, thereby failing the API call because "The specified resource does not exist."

There is an API endpoint where I can use a Section_ID to determine its associated Course_ID. For all enrollment deletes, use this endpoint, first, to determine the correct course from which to actually delete the enrollment, then use the endpoint above to delete the enrollment.

Section Information Endpoint: GET /api/v1/sections/:id
The base response from this endpoint provides the course_id and sis_course_id of the course with which it is associated. Use either of these in the DELETE endpoint above Smiley Happy

2 1 985
Community Champion

A long, long time ago, in an office somewhere in my building (I'm guessing), a conversation probably nothing like this took place:

vintage secretary

"Okay, it's time to implement the new LMS, Canvas."

"Hooray! Anything special we need to do?"

"Well, we should probably decide if our Canvas instance should be one big bucket of stuff, or if we need sub-accounts for our 5 separately accredited colleges."

"I'd think sub-accounts by college would be smart!"

"Definitely. Oh... wait... We're integrating Banner with Canvas, right?"

"Of course!"

"Well, we have Banner set up to essentially treat our district as a single entity."


"Well, there's no easy way to tell Canvas which sub-account the course should be in."

"Oh. One giant bucket, it is!"

Fast forward a year or two. The transition from Blackboard to Canvas is complete. Faculty adoption is growing rapidly. Online course offerings are expanding. Requests for adding account-level LTIs keep coming. Data and outcomes and analytics are hot topics. People begin to question with more regularity why we don't have sub-accounts to keep these things useful, streamlined, and logical. The answer continues to be "because we don't have Banner set up in a way that makes sub-accounts possible". Fast forward a few more years. People are asking at least weekly why we can't do a thing that can't be done because we don't have college sub-accounts. The requests finally prompt action. The action hits numerous brick walls. There is a script written that seems to solve the problem by moving the courses from the root account to the correct sub-account after the fact. Testing is going well. There is dancing and singing. Then someone performs a section sync in Test Banner, and the course in Test Canvas gets thrown out of its sub-account and back to the root account. Canvas insists that "account" should be sticky. Testing continues to prove that Banner is a jerk and refuses to listen to what Canvas says. Other avenues are considered and defeated. Tears are shed. And here we are, seven long years into our Canvas adventure, without any hierarchy whatsoever. And we've exhausted every reasonable idea, short of demolishing the entire Banner > Middleware > Canvas structure and starting from the ground up.

cartoon guy at computer

So what's the point of my blog post / stream of consciousness rant / discussion prompt / cautionary tale / plea for help? Well, first, my words of advice:

  1. If you are new to Canvas, consider your account/sub-account structure, very very very carefully.
  2. Canvas is a wonderful company full of wonderful people, but they do not have expertise in your SIS of choice, so make sure your SIS of choice is ready and willing to work with you on establishing (and maintaining, and adjusting as needed) a good integration with Canvas.

And my points of discussion:

  1. I know people have created a sub-account structure "after the fact". What makes our situation challenging is the way we set up Banner. We are unable to change how Banner is configured.
  2. We have had various vendors swear they can help us using APIs. This may be true, but they neither understand our Banner setup nor possess actual, practical, deep Canvas knowledge.
  3. We are still using Luminis Message Broker as our middleware. My understanding is that LMB is an outdated tool. But the alternatives (Ethos, ILP) are described to me as not mature enough, not robust enough, and/or possibly not compatible with our current portal (or version of).
  4. We haven't completely ruled out turning off real-time events and doing automated batches. BUT we have a bazillion (actual number) Banner users across our 5 colleges who would need to be re-educated on new processes and that is... daunting.

The plea:

  1. Is there anyone out there that knows Banner really well AND knows Canvas really well AND is not already working full time for a Banner/Canvas institution? We have tried the "hire a consultant" route, and a creature with such expertise (in the land of hire-able consultants) appears to be rarer than a 3-legged unicorn.
  2. Has anyone had a similar situation and come up with a miracle workaround that they'd be willing to share? I will pay you in cookies and a coffee mug that says "You're Awesome!" [side note: don't search Amazon for "You're awesome" coffee mugs. Someone apparently decided not to stop there and made all the mugs NSFW]
  3. Please consider this an open discussion, and throw any ideas into the comments, no matter how wild and insane they might sound. Tag any groups or spaces I missed, or any smart folks who might know something, anything. It's possible there will be points for creativity, but it depends on whether I'm continuing to lose ground in the Community. I once made it to 17th. Those were the days.

Thank you in advance, Canvas friends!


Also tagging Canvas DevelopersInstructional DesignersHigher EducationHigher Education SIS

7 15 3,216

There have been some important changes to developer keys (see the 14 Jul 2018 and 10 Oct. 2017 release notes) since my last blog post about what administrators should consider when issuing developer keys. Through feedback from the community and from our clients, I’ve noticed there is a lot of confusion surrounding developer keys in general, and specifically surrounding the Instructure-issued developer keys that appear in the “Inherited” tab of the Developer Key Management UI. This blog post should hopefully answer many of the frequently asked questions surrounding developer keys and give Canvas Account Administrators a deeper understanding of how to best manage developer keys.

Dev Keys overview

A developer key allows an application to request access to a users API information. In all cases, the user must authorize the application to act on his/her behalf. If the user authorizes an app, Canvas generates an API token and securely responds to a request from the external application with the generated token. The app can then store that token and use it to perform the API requests that it has access to. You’ve probably seen this in the UI (see image below), but may not realize that a developer key is involved:

305237_pastedImage_1.png   An example of the UI shown when a developer key to requests access to a user's Canvas API


Types of Developer Keys

There are two types of API developer keys: account-bound or inherited (this may also be referred to as a global developer key).

Account-bound developer keys are issued by the Account Admin and will only be able to request API access from users that belong to that account.

Inherited developer keys are issued by Instructure based on a basic set of criteria (see our API docs under the “What management features are available?” section) and can access any account where they have been enabled.


Understanding the history of developer keys in Canvas will help you understand your current list of inherited developer keys.

Before October 2015

Account-bound Developer Keys did not exist, so if a vendor wanted API access, we had to work with them to issue a global/inherited developer key. As you can imagine, this raised concerns, so a series of releases (19 Sept 2015, 10 Oct 2015, and 31 Oct 2015) allowed developer keys to be account-bound and managed by the Canvas Account Administrator for each institution.

October 2015 to July 2018
Vendors had to work with each institution they integrated with to obtain a developer key. They also had to manage multiple developer keys inside of their application. Instructure severely limited its issuance of developer keys and pressured vendors to work with individual institutions to obtain an account-bound developer key. Inherited developer keys were only issued In rare cases where they seemed like only reasonable option and where Instructure had a very strong partnership in place.

Canvas Account Administrators raised legitimate concerns over the level of access that developer keys had (the API token issued had access to every API endpoint the user had permission to access). Furthermore, they had no way to know which inherited developer keys had access to their accounts, let alone the approval process by which Canvas issued the developer keys, without reaching out to their CSM.

July 2018-present
In response to the concerns raised by our customers, we released a set of features (14 July 2018 and 10 Oct. 2017 releases) that allow for admins to manage which inherited keys have access to their accounts. Furthermore, developer keys can now have their access restricted through the use of developer key scoping.

Instructure began issuing inherited developer keys, simplifying both the management of developer keys for application owners and reducing the steps required to enabling developer key access for Canvas Account Administrators.

Default settings for inherited keys

Simply put, developer keys issued prior to July 2018 were defaulted to ON, since we wanted to avoid breaking any existing integrations. Developer keys issued after that default of OFF so that a Canvas Account Admin must approve and enable the developer key to have access to their account.

Instructure’s approval process/vetting for Inherited Developer Keys
Many admins have been surprised at the number of developer keys that they find in their “Inherited” tab, spurring (rightly) many questions related to how they got there and what processes are in place for approval. The previous section should help understand the how they got there aspect for anything issued before July 2018, but may want to know more about the current policy that Instructure takes when issuing a new developer key.

As mentioned previously, we (the Partnerships/Platform team) issue developer keys based on a basic set of criteria (see our API docs under the “What management features are available?” section).  I’ll flesh this out in a bit more detail here:

What Instructure does:

  • Requires scope enforcement for newly issued inherited developer keys.
  • Requires justifications for why those scopes are needed.
  • Requires explanations of how developer keys and tokens are securely stored.

  • Expects Canvas Account Admins to determine their own vetting policies and apply them when enabling an inherited developer key.

What Instructure likes to have:

  • A basic product demo showing the integration. This is pretty much required for anyone who doesn’t have a partnership with us.
  • Links to relevant documentation about the integration.


What Instructure does not do:

  • Thoroughly examine the application’s code base
  • Audit security. privacy, or storage policies for the application. As noted above, Instructure expects Canvas Account Admins to determine their own vetting policies.
  • Ensure that the application will comply with all of our customers various integration policies.

Best practices for managing inherited developer keys as a Canvas Account Administrator

As a Canvas Account Administrator, you are the gatekeeper for which applications have the ability to request API access from users in your account. That’s a lot of responsibility, but fear not, I can help.

The first thing your should do is audit which developer keys have already been used to issue tokens in your account. The best way I have found to do this is by running the User Access Tokens report. You can then look at the “dev key id” column and make sure those are all turned on.


Next, Once you have the report, use whatever internal policies or processes you may have to determine if each of the developer keys is appropriate for your organization. You might consider doing the following:

  • Determine who (at your organization) requires these developer keys / access tokens
  • Check if these are still actively being used, and why
  • Determine why these are being used (and document these reasons)
  • Approve the continual use of these


Next, You can turn off any of the inherited keys that don’t appear in the report, but you may want to check with any of your internal teams to verify. There may even be someone who already knows which integrations are currently being used. Just know that if you turn off a developer key for an integration that depends on it, you will be limiting it or completely breaking its functionality. Note that a key can be turned on again in case of error.


Last (optional) you may want to reach out to others (admins, teachers, students etc) to keep an eye out for any errors related to integrations that throw authorization errors just in case you made a mistake.


That’s it! You’ve now limited access to only those keys that your account is dependant on. When you work with a new vendor that has an inherited key, you can now push them through whatever vetting processes your institution may have in place and then enable their inherited dev key.


A note about scope enforcement

Using scoped developer keys requires some work by the vendor that uses the developer key. They must determine the scopes they need access to and modify their OAuth2 request to include the scopes they want access to. Furthermore, some applications use “include” query parameters in their API requests. Canvas scoping ignores these parameters, so they must come up with alternatives API endpoints to more explicitly obtaining the same information. Our Partner Support team is happy to help our partners create a roadmap toward this end, but it’s up to our clients to put the heat on while understanding the technical challenges that they face.

Roadmap: Developer Key Experience Improvements

The Canvas API integration experience can be better. I’m currently scoping out requirements to enhance the developer key experience. I have some ideas in mind (ex: exposing the scopes for Inherited developer keys to Canvas Account Admins), but I’d rather hear your ideas to make sure mine are as good as I think they are! If you have an idea for an experience enhancement or any other ideas related to developer keys, please follow the Feature Idea creation process and throw a link to the feature idea in the comments! Make sure to review the comments and open feature ideas to make sure there isn’t one already open that addresses your idea.


I hope that this blog post has helped our Canvas Account Administrators understand more about all the recent changes to dev keys and how best to user their new shiny developer key management UI. Happy vetting!



Q: "If we disable these inherited keys, do we in effect become a gatekeeper to individual instructors being able to install an app for their course?”
A: Yes. Instructors must work with their Canvas Account Administrator to turn on any inherited developer keys before their course-level integration that depends on it will work.


Q: "Where do the Inherited Developer Keys originate?"

A: Instructure issues them. See the “History” section above for more info.

Q: "Are there any best practices recommended for ensuring a higher degree of security?
A: The same recommendations that are outlined in my previous blog post should help here, but there are no specific Instructure defined “best practices” that will work for everyone. Talk to your technical folks at your Institution, or your network of professionals to see what they think is best on top of what I’m presenting here.


Q: “Does allowing the inherited keys to remain enabled cause any unintended data to be sent to these vendors?
A: Only if the vendors requests access to data and the end-user authorizes access. Most vendors have an LTI integration that is required before the app will even ask for authorization, so if it’s not installed, chances are you can safely turn the integration off. That said, there are API based integrations that don’t require an LTI component.


Q: “How do I know which developer keys are being used in my account?”

A: Run the User Access Tokens report and examine the “dev key id” column. If a dev key has been used to issue an API token for your account, it should appear here.

17 14 7,525

The Story

A common frustration of teachers using Canvas at an institution is when their course navigation gets messed up by an admin. A teacher may have been teaching their courses for a couple of weeks and then all of a sudden, their institution adds a new LTI that has a course navigation launch and it messes up their course navigation in all their courses. Then their students start asking about the new LTI and the teacher has to tell them all they aren't using it and hide it in their course settings


However, the story is worse for those instructors who don't know to customize their course navigation. Before you know it, they have a course navigation list that is so long that students have to scroll to see everything on it (see image to right). Core Canvas tools will hide themselves if they aren't being used (light grey items), but not with LTI's. So now those teachers have their students asking about each new tool and wondering why their teachers aren't using it...

Taking Control

Part 1 - Installing New LTIs

As the Canvas admin, you have the ability to decide, when an LTI is installed, whether you want it to be visible or not. Most LTI vendors want to encourage your teachers to use the tool and they enable it by default. A simple edit can help you avoid the above hassle for your teachers, while allowing you to roll the tool out to the teachers who need it.

When installing an LTI tool, the 3rd party provider will generally provide you a URL to configure the tool. If installing the tool from the apps tab in Canvas, the URL's for the installs can be found at the tool's detail page on These URLs should end in .xml which is the format needed to configure an LTI tool. Navigating to these URL's in your browser should take you to a page that looks like this:

<blti:title>Course Wanda Fish</blti:title>
<blti:description>This tool adds a course navigation link to a page on a fish called "Wanda"</blti:description>
<blti:extensions platform="">
  <lticm:property name="tool_id">course_navigation</lticm:property>
  <lticm:property name="privacy_level">anonymous</lticm:property>
  <lticm:options name="course_navigation">
    <lticm:property name="text">Course Wanda Fish</lticm:property>

The trick is to find the <lticm:options name="course_navigation"> part of the xml file so we can customize it a little bit. We just need to add one more line after this and then we can install the LTI by pasting in XML. So here are the instructions:

  1. Copy and paste the XML into a text editor (Notepad for Windows or textEdit for Mac)
  2. Find the <lticm:options name="course_navigation"part of the code
  3. Copy and paste the following code immediately below the course navigation line
    • <lticm:property name="default">disabled</lticm:property>
  4. Follow these instructions to install the LTI via XML
  5. Enjoy the new LTI being hidden by default

Part 2 - Editing Existing LTIs

If you already have LTIs that are visible by default that you'd like to change, you can do that by using the Canvas API. There is a lot to working with the API that I will not go into on this post, but if you have a Mac you already have "cURL" installed by default so you can copy and paste the following code to update your LTI tools. If you have a PC, you can try and follow the instructions here to install cURL on Windows.

You will need to:

  1. Copy and paste the following code snippet to a text editor
    • NOTE: If the tool has other custom navigation parameters they need to be added to avoid breaking some functionality
      • View the existing parameters by viewing the response from step 4
    • curl -X PUT 'https://<canvas>/api/v1/accounts/<account_id>/external_tools/<external_tool_id>' \
             -H "Authorization: Bearer <token>" \
             -F 'course_navigation[default]=disabled'\

             -F 'course_navigation[*CUSTOM PARAMETERS*]=VALUE"

  2. Put your Institution's Canvas URL where <canvas> placeholder is ( or vanity URL)
  3. Put the Account/Sub-Account ID where the <account_id> placeholder is
    • The can be found by navigating to the Account/Sub-account in Canvas and looking at the url. It should be https://<canvas>/accounts/123 with number 123 being the account_id
  4. External tool ID can be found by updating the <school> and <account_id> fields in the url below and then navigating to it in your browser
    • https://<school>/api/v1/accounts/<account_id>/external_tools?per_page=100

    • You may want to download this Chrome Plugin to make the text more organized (similar plugins exist for other browsers)
    • Find the LTI tool you are looking for, the LTI should be a number. eg:


  1. Update the <external_tool_id> placeholder with the LTI id number
  2. Generate an API Token
  3. Replace the <token> placeholder with your new API Token
  4. Copy and paste the edited code snippet into Terminal (on mac) or Command Prompt (on Windows after you install cURL)
  5. Hit Enter and watch the LTI be hidden by default in all courses. 

*Note, your teachers may have to re-enable the LTI tool in their courses to have access. So you may want to send out communication about this change before you do it.

39 31 11.8K

24 Jan 2019 Update: This guide is still relevant, but you should also learn more about some new features that have been released since this was posted. Thanks for all the comments and feedback that helped me understand where the knowledge gaps and confusion are surrounding our new developer key features!

When working with an integration provider (i.e. vendor, internal development, etc.), they will often ask for a developer key. Many integrations require Canvas API access to function correctly, and developer keys are currently the most secure way to allow integrations to access the API. This documentation covers some of my recommendations for how to decide whether issuing a developer key to a vendor is secure and safe. There is no general “best practice” for addressing this challenge since every institution has it’s own sets of requirements and procedures for security and privacy.

Developer keys can be issued by a Canvas Account Admin in order to allow an integration to request access on behalf of the user to the API. The user is prompted to approve access to the application and upon approving an API token is granted to the integration.


     An example of the UI shown when a developer key to requests access to a user's Canvas API

After authorization, Canvas generates an access code that is later exchanged for an API token by the external application. The integration can then make API requests as if they were that user. In other words, they are able to make API requests to all of the same endpoints that the authorizing user has permission to.

Considering API tokens inherit the permissions of the granting user therefore granting that same level of access to the vendor, you might consider doing the following before issuing a developer key:

  • Ensure you have a formal written contract with the vendor and that your relationship with the vendor is in good standing.
  • Ask the vendor to explicitly document all of the API endpoints their integration will be using (this should be a list of all the data that is being read and written) and, optionally, why.
  • Reserve the right, in writing, to take any actions your team deems appropriate should the vendor make API calls outside of the scope that they defined without notifying your team.
    • NOTE: Developer keys can be disabled via the Admin UI which will invalidate all issued API tokens to the integration.
  • Ask the vendor to clearly state how it complies with FERPA and any other laws that apply to your students (e.g. COPPA, accessibility laws etc...).
  • Ensure you understand the vendor’s security policy surrounding how they store API tokens and developer keys (ex. they should never be exposing the developer key or API tokens in any kind of UI, including error reports; only their core engineering team should have access).
  • If available, ensure that your institution's security team has reviewed the application’s security.

Again, whether you follow some, all, or even more of these guidelines is up to your team to decide. Hopefully this discussion can help you make the best choice for your institution.

Discuss: What are some of the policies that your institution has taken to determine when to issue developer keys?

22 33 18.1K

🛑 Please Read the Following before using this guide

This document is for a legacy implementation of Google Analytics on a deprecated version of Google Analytics no longer offered by Google. Do not attempt to use it. Instructure no longer uses Google Analytics in Canvas, and therefore updated documentation on using Google Analytics 4 (or similar) will not be provided by Instructure.  

An alternative option for Canvas usage data supported by Instructure is our Impact by Instructure product. Please reach out to your Instructure CSM or Sales representative if you'd like to explore this option.

If you still want to use Google Analytics, you should look into some of the below community resources if you are configuring Google Analytics for the first time. 


Using Google Analytics means that your institution is subject to Google's terms of service. Please verify with your institution's legal team before installing Google Analytics in your Canvas instance to ensure that you are in compliance with relevant laws and regulations.

New Custom Dimensions & Updated Script - May 31, 2019

A core code change to Canvas on Aug 28, 2019 may have broken your implementation of Google Analytics. Please install the updated code to restore functionality.


The steps below will help you get set up to use Google Analytics for Canvas. If you'd like to know more about why you might want to do this, please watch this Canvas Live session.


Create a Google Analytics Account

  1. Sign into Google
  2. Navigate to  and click Sign Up
  3. Fill out the Form as desired, using your Canvas url in the website url field (eg: (or vanity url if you have one)
  4. Click "Get Tracking ID" button
  5. Accept Terms of Service
  6. Copy down the "Tracking ID" (eg: UA-12345678-1)


Set Up Tracking By User-ID New

Allows for more accurate session unification based on the user's Canvas ID, granting better user overtime metrics

  1. Navigate to the Admin Portal (If you just created your account you are already there)

  2. Click on "Tracking Info" to expand the menu item

  3. Click on "User-ID" option

  4. Read the agreements and follow the prompts


Add Custom Dimensions to Google Analytics Updated

This will let you pass custom variables from Canvas to Google Analytics such as User ID's, User Names, Course ID's and more.

  1. Navigate to the Admin Portal (If you just created your account you are already there)
  2. Click on "Custom Definitions" menu item
  3. Click on "Custom Dimensions menu item
  4. Click "+ NEW CUSTOM DIMENSION" button
  5. Add the following dimensions in order (If your index number is incorrect shared dashboards may not work)314355_pastedImage_7.png


Add Custom Javascript to Your Institution's Theme Editor

** Custom Javascript has been known to break things in Canvas. As of writing this post, May 31, 2019, it is working, but be aware that could change in the future.

  1. Download the attached Javascript File (If you cannot download for browser security reasons, you may copy and paste the code into a .js file from github. Minified Script. Non-minified Script.)
  2. Edit the File in a text editor
    • You'll need to customize line 3 (if using the minified version), or line 168 (non-minimized) with your tracking id number. Updated
    • //Working as of Aug 28, 2019
      function removeStorage(e){try{localStorage.removeItem(e),localStorage.removeItem(e+"_expiresIn")}catch(t){return console.log("removeStorage: Error removing key ["+e+"] from localStorage: "+JSON.stringify(t)),!1}return!0}function getStorage(e){var,o=localStorage.getItem(e+"_expiresIn");if(null==o&&(o=0),o<t)return removeStorage(e),null;try{return localStorage.getItem(e)}catch(t){return console.log("getStorage: Error reading key ["+e+"] from localStorage: "+JSON.stringify(t)),null}}function setStorage(e,t,o){o=null==o?86400:Math.abs(o);var*o;try{localStorage.setItem(e,t),localStorage.setItem(e+"_expiresIn",s)}catch(t){return console.log("setStorage: Error setting key ["+e+"] in localStorage: "+JSON.stringify(t)),!1}return!0}async function coursesRequest(e){let t=await fetch("/api/v1/users/self/courses?per_page=100"),o=await t.text();o=o.substr(9),o=JSON.parse(o);var s=JSON.stringify(o);return setStorage("ga_enrollments",s,null),parseCourses(e,s)}function parseCourses(e,t){if(null!=t){let s=JSON.parse(t);for(var o=0;o<s.length;o++)if(s[o].id==e)return s[o]}return null}function gaCourseDimensions(e){custom_ga("set","dimension4",,custom_ga("set","dimension5",,custom_ga("set","dimension6",e.account_id),custom_ga("set","dimension7",e.enrollment_term_id),custom_ga("set","dimension8",e.enrollments[0].type),custom_ga("send","pageview")}function googleAnalyticsCode(e){var t,o,s,n;if(custom_ga("create",e,"auto"),t=ENV.current_user_id,o=ENV.current_user_roles,custom_ga("set","userId",t),custom_ga("set","dimension1",t),custom_ga("set","dimension3",o),n=window.location.pathname.match(/\/courses\/(\d+)/)){n=n[1],s=0;try{let e=getStorage("ga_enrollments");if(null!=e){var r=parseCourses(n,e);null===r?coursesRequest(n).then(e=>{null===e?(custom_ga("set","dimension4",n),custom_ga("send","pageview")):gaCourseDimensions(e)}):gaCourseDimensions(r)}else coursesRequest(n).then(e=>{null===e?(custom_ga("set","dimension4",n),custom_ga("send","pageview")):gaCourseDimensions(e)})}catch(e){if((s+=1)>5)return custom_ga("set","dimension4",n),void custom_ga("send","pageview")}}else custom_ga("send","pageview")}!function(e,t,o,s,n,r,a){e.GoogleAnalyticsObject=n,e[n]=e[n]||function(){(e[n].q=e[n].q||[]).push(arguments)},e[n].l=1*new Date,r=t.createElement(o),a=t.getElementsByTagName(o)[0],r.async=1,r.src="",a.parentNode.insertBefore(r,a)}(window,document,"script",0,"custom_ga");
      googleAnalyticsCode("UA-12345678-1"); // customize google analytics tracking number here‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
  3. Save the javascript file after making changes
  4. Upload the javascript file into your institution's Theme Editor
  5. Navigate back to Google Analytics and view real-time data to see if it is tracking real-time users on your account. Know that you may have to wait for 15 minutes or so to be able to see custom dimension data.


Build Dashboards & Reports in Google Analytics

  1. You may wish to get started by downloading the following dashboards:
  2. Build your own and once you get it the way you want, share the template URL in the comments on this document!


Embedded Custom Javascript File

There were issues with people copying the embedded Javascript. Please view the scripts on Github.


Changes Made (May 31, 2019) New

The updated script has undergone significant change. Below are some highlights of what the updated script does. The updated script is mostly backwards compatible with the previous version.


General Changes

  • Updated to remove any jQuery dependencies. Modern javascript is being used (may not work on Internet explorer). This means that API calls are now asynchronous and non-blocking. Improves performance.
  • Fixed a bug that meant some pageviews were never sent.
  • Added a timeout if API requests fail to avoid server spamming
  • Added support for Google Analytics User-ID tracking
  • Altered API Endpoint & Data Caching
    • Instead of pulling the course information (/api/v1/course/:course_id), the script pulls a user's courses ("/api/v1/users/self/courses?per_page=100") which includes an enrollment object and caches the returned value into local storage. The associated enrollment object allows us to set the "Canvas Course Role" dimension. Caching the data should also result in significantly better performance due to reduced API calls. If a course does not appear in the user's course list just the course_id and user dimensions will be sent (should only be for Admins and public courses).  Also it will only pull the first 100 enrollments for a user (let me know if you need support for pagination with the revised endpoint).

Dimension Changes

  • Canvas User RoleUpdated
    • Instead of filtering for a best match of a user's roles and only returning 1 value this is now a comma separated array of all of a user's roles. When filtering on this value, look use something like "contains" & "student" to get any users with a role of student. The data is still pulled from environmental variables. You should be able to build better Audience Segments from this data. (ie: Students should only have user, student, and maybe ta roles).
  • Canvas Term ID New
    • This can be a valuable filtering tool as much of Canvas built in reports filter on terms
  • Canvas Course Role New
    • By altering the way the script queries the course data, we are able to now capture the exact role the user has in a course. If a user has multiple user roles in a course, the value assigned to the pageview will always be the first role in the array. This should be a very useful dimension for building reports and analyzing course usage. If you think capturing all user roles will be helpful, let me know in the comments.

27 119 51K