themidiman
Community Champion

Google App Scripts - URLFetchApp API Endpoint Requests other than GET method

Jump to solution

I've given myself a project to build some API automation tools around data that is contained in Google Sheets.

I see only a smattering of similar projects on the Developers Group forum that seem to imply success without re-educating the group what their entire code is that solved their issue. Here's one that implies success but seems to be missing the entire code: https://community.canvaslms.com/t5/Canvas-Developers-Group/POST-request-with-JSON-data-from-Google-A... 

@bbennett2 would you be able to share what you did that worked?

I can use URLFetchApp all day to get data out of Canvas using GET, but when it comes to putting it back or manipulating it using POST, PUT, or DELETE, I'm running into roadblocks.

I've been trying to automate the creating of sandbox type courses for faculty, and want to use these two endpoints:

https://canvas.instructure.com/doc/api/courses.html#method.courses.create

and

https://canvas.instructure.com/doc/api/enrollments.html#method.enrollments_api.create 

Both use the POST method, and I'm starting with just the Enrollments endpoint for starters. Here's my code:

 

 

function enrollUser(user_id,course_id) {
  var requestURL = "<CANVAS_URL>/api/v1/courses/" + course_id + "/enrollments"
  var data = {
     'user_id': user_id
  };
  var options = {
    'muteHttpExceptions': true,
    'method': 'post',
    'Content-Type': 'application/json',
    'headers': {
      "Authorization" :"Bearer "+token,
      
      },
    
    'payload': JSON.stringify(data)
  };
  var result = UrlFetchApp.fetch(requestURL, options);
  Logger.log(result);
}

 

 

This results in 

 

{"message":"No parameters given"}

 

So my confusion is when comparing to the Live API where you just submit the required information, is all of that supposed to go in the data payload, does it go on the URL as part of the query string? 

If I can get a concrete example of what constitutes a successful POST, PUT, or DELETE request in Google Apps Scripts, I can build up more of my library of tools to use Google Workspace/Docs to automate certain aspects of my role at my institution.

Thanks in advance.

Labels (4)
0 Kudos
1 Solution

@JamesSekcienski ,

Thank you 

This is the missing piece of the puzzle. The earlier forum post was misleading me all this time. The assertion was that Google's App Script Documentation for URLFetch used 'contentType' instead of 'Content-Type' as the header name, but Canvas API didn't like it that way. It appears that isn't the case. Here's my final code that actually works now (hooray!)

 

 

function enrollUser(user_id,course_id) {
  var requestURL = "<CANVAS_DOMAIN_URL>/api/v1/courses/" + course_id + "/enrollments"
  var data = {
     'enrollment': {
        'user_id': user_id,
        'type': 'TeacherEnrollment'
    }
  };
  var options = {
    'muteHttpExceptions': true,
    'method': 'post',
    'contentType': 'application/json',
    'headers': {
      "Authorization" :"Bearer "+token
      
      },
    'payload': JSON.stringify(data)
  };
  var result = UrlFetchApp.fetch(requestURL, options);
  Logger.log(result);
}

 

 

View solution in original post

17 Replies
melodyc_lam
Community Participant

Hey! Glad that someone is working with Google App Script!

So unfortunately for the Enrollments endpoint, you actually need to write the parameters in the URL as query parameters instead of sending a payload. The message actually hints at this solution but I totally understand getting confused by it.

Your URL will look something like this:

"<CANVAS_URL>/api/v1/courses/" + course_id + "/enrollments?enrollments[user_id]=" + user_id + "&enrollments[type]=StudentEnrollment"

And you'll not have a "payload" key in the JSON object for options.

I tried this in my beta environment and it worked, returning the Enrollment object.

