I am trying to use the Google Sheets API. The problem is, once I call my script function on the google spreadsheet, I get the following error:
API call to sheets.spreadsheets.values.get failed with error: The request is missing a valid API key. (line 5).
where line 5 in the script looks like this:
var values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values;
and spreadsheetId and rangeName are defined in the first lines.
I think the problem might be that I did not copy the API key anywhere, but I really do not understand where and how I can do it.
I call the function just using = function().
When you use Sheets API by a custom function like =myFunction() put to a cell, such error occurs. When the custom function is run, ScriptApp.getOAuthToken() returns null. I think that this is the mainly reason of your issue. And unfortunately, I think that this is the specification. In order to avoid this issue, I would like to propose 2 workarounds.
Workaround 1:
A part of Spreadsheet Services can be used at the custom function. So using this, it obtains the same result with var values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values;. In the case of your script, openById() cannot be used. So the script is as follows.
Sample script:
function customFunc() {
var rangeName = "#####"; // Please set this.
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var values = sheet.getRange(rangeName).getValues();
return values;
}
Workaround 2:
If you want to use Sheets API, the access token is required. But it is required to directly request to the endpoint of Sheets API, because the access token is automatically used in internal at Advanced Google Services. As an issue of this case, there is that when ScriptApp.getOAuthToken() is run in the custom function, null is returned. In order to avoid this, as a method, the access token is directly given to the custom function using PropertiesService. The sample flow is as follows.
When the Spreadsheet is opened, it puts the access token to PropertiesService by the OnOpen trigger.
When you use this, please install the OnOpen trigger to onOpenFunc() in the sample script.
When the custom function is run, the access token retrieved by PropertiesService is used for using Sheets API.
By this, Sheets API can be used in the custom function.
Sample script:
// Please install OnOpen trigger to this function.
function onOpenFunc() {
PropertiesService.getScriptProperties().setProperty("accessToken", ScriptApp.getOAuthToken());
}
function customFunc() {
var spreadsheetId = "#####"; // Please set this.
var rangeName = "#####"; // Please set this.
var accessToken = PropertiesService.getScriptProperties().getProperty("accessToken");
var url = "https://sheets.googleapis.com/v4/spreadsheets/" + spreadsheetId + "/values/" + rangeName;
var res = UrlFetchApp.fetch(url, {headers: {"Authorization": "Bearer " + accessToken}});
var obj = JSON.parse(res.getContentText());
var values = obj.values;
return values;
}
The expilation time of access token is 1 hour. In this sample script, PropertiesService is used. In this case, when 1 hour is spent after the Spreadsheet was opened, the access token cannot be used. If you want to use continuously the access token, you can also update it using the time-driven trigger.
Note:
When you use Sheets API, please enable Sheets API at API console.
References:
Custom Functions in Google Sheets
spreadsheets.values.get
PropertiesService
If these workarounds were not what you want, I apologize.
I want to thank you, #Tanaike, for your response (I don't have enough 'points' to upvote or comment, so my only option is an 'Answer')
I know this thread is several years old, but I thought others might be interested in my personal experience.
First of all: "Workaround 1" worked for me!
The function/method "Sheets.Spreadsheets.Values.get(spreadsheetID, RangeName).values" was giving me an "missing a valid API key" error, so I swapped it for "sheet.getRange(RangeName).getValues()".
Most of the above was set as Global Variables, i.e. outside of any functions.
Weird thing was that this error occured only when running from within the [container] sheet, not from the "embedded" script.
For instance: If I had an active onOpen() function, and I opened/refreshed the Sheet, the script would log a "The request is missing a valid API key." error, and the UI/Menu I had built therein would not load.
I could, however run the onOpen() function from within the script itself, and the menu would appear, and function, within the Sheet. If I disabled/renamed the onOpen() function, and reloaded the Sheet, I would not get the error message.
Makes sense, as the simple loading of the Sheet does not appear to run the script, but when one does access it (the script), i.e. through the onOpen() function, then the initial global variables are read (and the error occurs).
However, when I ran the same function, or others, from within the script itself, they would run ok. This permissions conundrum is what has led me on a wild goose chase all over the Internet, ultimately landing here.
All this after numerous other issues, in the course of whose resolution I built a Google Cloud Project, added APIs (e.g. for Sheets), added scopes to the oauthScopes section of the manifest, and more.
It was only after I made the replacements described above that everything worked, both from the script, and its container spreadsheet! So, THANKS!
Anyway... Sorry for the long post, but I hope others may benefit from your solution, and in which context it helped me.
Cheers,
-Paul
Related
I'm new to GAS and I struggle with the permission system.
I'm a normal Google drive user and I started a spreadsheet and tried to add some code to it. My code is working, but only if I'm in the code editor. I want to use the onEdit() function so it's important for me that it works within the sheet as well. When I ran my code in the editor for the first time it opened a new window where I needed to enter my credentials to allow the script, then it worked. If I do some changes to a cell in my sheet and the onEdit() function is triggered I receive an error message that says something like this(translated):
Exception: You are not permitted to call UrlFetchApp.fetch. Required permission: https://www.googleapis.com/auth/script.external_request
In the editor I displayed the manifest file and added the permission to the oauthScopes but within the sheet I still receive the message. This is how my code looks like (simplified):
function onEdit(e)
{
var data = {
'key1': 'value1',
'key2': 'value2'
};
var options = {
'method' : 'post',
'contentType': 'application/json',
'payload' : JSON.stringify(data)
};
try{
var response = UrlFetchApp.fetch('https://a-working-url.com', options); //error happening in this line
//some more data wizardry
}catch(error)
{
Browser.msgBox(error)
}
}
Any ideas how I can open this permission screen in my sheet or any hints how to solve it in a different way? I want to create a sheet with some code running in the back online. I want to share the sheet with some friends, tried it with Excel and VBA before until I realized that it's not working with Excel Online, so I switched to GAS.
onEdit(), like all simple triggers, is bound by the following restrictions (see official documentation):
The script must be bound to a Google Sheets, Slides, Docs, or Forms file, or else be an add-on that extends one of those
applications.
They do not run if a file is opened in read-only (view or comment) mode.
Script executions and API requests do not cause triggers to run. For example, calling Range.setValue() to edit a cell does not cause
the spreadsheet's onEdit trigger to run.
They cannot access services that require authorization. For example, a simple trigger cannot send an email because the Gmail
service requires authorization, but a simple trigger can translate
a phrase with the Language service, which is anonymous.
They can modify the file they are bound to, but cannot access other files because that would require authorization.
They may or may not be able to determine the identity of the current user, depending on a complex set of security restrictions.
They cannot run for longer than 30 seconds.
In certain circumstances, editor add-ons run their onOpen(e) and onEdit(e) simple triggers in a no-authorization mode that presents some additional complications. For more information, see the guide
to the add-on authorization lifecycle.
Simple triggers are subject to Apps Script trigger quota limits.
The ones highlighted in bold apply to your question.
Basically, it boils down to this - UrlFetchApp.fetch() is a service that requires authorization, so you won't be able to execute it from your onEdit(e) trigger, even if you have its associated scope set in your manifest file.
Use installable trigger instead and write your own "onEdit" function (with a different name) that you bind to your installable trigger.
https://developers.google.com/apps-script/guides/triggers/installable#g_suite_application_triggers
This solved the issue for me.
What I wish to achieve:
Whenever a cell is changed in any google sheet on my shared drive (by
any user on the domain) I want to call an API endpoint and include
information about which cell was edited.
My approach:
I believe Google App Scripts Add-on is what I need. Installed for all users on the domain.
I see there are "bound" scripts and standalone scripts. For standalone scripts I am not able to create any other triggers than timer and calender based triggers. Bound scripts seem to be permanently bound to a single sheet and won't impact other sheets in any way.
What am I missing?
I find a few end-to-end tutorials on blogs for making bound scripts, but nothing for generic cross-domain stuff.
You can achieve all this through a standalone script. Create a standalone script and follow these steps:
Step 1: Get spreadsheet ids
First you would have to get the id of the different Spreadsheets in your shared drive. You can do it in Google Apps Script itself if you use the Advanced Drive Service (see Reference below). To activate this service, go to Resources > Advanced Google services... in your script editor and enable Drive API.
Then, write a function that will return an array of the spreadsheet ids in the shared drive. You will have to call Drive.Files.list for that. It could be something along the following lines (please write your shared driveId in the corresponding line):
function getFileIds() {
var params = {
corpora: "drive",
driveId: "your-shared-drive-id", // Please change this accordingly
includeItemsFromAllDrives: true,
q: "mimeType = 'application/vnd.google-apps.spreadsheet'",
supportsAllDrives: true
}
var files = Drive.Files.list(params)["items"];
var ids = files.map(function(file) {
return file["id"];
})
return ids;
}
Step 2: Create triggers for each spreadsheet
Install an onEdit trigger programmatically for each of the spreadsheets (an edit trigger fires a function every time the corresponding spreadsheet is edited, so I assume this is the trigger you want). For this, the ids retrieved in step 1 will be used. It could be something similar to this:
function createTriggers(ids) {
ids.forEach(function(id) {
var ss = SpreadsheetApp.openById(id);
createTrigger(ss);
})
}
function createTrigger(ss) {
ScriptApp.newTrigger('sendDataOnEdit')
.forSpreadsheet(ss)
.onEdit()
.create();
}
The function createTriggers gets an array of ids as a parameter and, for each id, creates an onEdit trigger: everytime any of these spreadsheets is edited, the function sendDataOnEdit will run, and that's where you will want to call your API endpoint with information about the edited cell.
Step 3: Call API endpoint
The function sendDataOnEdit has to get data from the edited cell and send it somewhere.
function sendDataOnEdit(e) {
// Please fill this up accordingly
var range = e.range;
var value = range.getValue();
UrlFetchApp.fetch(url, params) // Please fill this up accordingly
}
First, it can get information about the cell that was edited via the event object, passed to the function as the parameter e (you can get its column, its row, its value, the sheet and the spreadsheet where it is located, etc.). For example, to retrieve the value of the cell you can do e.range.getValue(). Check the link I provide in reference to get more details on this.
Second, when you have correctly retrieved the data you want to send, you can use UrlFetchApp.fetch(url, params) to make a request to your URL. In the link I provide below, you can see the parameters you can specify here (e.g., HTTP method, payload, etc.).
Please bear in mind that you might need to grant some authorization to access the API endpoint, if this is not public. Check the OAuth reference I attach below.
(You have to edit this function accordingly to retrieve and send exactly what you want. What I wrote is an example).
Summing this up:
In order to create the triggers you should run createTriggers once (if you run it more times, it will start creating duplicates). Run for example, this function, that first gets the file ids via Drive API and then creates the corresponding triggers:
function main() {
var ids = getFileIds();
createTriggers(ids);
}
Also, it would be useful to have a function that will delete all the triggers. Run this in case you want to start from fresh and make sure you don't have duplicates:
function deleteTriggers() {
var triggers = ScriptApp.getProjectTriggers();
triggers.forEach(function(trigger) {
ScriptApp.deleteTrigger(trigger);
})
}
Reference:
Advanced Drive Service
Drive.Files.list
onEdit trigger
Install trigger programmatically
onEdit event object
UrlFetchApp.fetch(url, params)
Connecting to external APIs
OAuth2 for Apps Script
ScriptApp.deleteTrigger(trigger)
I hope this is of any help.
I am writing apps script of A spread sheet for my custom functions and trying to get values in B spread sheet from there using openUrl()
However, I got ERROR in A spreadsheet when I use the custom function..
in Google Document, it says
If your custom function throws the error message You do not have permission to call X service., the service requires user authorization and thus cannot be used in a custom function.
ref: https://developers.google.com/apps-script/guides/sheets/functions
However, it doesn't say anything how to get the permission..
I tried with sharable link to everyone but it didn't work.
I tried with url&scope=https://www.googleapis.com/auth/spreadsheets
Both way didn't work. How can I solve this problem?
Unfortunately, what you're asking for cannot be done directly. In the same documentation, please scroll all the way down to sharing; that's -
Custom Functions in Google Sheets > Advanced > Sharing.
Here, you'll see the following -
Custom functions start out bound to the spreadsheet they were created in. This means that a custom function written in one spreadsheet can't be used in other spreadsheets unless...
If it suits you, you can make use of the 3 methods that they've listed there to overcome this problem.
Hope this helps!
Here's how I solved:
You can't access other spreadsheet files, which means you can't use openById() or openByUrl(). So, there is only one method you can achieve this - using getActiveSpreadSheet() which is the current spreadsheet that the app script belongs to.
If you wanted to hide the original file, then you can consider making a reference file that is sharable for anyone. And your original file is private.
So, You need to make it one more file to share.
original file (private)
Use getactiveSpreadSheet() instead of openById() or openByUrl(). And implement your custom functions. You won't have any problem with accessing the current spreadsheet in this way.
function YOUR_CUSTOM_FUNCTION(val1, val2, ...){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("YOUR_SHEET_NAME");
return getMyValue(val1, val2, ...);
}
function getMyValue(val1, val2, ...){
var result = 0;
// TODO: calculate values as you want.
return result;
}
Use your custom function in the file like this:
=YOUR_CUSTOM_FUNCTION(E5, F6, ...)
You won't get any ERROR.
reference file (sharable)
You can make a reference sheet with using:
=IMPORTRANGE("spread sheet link","sheet_name!range")
This will show access button to get authorization. Once you click, it will make a reference from your original sheet.
*range is something like A:C or A1:Z55.
I am trying to automate a google spreadsheet on google script editor. However, when I try to identify the spreadsheet and select as the one that we are working on, it is written in the documentation cited below (openById) that scripts that use this method require authorization with one or more of the following scopes:
--> https://www.googleapis.com/auth/spreadsheets.currentonly
--> https://www.googleapis.com/auth/spreadsheets
When I add these scopes, my function doesn't run. It only runs when those scopes are not added. Either way, I am met with the error msg that reads: "We are sorry, a server error occurred. Please wait a bit and try again."
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#openbyidid
I tried various methods include OpenByUrl and getActiveSpreadsheet...
function automatedInvoiceEmailing() {
var people = [];
// selecting the spreadsheet (without the bottom line, the function works just fine)
var ss = SpreadsheetApp.openById("1jdn3S1Iv2zDAqF6Hyy3fybKARZJYmg-LJVdUWJJS3LA");
}
Either way, I am met with the error msg that reads: "We are sorry, a server error occurred. Please wait a bit and try again."
I expected the sheet to have been selected
Edit: hmm, when I copied the google sheets and saved the new code, it runs properly!! :)
Since you are working with a bounded script (bound to the sheet you’re working with), the way to get a reference to the sheet is with var ss = SpreadsheetApp.getActiveSheet();, this will provide a reference to the bound document. You would use openById() to access another file.
As for scopes, they are added automatically when you run the script for the first time, it will ask for your permission to access your data, and when you accept, google adds them to the script project automatically.
Here are some quick-start examples on working with sheets and apps script
When I create an Apps Script library that accesses a spreadsheet and use it in a script then I get a permission error (for setValue in the following example).
If I call the same spreadsheet function in the script once (and then remove it) and then call the library function I will never get the permission error again (It's reproducible).
Have you ever experienced such behavior and if yes how did you solve this problem?
Thanks
The library
function addRecord(ss, sheetName) {
var sheet = ss.getSheetByName(sheetName);
sheet.getRange("A1:A1").setValue("Hello World!");
}
The script
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
MyLib.addRecord(ss, "Sheet1");
}
The error message
"You don’t have the required permission to call setValue"
Your script must have the same authorizations than the libraries need to have : if you know that the library reads/writes a spreadsheet or a calendar (or whatever) you have to authorize the script for these services.
You can do that quite easily with some (even dummy ) function that will tell the system to call the authorization process for the required services. From what you describe I guess the system doesn't check what is inside the library when you save you script so it's kind of a 'surprise' for the script to write to the spreadsheet without authorization. If you include the (dummy) calls I mentioned before the script will know what you are going to do and ask for authorization on the first run attempt.
Hoping I'm clear enough.
To close this - the issue reported by Stefan has been confirmed as an issue. We are looking into this. Permissions to a passed in spreadsheet are not being correctly absorbed by a library.
Stefan - please feel free to create a report on issue tracker if you prefer.