Task guide
Import data from a web API into Sheets
Pull a seven-day weather forecast from the free Open-Meteo API into a practice spreadsheet. The script checks the response and writes only cells A1:C12 of the Forecast tab, with the source and license links below the data.
1 · Prepare
Put these pieces in place
- Create a new Google spreadsheet named Forecast practice. Do not use a spreadsheet you already work in. Rename its blank tab Forecast. Leave cells A1 to C12 empty and unmerged, because the script writes there.
- The free Open-Meteo API is for noncommercial use that meets https://open-meteo.com/en/terms. It needs no key. Commercial use and higher request volumes have separate requirements.
- Keep the supplied Berlin coordinates for the first run. Open-Meteo sees the coordinates, the usual request headers, and the address of the Google server that sends the request. The script sends no spreadsheet data and no Google token.
- Keep the Open-Meteo credit, the CC BY 4.0 link, and the note that says which data was requested next to any forecast data you copy. If you change the data, say how. The data license at https://open-meteo.com/en/licence is separate from permission to use the free service.
Change these values
- Forecast length
FORECAST_DAYS = 7A whole number from 1 to 7. The output area always has room for seven days. When you ask for fewer, the unused daily rows are left blank.
- Public location
FORECAST_LATITUDE = 52.52; FORECAST_LONGITUDE = 13.41Latitude is a number from -90 to 90 and longitude a number from -180 to 180. Use a public place, such as a city center, instead of your home address.
2 · Review before running
What the script can access and change
Permissions you approve
- Connect to an external service. The manifest lists script.external_request, which lets the script send HTTPS requests to other services. Approving access in Google does not give you rights under the Open-Meteo terms. The script requests one public forecast and sends no credentials.
- View and manage spreadsheets that this application has been installed in. The manifest lists spreadsheets.currentonly, which covers the whole spreadsheet the script is attached to. The code keeps writes inside Forecast!A1:C12, and a manual run acts as the signed-in user who clicks Run.
Effects of one run
- Sends at most one GET request to api.open-meteo.com with the coordinates, the requested fields, the units, the time zone setting, and the number of days. It does not retry automatically. It sends no spreadsheet ID, no cell values, and no Google access token.
- Checks the Forecast tab before the request and again before writing. It stops with an error if the tab is missing, if any of A1:C12 is merged, or if the range has data but no marker in A12. It never clears existing data to make room. The marker only tells the script which cells it wrote. It does not stop other people or scripts from editing them.
- Checks the response before writing: the status code, the content type, the size, the JSON, the number of days, the dates, the units, and that every temperature is a number. Then it writes one block of 12 rows by 3 columns. It does not clear the tab, create a sheet, or write outside A1:C12. Blank cells replace old daily rows that a shorter forecast does not use.
- If the script fails before setValues, the earlier forecast stays in place and may be out of date. The write itself can also fail partway. Look at the cells before you decide to run again.
3 · Copy
Paste this into the Apps Script editor
Create or open a script file, replace the example values marked above, then save the project.
const FORECAST_DAYS = 7;
const FORECAST_LATITUDE = 52.52;
const FORECAST_LONGITUDE = 13.41;
const FORECAST_OWNER_MARKER = 'Owned by weather-import';
/**
* A property-access view for validation, not a validated provider response.
* Every data field remains unknown until the checks in forecastRows succeed.
* @typedef {{daily?: {time?: unknown, temperature_2m_max?: unknown, temperature_2m_min?: unknown}|null, daily_units?: {temperature_2m_max?: unknown, temperature_2m_min?: unknown}|null}} ForecastCandidate
*/
function importForecastToSheet() {
if (!Number.isInteger(FORECAST_DAYS) || FORECAST_DAYS < 1 || FORECAST_DAYS > 7) {
throw new Error('Set FORECAST_DAYS to a whole number from 1 to 7.');
}
if (!Number.isFinite(FORECAST_LATITUDE) || Math.abs(FORECAST_LATITUDE) > 90 ||
!Number.isFinite(FORECAST_LONGITUDE) || Math.abs(FORECAST_LONGITUDE) > 180) {
throw new Error('Use numeric latitude from -90 to 90 and longitude from -180 to 180.');
}
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
if (!spreadsheet) {
throw new Error('Open this project through Extensions > Apps Script in the practice spreadsheet.');
}
const sheet = spreadsheet.getSheetByName('Forecast');
if (!sheet) {
throw new Error('Create a blank tab named Forecast before running this function.');
}
const target = sheet.getRange('A1:C12');
checkForecastDestination(target, sheet);
const query = [
'latitude=' + encodeURIComponent(FORECAST_LATITUDE),
'longitude=' + encodeURIComponent(FORECAST_LONGITUDE),
'daily=temperature_2m_max,temperature_2m_min',
'temperature_unit=celsius',
'timezone=auto',
'forecast_days=' + FORECAST_DAYS
].join('&');
const response = UrlFetchApp.fetch('https://api.open-meteo.com/v1/forecast?' + query, {
method: 'get',
headers: { Accept: 'application/json' },
muteHttpExceptions: true,
followRedirects: false
});
const statusCode = response.getResponseCode();
if (statusCode !== 200) {
throw new Error('The forecast service answered with status code ' + statusCode + '. No cells were written.');
}
const contentType = forecastHeader(response.getAllHeaders(), 'content-type');
if (contentType.split(';')[0].trim().toLowerCase() !== 'application/json') {
throw new Error('The forecast response was not application/json. No cells were written.');
}
const text = response.getContentText('UTF-8');
if (text.length > 50000) {
throw new Error('The forecast response exceeded the 50000-character parsing limit.');
}
/** @type {unknown} */
let forecast;
try {
forecast = JSON.parse(text);
} catch (error) {
throw new Error('The forecast response was not valid JSON. No cells were written.');
}
const rows = forecastRows(forecast, FORECAST_DAYS);
// Check again after the request in case the destination changed while it was pending.
checkForecastDestination(target, sheet);
target.setValues(rows);
Logger.log('Wrote ' + FORECAST_DAYS + ' days of forecast.');
}
/**
* @param {SpreadsheetApp.Range} target
* @param {SpreadsheetApp.Sheet} sheet
* @returns {void}
*/
function checkForecastDestination(target, sheet) {
if (target.isPartOfMerge()) {
throw new Error('Forecast!A1:C12 must not overlap merged cells.');
}
if (target.isBlank()) return;
const markerCell = sheet.getRange('A12');
if (markerCell.getValue() !== FORECAST_OWNER_MARKER || markerCell.getFormula() !== '') {
throw new Error('Forecast!A1:C12 is not blank and has no valid ownership marker. Use a new practice sheet; do not clear existing data to bypass this check.');
}
}
/**
* @param {unknown} rawForecast
* @param {number} expectedDays
* @returns {Array<Array<string|number>>}
*/
function forecastRows(rawForecast, expectedDays) {
// This view permits property probes; the original runtime checks still validate the value.
const forecast = /** @type {ForecastCandidate|null|undefined} */ (rawForecast);
if (!forecast || typeof forecast !== 'object' || Array.isArray(forecast) ||
!forecast.daily || !forecast.daily_units) {
throw new Error('The forecast response is missing daily data or units.');
}
const days = forecast.daily.time;
const highs = forecast.daily.temperature_2m_max;
const lows = forecast.daily.temperature_2m_min;
if (!Array.isArray(days) || !Array.isArray(highs) || !Array.isArray(lows) ||
days.length !== expectedDays || highs.length !== expectedDays || lows.length !== expectedDays) {
throw new Error('The forecast arrays must each contain exactly ' + expectedDays + ' days.');
}
if (forecast.daily_units.temperature_2m_max !== '°C' ||
forecast.daily_units.temperature_2m_min !== '°C') {
throw new Error('The forecast temperature units must be Celsius.');
}
/** @type {Array<Array<string|number>>} */
const rows = [['Date', 'High', 'Low']];
let index = 0;
/** @type {number|null} */
let previousDay = null;
while (index < expectedDays) {
/** @type {unknown} */
const day = days[index];
/** @type {unknown} */
const high = highs[index];
/** @type {unknown} */
const low = lows[index];
if (typeof day !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(day)) {
throw new Error('Forecast day ' + (index + 1) + ' is not a YYYY-MM-DD date.');
}
const timestamp = Date.parse(day + 'T00:00:00Z');
if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString().slice(0, 10) !== day ||
(previousDay !== null && timestamp - previousDay !== 86400000)) {
throw new Error('Forecast dates must be valid consecutive calendar days.');
}
if (!Number.isFinite(high) || !Number.isFinite(low) ||
/** @type {number} */ (high) < /** @type {number} */ (low)) {
throw new Error('Forecast day ' + (index + 1) + ' needs numeric high and low temperatures with high >= low.');
}
rows.push([day, /** @type {number} */ (high), /** @type {number} */ (low)]);
previousDay = timestamp;
index = index + 1;
}
// The fixed output area has room for seven days, even when fewer are requested.
while (rows.length < 8) {
rows.push(['', '', '']);
}
rows.push(['Weather data by Open-Meteo', 'https://open-meteo.com/', '']);
rows.push(['Licence: CC BY 4.0', 'https://creativecommons.org/licenses/by/4.0/', '']);
rows.push(['Selected daily highs/lows; Celsius; local dates', '', '']);
rows.push([FORECAST_OWNER_MARKER, '', '']);
return rows;
}
/**
* @param {Record<string, string|string[]>} headers
* @param {string} wantedName
* @returns {string}
*/
function forecastHeader(headers, wantedName) {
for (const name of Object.keys(headers)) {
if (name.toLowerCase() === wantedName) {
const value = headers[name];
if (Array.isArray(value)) return value.join(', ');
return String(value);
}
}
return '';
}
4 · Run
Follow these steps in order
Paste the code into the new spreadsheet
In Forecast practice, choose Extensions, then Apps Script. Replace all of Code.gs with the complete source, including its constants and every helper function. Leave Forecast!A1:C12 blank and unmerged. On each run the script writes a marker in A12, Owned by weather-import, so it can recognize its own output later. Do not type that marker into cells yourself.
Set the runtime and permissions in the manifest
Open Project Settings, the gear icon in the left sidebar, and check Show "appsscript.json" manifest file in editor. Back in the editor, open appsscript.json and replace its contents with this manifest: {"timeZone":"Etc/UTC","dependencies":{},"exceptionLogging":"STACKDRIVER","runtimeVersion":"V8","oauthScopes":["https://www.googleapis.com/auth/script.external_request","https://www.googleapis.com/auth/spreadsheets.currentonly"]}. Save both files. You do not need an advanced service, a library, a deployment, or a trigger.
Run the seven-day forecast
Keep FORECAST_DAYS at 7 and the supplied coordinates. Choose importForecastToSheet in the function dropdown above the code, then click Run. Run it while signed in as the owner of this practice spreadsheet. Google asks to let the script use this spreadsheet and connect to an external service. Approve both. Do not start a second run, and do not edit cells A1:C12 while the script runs.
Inspect the forecast and its attribution
Expect Date, High, and Low in row 1 and seven daily forecasts in rows 2 to 8. Rows 9 to 12 hold the source link, the license link, the note on which data was requested, and the marker. The Execution log says Wrote 7 days of forecast. Exact dates and temperatures vary.
Try a shorter forecast, then go back to seven days
Change FORECAST_DAYS to 3 and run once. Expect three daily rows and blank cells in A5:C8. Rows 9 to 12 stay the same. Change the value back to 7 and run again. To see the input check, set FORECAST_DAYS to 8 and run. The script stops with an error before it fetches or writes anything. Set it back to 7 before you continue.
Keep notes outside A1:C12 and delete the practice file when done
Every later run overwrites all of A1:C12, including anything you typed there. Keep other notes outside that range. If you are not sure a run finished writing, look at A1:C12 before you run again. The fetch and the write to Sheets are separate steps, and one can succeed while the other fails. When you finish, delete the Forecast practice spreadsheet, after checking that nothing else was added to it.
5 · Confirm
How to tell it worked
Forecast!A1:C8 contains headers and seven daily forecasts in Celsius, one per local date. Rows 9 to 12 hold the credit, the license, the note on which data was requested, and the marker. The Execution log says Wrote 7 days of forecast. A three-day run blanks the unused daily rows, so no older forecast is left below the new one.
If the result is different
Troubleshooting
- The run stops with an error saying the Forecast tab is missing, merged, or has data but no marker.
- Check that you are in the new practice spreadsheet and that its Forecast tab is blank and unmerged. Do not clear other data or type the marker by hand to get past this check. After the first successful write, leave the marker in A12 and keep everything else out of A1:C12.
- The run stops with an error naming a status code other than 200, or the fetch itself fails.
- Read the status code or the error message in the Execution log. Do not log the full response. A 400 usually means a request parameter is invalid. Authorization and quota errors each need their own fix. If the service is busy or down, check the Open-Meteo documentation and your remaining quota, then try again later. Do not add a loop that retries without checking why the request failed.
- The run stops with an error about the content type, the JSON, the dates, the units, or the temperatures.
- Note the error message and your request settings, and compare them with https://open-meteo.com/en/docs. Leave the earlier output as it is. The script stops instead of writing unexpected data to cells. Do not change the code to turn missing values into zero or to skip the checks.
- Notes I typed inside A1:C12 were overwritten.
- Keep your own notes outside A1:C12. The script overwrites that whole range on every run, and the marker cannot tell your notes apart from the script output.
- The run ended and the Execution log does not say Wrote 7 days of forecast.
- Look at Forecast!A1:C12 and at Executions in the left sidebar before you run again. The earlier data may be out of date, and some cells may have changed even though the run reported an error. Do not clear the range as a way to recover.
Technical details
Names used in this script
Open these details when you want to adapt the example or look up an exact service or method name.
Show Apps Script symbols and service pages
Apps Script symbols
Service pages
Keep going
Related tasks, basics, and apps
- Task guide Check a website and alert when it is down
Get one email when a website stops answering and one when it is back. An hourly trigger fetches the address, and a saved script property stops the script from emailing you every hour during one outage.
- Task guide Store settings with PropertiesService
Keep a setting such as an email address out of your code and change it without editing the script. The project stores it as a script property that every run can read.
- Object reference HTTP response
UrlFetchApp.fetch() returns an HTTPResponse after calling a URL. Pass muteHttpExceptions: true so error status codes return an HTTPResponse you can check with getResponseCode() and getContentText().
- Object reference Spreadsheet
A Spreadsheet is the whole Google Sheets file. Use SpreadsheetApp.getActiveSpreadsheet() or openById() to get it, then call getSheetByName() or getActiveSheet().
- Object reference Sheet
A Sheet is one tab inside a Google spreadsheet. Get it with getSheetByName() or getActiveSheet(). Use getLastRow() to find the last row with content, or getRange() to select cells to read.
- Object reference Range
A Range represents one cell such as A2 or a rectangular block of rows and columns in a Sheet. Use getValue() for one cell and getValues() or setValues() for a 2D array of rows.
- Google app Script utilities
UrlFetchApp, PropertiesService, and LockService work in any Apps Script project. Use them to call web APIs, save settings between runs, and stop two runs from overlapping.
- Google app Google Sheets
SpreadsheetApp lets a Google Sheets script read a Sheet or Range, add a custom menu or formula, and act on each row, such as sending one email per row.
- Diagnostic SyntaxError: Unexpected token in JSON
Why JSON.parse() fails on HTML error pages or empty bodies, and what to check on the HTTPResponse first.
- Book chapter Fetch data from web APIs
Build and inspect real HTTP requests with UrlFetchApp, validate responses, and handle paginated public data.