I believe (and correct me if I'm wrong) the cURL command examples can give you a hint as to where your parameters go. If you see "-F" that means that it's probably going to go in the url, and if you see "-d" then that is going to go into the payload.

Good luck!

Thanks @melodyc_lam 

I've updated my code to look like so:

 

 

function enrollUserByURL(user_id, course_id) {
  var requestURL = "<CANVAS_URL>" + course_id + "/enrollments?enrollments[user_id]=" + user_id + "&enrollments[type]=TeacherEnrollment";

  var options = {
    'muteHttpExceptions': true,
    'method': 'post',
    'headers': {
      "Authorization" :"Bearer "+token
      
    }
  }

  var result = UrlFetchApp.fetch(requestURL, options);
  Logger.log(result);
}

 

 

You'll notice I'm attempting to set up teachers in sandbox courses so the enrollment type will always be TeacherEnrollment in my use case. 

I'm still getting:

 

{"message":"No parameters given"}

 

I do like that cURL approach to deciphering how to pass parameters you've pointed out though. After a bit of my own research the -F flag (or --form) is to emulate a form submission which implies POST method, but HTML forms can also use the GET method so this is a bit ambiguous. Do you happen have a working example of a Google Apps Script that executes a successful POST request to any endpoint?

0 Kudos

I think you're missing the "Content-Type" parameter, I used 'multipart/form-data'.

Code is below. Hope this helps!

function enrollUser(user_id,course_id) {
  var requestURL = <canvas URL> + course_id + "/enrollments?enrollment[user_id]="+user_id+"&enrollment[type]=StudentEnrollment"

  var options = {
    'muteHttpExceptions': true,
    'method': 'POST',
    'Content-Type': 'multipart/form-data',
    'headers': {
      "Authorization" :"Bearer "+ token,
      
      },
    
  };
  var result = UrlFetchApp.fetch(requestURL, options);
  Logger.log(result);
}
0 Kudos

@melodyc_lam ,

That makes sense, but so far no success...I'm still getting the response of "No Parameters Given"

Here's my new updated function:

function enrollUserByURL(user_id, course_id) {
  var requestURL = "<CANVAS_URL>/" + course_id + "/enrollments?enrollments[user_id]=" + user_id + "&enrollments[type]=TeacherEnrollment";

  var options = {
    'muteHttpExceptions': true,
    'method': 'post',
    'Content-Type': 'multipart/form-data',
    'headers': {
      "Authorization" :"Bearer "+token
      
    }
  }
  var result = UrlFetchApp.fetch(requestURL, options);
  Logger.log(result);
}

 

0 Kudos
Are you adding "/api/v1/" to your Canvas URL for your institution?
So it should look like:
school.instructure.com/api/v1/courses/ followed by the rest of the endpoint.
0 Kudos

@melodyc_lam ,

Yes...that was a typo in my example post. Here's my actual code:

 

function enrollUserByURL(user_id, course_id) {
  var requestURL = "<CANVAS_URL>/api/v1/courses/" + course_id + "/enrollments?enrollments[user_id]=" + user_id + "&enrollments[type]=TeacherEnrollment";

  var options = {
    'muteHttpExceptions': true,
    'method': 'post',
    'Content-Type': 'multipart/form-data',
    'headers': {
      "Authorization" :"Bearer "+token
      
    }
  }
  var result = UrlFetchApp.fetch(requestURL, options);
  Logger.log(result);
}

 

This code still yields the "No parameters given" message in the result log. 

0 Kudos

Hm. Are you running a test function to test the function with known good values on your beta instance?

So something like this:

function test_enroll() {
  enrollUserByURL(<known good user id>, <known good test course id>)
}

Because I copied and pasted your code, replaced the URL and token with my values, and ran it and got the same result as my function. So I'm not exactly sure what is different between my environment and your environment.

0 Kudos

@melodyc_lam 

I really appreciate your help, but @JamesSekcienski helped me narrow it down to a malformed header for content type. It appears that POST requests actually should accept a JSON payload. The challenge going forward is figuring out the challenge of translating more API endpoints that aren't GET requests. I'm going to start building up a library of these and perhaps come up with a shareable library or API wrapper of sorts similar to the one that UCF put together for Python.

I'm not sure at the moment if Google App Scripts can import external libraries of additional JS code, but if this is possible, I will definitely write a blog post about it for sharing with the Canvas Developer Community in the future.

JamesSekcienski
Community Contributor

Since there are two required parameters for this request, have you tried the following for the data?

var data = {
    'enrollment': {
        'user_id': user_id,
        'type': 'StudentEnrollment'
    }
};

If you are going to enroll users as different types, you may want to add a parameter to your function for the enrollment type. 

0 Kudos

Thanks, @JamesSekcienski 

I've tried this change to the payload, but I'm still getting 

 

{"message":"No parameters given"}

 

Following my own advice of posting entire code for the benefit of the community here's my updated function code:

function enrollUser(user_id,course_id) {
  var requestURL = "<CANVAS_URL>/api/v1/courses/" + course_id + "/enrollments"
  var data = {
     'enrollment': {
        'user_id': user_id,
        'type': 'TeacherEnrollment'
    }
  };
  var options = {
    'muteHttpExceptions': true,
    'method': 'post',
    'Content-Type': 'application/json',
    'headers': {
      "Authorization" :"Bearer "+token
      
      },
    
    'payload': JSON.stringify(data)
  };
  var result = UrlFetchApp.fetch(requestURL, options);
  Logger.log(result);
}
0 Kudos

@themidiman 

You're welcome!  In the options variable, try changing 'Content-Type' to 'contentType' to see if that helps.

0 Kudos

@JamesSekcienski ,

Thank you 

This is the missing piece of the puzzle. The earlier forum post was misleading me all this time. The assertion was that Google's App Script Documentation for URLFetch used 'contentType' instead of 'Content-Type' as the header name, but Canvas API didn't like it that way. It appears that isn't the case. Here's my final code that actually works now (hooray!)

 

 

function enrollUser(user_id,course_id) {
  var requestURL = "<CANVAS_DOMAIN_URL>/api/v1/courses/" + course_id + "/enrollments"
  var data = {
     'enrollment': {
        'user_id': user_id,
        'type': 'TeacherEnrollment'
    }
  };
  var options = {
    'muteHttpExceptions': true,
    'method': 'post',
    'contentType': 'application/json',
    'headers': {
      "Authorization" :"Bearer "+token
      
      },
    'payload': JSON.stringify(data)
  };
  var result = UrlFetchApp.fetch(requestURL, options);
  Logger.log(result);
}

 

 

@themidiman 

I'm glad that solved it for you!  I guess there must have been an update since that other forum post.

dtod
Community Contributor

"I'm not sure at the moment if Google App Scripts can import external libraries of additional JS code"

Yes, they can. I built a generic Canvas API for apps script that also used an OAuth2 library. I never quite got to the point of finalizing it though.

 

0 Kudos
bbennett2
Community Champion

@themidiman Sorry for not replying sooner - I honestly don't remember that original forum post or what I was doing at the time. It depends on where you're putting the options. In GAS, you can set the content type header either in the options with contentType like you did or adding it as Content-Type inside the header object before the request. Either should work.

As far as examples, I also started a wrapper library for Canvas in Google Apps script based on the UCF python library. I haven't touched it in a while, but I have the PaginatedList object for iterating results Requester and objects working for GET requests. Here's the project so far. Inside src, I have some modules started.

To be honest, I've moved away from GAS as a go-to for working with Canvas because there are so many variables I can't control and I felt like I was fighting the script more often than being productive. But, from time to time, I'll jump back in. I think for the GAS library, I'm going to focus more on utility classes like the Requester object rather than duplicating the entire project.

@bbennett2 , thanks for getting back to this thread.

I'm not denying your claim that either 'contentType' or 'Content-Type' header will work, but until I changed it to 'contentType' my POST requests wouldn't work.

That's awesome you've started a GAS library. I'll have to check it out.

0 Kudos