> ## Documentation Index
> Fetch the complete documentation index at: https://help.airbridge.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Import Sharelink Data With Google Apps Script

With Google Apps Script, you can import Airbridge Sharelink data into the sheet and cell you choose in Google Sheets. After you paste the sample script and enter your Sharelink URL, you can set it to run automatically every day during the hour you set. This guide explains how to add the sample script and run it for the first time, set up a daily automatic refresh, and change the settings.

## Before you start

Prepare the following to import Sharelink data with Google Apps Script:

* An Airbridge Sharelink URL
* Edit access to the Google spreadsheet you want to import the data into

To create a Sharelink, see [Sharing Airbridge Reports](https://help.airbridge.io/en/guides/share-airbridge-reports.md).

## Add the sample script

In Google Sheets, go to **\[Extensions]>\[Apps Script]**, paste the sample script, and enter your Sharelink URL.

1. In Google Sheets, go to **\[Extensions]>\[Apps Script]**. The Apps Script editor opens in a new tab. The add-ons listed under **\[Apps Script]** vary by account.

![google-apps-script-sharelink-01-menu-extensions-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-01-menu-extensions-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=56d697af554941b08e516a4b5b142e32)

2. Delete all the `function myFunction()` code in the Apps Script editor, and then copy and paste the code from [Sample script](#sample-script).

![google-apps-script-sharelink-03-editor-pasted-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-03-editor-pasted-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=1c031fd2bc1d053cee1db4fbfccb94b5)

3. Paste your Sharelink URL between the quotes of `url` in `TARGETS`, and enter the name of the sheet for the data in `sheet`. Don't delete the single quotes (`' '`) or the comma (`,`) at the end of the line.

![google-apps-script-sharelink-04-targets-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-04-targets-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=9421e79ca46c0490945426bb57ea469c)

4. To save, click the save icon or press Cmd + S on Mac or Ctrl + S on Windows. The script is saved when the status at the top of the editor changes from **\[Unsaved changes]** to **\[Saved to Drive]**.

![google-apps-script-sharelink-05-save-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-05-save-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=e1a5a903dc39e08bae4457012cf9434c)

5. Return to the spreadsheet tab and reload the page. A few seconds after you reload, **\[Sharelink]** appears to the right of **\[Help]**. You can close the Apps Script editor tab.

![google-apps-script-sharelink-06-sheet-menu-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-06-sheet-menu-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=60464d3e550a1cac65a209f1992ef7c6)

## Authorize the script and run it for the first time

The first time you run Google Apps Script, you must authorize it with your Google account. You only need to authorize it once.

> [!NOTE]
>
> **Note**
>
> If you authorize Google Apps Script with a personal Google account, a **\[Google hasn't verified this app]** warning may appear before the Google permission screen. The warning means that the script you pasted has not gone through Google's verification process. To continue to the Google permission screen, click **Advanced**, and then click the link that appears below it.

1. In Google Sheets, select **\[Sharelink]>\[Refresh now]**.

![google-apps-script-sharelink-07-menu-open-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-07-menu-open-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=c57a401f655dc2185e2e1761b8a0b959)

2. In the **\[Authorization required]** window, click **OK**.

![google-apps-script-sharelink-08a-auth-required-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-08a-auth-required-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=797236be94c12913a003c04a3b97430c)

3. On the Google permission screen that opens in a new window, select the **Select all** checkbox, and then click **Continue**.

![google-apps-script-sharelink-08e-allow-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-08e-allow-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=5c7cb1154d39a234c1d7aea59d3b50c2)

The following table lists the three permissions on the Google permission screen and why the script needs each one.

| Permission shown on the screen | Why it's needed |
| - | - |
| See, edit, create, and delete all your Google Sheets spreadsheets | Required to write the imported data to the spreadsheet. |
| Connect to an external service | Required to fetch the data from the Sharelink URL. |
| Allow this application to run when you are not present | Required to run automatically every day during the hour you set. |

4. Wait for the Google permission screen to close. The refresh then continues automatically, so you don't need to select **\[Refresh now]** again.

![google-apps-script-sharelink-08f-after-auth-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-08f-after-auth-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=bc1e4b66781f012c1f50b36cc0920948)

5. When `1 of 1 done` appears in the lower-right corner of the screen, check that the data is in the sheet you specified.

![google-apps-script-sharelink-09-result-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-09-result-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=bc45d5c541d9222b95fe147b593d662a)

## Set up a daily automatic refresh

To refresh Sharelink data automatically every day during the hour you set, turn on the daily refresh in **\[Sharelink]**. The default time in the sample script is 9 AM. To change the time, see [Change the sample script settings](#change-the-sample-script-settings).

1. In Google Sheets, select **\[Sharelink]>\[Turn on daily refresh at 9:00]**. The daily refresh is on when `Refreshes automatically every day around 9:00.` appears in the lower-right corner of the screen.

![google-apps-script-sharelink-10-menu-daily-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-10-menu-daily-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=f9b62aaf3f5d5f2922037761b11bcbe2)

2. To check that the daily refresh is registered, click **\[Triggers]** on the left of the Apps Script editor. The daily refresh is registered when one `refreshNow` row appears in the list.

![google-apps-script-sharelink-11-triggers-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-11-triggers-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=49a8753c18add78a4fedc19130a89307)

3. Hover over the cell where the data starts and check the `Last fetched:` note. The time in the note changes each time the daily refresh runs.

![google-apps-script-sharelink-12-note-en](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-12-note-en.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=6c560873148f6e4c176802962b85d91f)

The daily refresh runs at some point between 9 AM and 10 AM, not exactly at 9 AM, and at the same point every day. It runs on Google's servers whether or not the spreadsheet is open, even when your computer is off.

To turn off the daily refresh, select **\[Sharelink]>\[Turn off daily refresh]**. `Daily refresh turned off.` appears in the lower-right corner of the screen.

## Change the sample script settings

Change Google Apps Script settings in the `[Settings]` section at the top of the sample script. After you edit the values, save the script and reload the spreadsheet.

The following table shows what to edit for each setting.

| What you want to do | What to edit |
| - | - |
| Add a Sharelink | Add one line to `TARGETS`. |
| Change the sheet or starting cell for the data | Edit `sheet` and `cell` on that line. |
| Change the daily refresh time | Set `DAILY_HOUR` to a number from 0 to 23 (24-hour clock), and then select **\[Sharelink]>\[Turn on daily refresh at N:00]** again. N is the `DAILY_HOUR` value. |
| Start a new query on every run | Add `useCache=false` to the end of `url` on that line. This works only with `/v3/` URLs. |
| Change menu names and emojis | Edit `MENU`. |

The daily refresh time follows **\[Time zone]** in **\[Project Settings]** on the left of the Apps Script editor.

### Import multiple Sharelinks

To import multiple Sharelinks with Google Apps Script, enter one Sharelink per line in `TARGETS`. The following example imports three Sharelinks.

```javascript theme={"theme":{"light":"github-dark-dimmed","dark":"github-dark-dimmed"}}
const TARGETS = [
  { sheet: 'Raw_Actuals',   url: 'https://report.airbridge.io/v3/TOKEN.csv?language=en' },
  { sheet: 'Raw_Retention', url: 'https://report.airbridge.io/v3/TOKEN.csv?language=en' },
  { sheet: 'Dashboard', cell: 'H3', url: 'https://report.airbridge.io/v3/TOKEN.csv?language=en' },
];
```

* If `sheet` matches an existing sheet name, the data goes into that sheet. Otherwise, the script creates a new sheet.
* `cell` is the cell where the data starts. If you omit `cell`, the data starts at A1.
* If the sheet used the `IMPORTDATA` function, delete the function first. Then, in `cell`, enter the cell where the function was.
* To put two Sharelinks in one sheet, set `cell` for the second Sharelink below the last row or to the right of the last column of the first Sharelink's data.

### Start a new query on every run

If the same report was queried in the Airbridge dashboard or through a Sharelink within the last 3 hours, the Sharelink returns the report data from that query instead of starting a new one. Otherwise, the Sharelink starts a new query, and the sample script waits for the query to finish before it imports the new report data.

To start a new query every time Google Apps Script runs, add `useCache=false` to the end of the Sharelink URL in `url` of `TARGETS`. If the URL already contains a question mark (`?`), add `&useCache=false`. Otherwise, add `?useCache=false`. `useCache=false` works only with `/v3/` URLs.

```javascript theme={"theme":{"light":"github-dark-dimmed","dark":"github-dark-dimmed"}}
{ sheet: 'Raw_Fresh',     url: 'https://report.airbridge.io/v3/TOKEN.csv?language=en&useCache=false' },
```

Adding `useCache=false` can make each run take longer. If the query doesn't finish within about 1 minute, the sample script requests the URL again without `useCache=false`. If the query has finished by then, the script imports the newly queried report data. If not, it imports report data queried within the last 3 hours. For details, see [Limitations](#limitations).

### Change the URL in an existing script to `/v3/`

When you change `/v2/`, `/funnel/`, `/retention/`, or `/revenue/` to `/v3/` in the URL of a Google Apps Script you already use, add `useCache=false` to the end of the URL. If no report data was queried within the last 3 hours, a `/v3/` URL starts a new query and immediately returns a notice instead of data. With `useCache=false`, the Sharelink waits until the query finishes and then returns the report data. The Airbridge sample script handles `/v3/` URLs without `useCache=false`, so you can use it as is.

## Sample script

The following is the full sample script to paste into the Apps Script editor. Edit only the `[Settings]` section. The code below it works as is.

> [accordion] Sample script
>
> ```javascript theme={"theme":{"light":"github-dark-dimmed","dark":"github-dark-dimmed"}}
> /* ==========================================================================
>  * Airbridge Sharelink → Google Sheets import
>  *
>  * You only need to edit the [Settings] section. Everything below it works as is.
>  * After editing, save (Cmd+S or Ctrl+S) and reload the sheet.
>  *
>  * Common edits
>  *   Add a link                                   → Add one line to TARGETS
>  *   Change the sheet or position for the data    → Edit sheet and cell on that line
>  *   Change the daily refresh time                → Edit DAILY_HOUR, then turn daily refresh on again from the menu
>  *   Change menu names or emojis                  → Edit MENU
>  *   Frequent "Server is still calculating" fails → Increase WARMUP_WAIT_MS
>  *   Want a new query on every run                → Add useCache=false to a /v3/ URL (see the url notes below)
>  * ========================================================================== */
>
> // ------------------------------------------------------------------ [Settings]
>
> // List of links to import. One link per line.
> //
> //   sheet : Name of the sheet (tab) to put the data in. If a sheet with that name exists, the data goes there;
> //           if not, a new sheet is created.
> //   cell  : Cell where the data starts. Data fills right and down from this cell. Defaults to A1 if omitted.
> //           If the sheet used an IMPORTDATA formula, enter the cell where that formula was.
> //           (Delete the formula first. If the cell has content, it is not overwritten and is reported as a failure.)
> //   url   : Sharelink URL copied from Airbridge. Paste it as is, whether /v2/ or /v3/.
> //           Adding useCache=false starts a new query on every run (/v3/ URLs only).
> //           If the URL contains ?, append &useCache=false; otherwise append ?useCache=false.
> //           The run waits until the new query finishes, so it takes that much longer,
> //           and if there are several such links, it waits for them one by one.
> //
> // Examples
> //   { sheet: 'Raw_Actuals',   url: 'https://report.airbridge.io/v3/TOKEN.csv?language=en' },
> //   { sheet: 'Raw_Retention', url: 'https://report.airbridge.io/v3/TOKEN.csv?language=en' },
> //   { sheet: 'Dashboard', cell: 'H3', url: 'https://report.airbridge.io/v3/TOKEN.csv?language=en' },
> //   { sheet: 'Raw_Fresh',     url: 'https://report.airbridge.io/v3/TOKEN.csv?language=en&useCache=false' },
> //
> // If you miss a comma (,) at the end of a line or a quote ('), nothing works, so follow the example format exactly.
> // When putting two links in one sheet, space their cells far enough apart that they don't overlap.
> const TARGETS = [
>   { sheet: 'Actuals', cell: 'A1', url: 'PASTE_YOUR_SHARELINK_URL_HERE' },
> ];
>
> // Hour of the daily refresh. Enter a number from 0 to 23 (24-hour clock). 9 = 9 AM, 15 = 3 PM.
> // Google picks a moment within one hour from that time and runs it at the same time every day. Minutes cannot be set.
> // The time zone follows the Time zone in Project Settings (gear icon on the left of the Apps Script editor).
> const DAILY_HOUR = 9;
>
> // Menu names shown at the top of the sheet. Feel free to change the text and emojis inside the quotes.
> const MENU = {
>   title: '📊 Sharelink',
>   refresh: '🔄 Refresh now',
>   dailyOn: '⏰ Turn on daily refresh at ' + DAILY_HOUR + ':00',
>   dailyOff: '⏹️ Turn off daily refresh',
> };
>
> // Time to wait after starting the calculation on the server before fetching the result (1000 = 1 second).
> // If heavy reports often fail with "Server is still calculating", increase this to around 40000.
> const WARMUP_WAIT_MS = 25000;
>
> // Interval and number of retries when the data still isn't ready. Usually no need to change.
> const RETRY_WAIT_MS = 15000;
> const MAX_ROUNDS = 4;
>
> // ------------------------------------------------- No need to edit anything below
>
> const STATE_HEADER = 'x-airbridge-sharelink-state';
> const SIZE_KEY_PREFIX = 'written-size:';
>
> /** Creates the menu at the top when the sheet opens. */
> function onOpen() {
>   SpreadsheetApp.getUi()
>     .createMenu(MENU.title)
>     .addItem(MENU.refresh, 'refreshNow')
>     .addSeparator()
>     .addItem(MENU.dailyOn, 'autoDaily')
>     .addItem(MENU.dailyOff, 'autoOff')
>     .addToUi();
> }
>
> /** Entry point shared by the menu and the daily trigger. */
> function refreshNow() {
>   const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
>   spreadsheet.toast('Refreshing ' + TARGETS.length + ' link(s). This takes about a minute.', MENU.title, 120);
>
>   // A plain v3 URL only starts the calculation on the first call if there is no cache, so call each one once first to warm it up.
>   // A useCache=false request starts a new query and waits for the result by itself, so warming it up would run the query twice.
>   const warmupTargets = TARGETS.filter(function (target) {
>     return isV3_(target) && !skipsCache_(target);
>   });
>   warmupTargets.forEach(function (target) {
>     fetchOnce_(target.url);
>   });
>   if (warmupTargets.length > 0) Utilities.sleep(WARMUP_WAIT_MS);
>
>   var pending = TARGETS.slice();
>   const failed = [];
>   var done = 0;
>
>   for (var round = 1; round <= MAX_ROUNDS && pending.length > 0; round++) {
>     const stillWaiting = [];
>
>     pending.forEach(function (target) {
>       const skipCache = skipsCache_(target);
>       // Sending useCache=false again would start yet another query, so once we have to wait, later checks use a plain request.
>       const requestUrl = skipCache && round > 1 ? withoutUseCache_(target.url) : target.url;
>       const result = fetchOnce_(requestUrl, skipCache && round === 1);
>       if (result.waiting) {
>         stillWaiting.push(target);
>         return;
>       }
>       if (!result.rows) {
>         failed.push(labelOf_(target) + ' - ' + result.message);
>         return;
>       }
>       try {
>         write_(target, result.rows);
>         done++;
>       } catch (e) {
>         failed.push(labelOf_(target) + ' - ' + e.message);
>       }
>     });
>
>     pending = stillWaiting;
>     if (pending.length > 0 && round < MAX_ROUNDS) Utilities.sleep(RETRY_WAIT_MS);
>   }
>
>   pending.forEach(function (target) {
>     failed.push(labelOf_(target) + ' - Server is still calculating');
>   });
>
>   const summary = done + ' of ' + TARGETS.length + ' done';
>   spreadsheet.toast(failed.length === 0 ? summary : summary + ' / Failed: ' + failed.join(', '), MENU.title, failed.length === 0 ? 5 : 30);
> }
>
> function isV3_(target) {
>   return /\/v3\//i.test(target.url);
> }
>
> /** Checks whether the query of a v3 URL has useCache=false. The value is matched ignoring case. */
> function skipsCache_(target) {
>   if (!isV3_(target)) return false;
>   const queryStart = target.url.indexOf('?');
>   if (queryStart < 0) return false;
>   const query = target.url.slice(queryStart + 1).split('#')[0];
>   return /(?:^|&)useCache=[Ff][Aa][Ll][Ss][Ee](?:&|$)/.test(query);
> }
>
> /** Removes only the useCache parameter, leaving other query parameters and the fragment as they are. */
> function withoutUseCache_(url) {
>   const hashStart = url.indexOf('#');
>   const hash = hashStart < 0 ? '' : url.slice(hashStart);
>   const beforeHash = hashStart < 0 ? url : url.slice(0, hashStart);
>   const queryStart = beforeHash.indexOf('?');
>   if (queryStart < 0) return url;
>
>   const base = beforeHash.slice(0, queryStart);
>   const params = beforeHash.slice(queryStart + 1).split('&').filter(function (param) {
>     return param.split('=')[0].toLowerCase() !== 'usecache';
>   });
>   return base + (params.length > 0 ? '?' + params.join('&') : '') + hash;
> }
>
> function labelOf_(target) {
>   return target.sheet + '!' + (target.cell || 'A1');
> }
>
> /**
>  * v3 is judged by the state header. v2 and per-report URLs, which have no header, are judged by the shape of the body.
>  * A normal report has multiple columns, while a notice message comes as a single cell.
>  */
> function fetchOnce_(url, waitOnError) {
>   var response;
>   // muteHttpExceptions: 4xx and 5xx come back as responses and are judged by status code below.
>   // The catch block only runs when no response arrived at all (connection lost or timeout).
>   try {
>     response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
>   } catch (e) {
>     // With useCache=false, the server keeps running the query even if the client gives up first, so recheck with a plain request.
>     if (waitOnError) return { waiting: true };
>     return { message: 'No response' };
>   }
>
>   // Decision order. Only in-progress is rechecked in the next round (every RETRY_WAIT_MS, up to MAX_ROUNDS rounds).
>   // 4xx and 5xx fail immediately without a retry.
>   const state = readState_(response);
>   if (state === 'in-progress') return { waiting: true };
>   if (response.getResponseCode() === 404) return { message: 'Link not found' };
>   if (response.getResponseCode() >= 400) return { message: 'http ' + response.getResponseCode() };
>
>   const text = response.getContentText();
>   if (state && state !== 'ok') return { message: text.slice(0, 120) };
>
>   const rows = Utilities.parseCsv(text);
>   const isNotice = rows.length <= 1 && (rows.length === 0 || rows[0].length <= 1);
>   if (isNotice) return { message: text.slice(0, 120) };
>
>   return { rows: rows };
> }
>
> function readState_(response) {
>   const headers = response.getAllHeaders();
>   for (var key in headers) {
>     if (key.toLowerCase() === STATE_HEADER) return String(headers[key]);
>   }
>   return '';
> }
>
> /**
>  * Clears and rewrites only the area this script wrote last time, not the whole sheet.
>  * If the new area has content this script did not write, it stops without overwriting.
>  */
> function write_(target, rows) {
>   const width = rows.reduce(function (max, row) {
>     return Math.max(max, row.length);
>   }, 0);
>
>   // setValues requires all rows to have the same length.
>   const padded = rows.map(function (row) {
>     const copy = row.slice();
>     while (copy.length < width) copy.push('');
>     return copy;
>   });
>
>   const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
>   const sheet = spreadsheet.getSheetByName(target.sheet) || spreadsheet.insertSheet(target.sheet);
>   const start = sheet.getRange(target.cell || 'A1');
>   const top = start.getRow();
>   const left = start.getColumn();
>
>   const properties = PropertiesService.getDocumentProperties();
>   const sizeKey = SIZE_KEY_PREFIX + labelOf_(target);
>   const previous = (properties.getProperty(sizeKey) || '0,0').split(',').map(Number);
>
>   const blocker = findBlocker_(sheet, top, left, padded.length, width, previous[0], previous[1]);
>   if (blocker) throw new Error(blocker + ' has other content; not overwritten');
>
>   if (previous[0] > 0) sheet.getRange(top, left, previous[0], previous[1]).clearContent();
>   sheet.getRange(top, left, padded.length, width).setValues(padded);
>   properties.setProperty(sizeKey, padded.length + ',' + width);
>
>   const stamp = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm');
>   start.setNote('Last fetched: ' + stamp);
> }
>
> /** Returns the address of the first non-empty cell in the new area outside the area written last time. Empty string if none. */
> function findBlocker_(sheet, top, left, height, width, previousHeight, previousWidth) {
>   const values = sheet.getRange(top, left, height, width).getValues();
>   for (var r = 0; r < height; r++) {
>     for (var c = 0; c < width; c++) {
>       const wasOurs = r < previousHeight && c < previousWidth;
>       if (!wasOurs && values[r][c] !== '') return columnLetter_(left + c) + (top + r);
>     }
>   }
>   return '';
> }
>
> function columnLetter_(column) {
>   var letters = '';
>   for (var n = column; n > 0; n = Math.floor((n - 1) / 26)) {
>     letters = String.fromCharCode(65 + ((n - 1) % 26)) + letters;
>   }
>   return letters;
> }
>
> function autoDaily() {
>   clearTriggers_();
>   ScriptApp.newTrigger('refreshNow').timeBased().everyDays(1).atHour(DAILY_HOUR).create();
>   SpreadsheetApp.getActiveSpreadsheet().toast('Refreshes automatically every day around ' + DAILY_HOUR + ':00.', MENU.title, 5);
> }
>
> function autoOff() {
>   clearTriggers_();
>   SpreadsheetApp.getActiveSpreadsheet().toast('Daily refresh turned off.', MENU.title, 5);
> }
>
> /** Also cleans up triggers left by older versions. */
> function clearTriggers_() {
>   ScriptApp.getProjectTriggers().forEach(function (trigger) {
>     ScriptApp.deleteTrigger(trigger);
>   });
> }
> ```

## Limitations

Review the following limits before you rely on Google Apps Script to import Sharelink data.

> [!CAUTION]
>
> **Attention**
>
> The sample script doesn't overwrite content it didn't write. If the target cells contain other content, such as an `IMPORTDATA` function or values you entered, the script fails and shows the address of that cell, for example `B5 has other content; not overwritten`. Clear those cells or change `cell` to an empty cell, and then run the script again.

* The daily refresh runs under the Google account of the person who selected **\[Sharelink]>\[Turn on daily refresh at N:00]**. If that person loses edit access to the spreadsheet, the daily refresh stops.
* Copying the spreadsheet doesn't copy the daily refresh setting. Turn on the daily refresh again in the copy.
* If a daily refresh fails, no failure message appears. Check that the date in the `Last fetched:` note on the cell where the data starts is today.
* A Sharelink with `useCache=false` waits until the new query finishes, so a run can take a long time. If several Sharelinks have `useCache=false`, the script waits for them one by one.
* If the query for a Sharelink with `useCache=false` doesn't finish within about 1 minute and no report data was queried within the last 3 hours, the failure message shows `Server is still calculating`.

## Troubleshoot Google Apps Script issues

The following table lists issues that can occur when you import Sharelink data with Google Apps Script and how to resolve them.

| Message or symptom | Cause | Solution |
| - | - | - |
| **\[Sharelink]** doesn't appear | The script isn't saved, or the spreadsheet hasn't been reloaded. | Save the script, reload the spreadsheet, and wait a few seconds. |
| `Link not found` | The Sharelink URL format is invalid, or the user who created the Sharelink is inactive. | Copy the Sharelink URL again from the Airbridge dashboard and enter it in `url`. |
| `This Sharelink doesn't exist.` | The Sharelink was deleted, or it was entered incorrectly in `url`. A Sharelink is automatically deleted 90 days after it's paused, and it's also deleted when its saved report is deleted. | If you entered the Sharelink incorrectly, copy it again from the Airbridge dashboard and enter it in `url`. If the Sharelink was deleted, create a new one from the saved report and enter it in `url`. If the saved report was also deleted, create and save the report again, and then create a Sharelink. |
| `This Sharelink is paused.` | The Sharelink was paused. Unless you remove its expiration, a Sharelink is automatically paused 90 days after it becomes active. | In the Airbridge dashboard, go to **\[Management]>\[Sharelink Management]**, resume the Sharelink, and then run the script again. The Sharelink URL doesn't change, so keep `url` as it is. If you use the Sharelink for the daily refresh, also select **Remove expiration**. |
| `http 400` | A parameter value added to the Sharelink URL is invalid, such as a wrong date format, a start date later than the end date, or a date range longer than allowed. | Correct the parameter values in `url`, and then run the script again. For parameter formats, see the Sharelink guide for each report in [Sharing Airbridge Reports](https://help.airbridge.io/en/guides/share-airbridge-reports.md). If you didn't add any parameters, copy the Sharelink URL again from the Airbridge dashboard and enter it in `url`. |
| `Server is still calculating` | The report query is taking a long time, or the Sharelink request limit has been exceeded. | Run the script again after a while. If the message appears often for a Sharelink without `useCache=false`, increase `WARMUP_WAIT_MS` to `40000`. If it still appears after that, the request limit may have been exceeded. The limit is lifted after a while. |
| `This Sharelink uses an older version.` | The Sharelink uses an older URL with the `/v2/`, `/funnel/`, `/retention/`, or `/revenue/` path, and the report query is taking a long time or the Sharelink request limit has been exceeded. | In `url`, change `/v2/`, `/funnel/`, `/retention/`, or `/revenue/` to `/v3/`, and then run the script again. |
| `Data has not been imported yet.` | The report query failed. | Run the script again after a while. |
| `No response` | Google Apps Script didn't receive a response from the Airbridge server. | Run the script again after a while. |
| `… has other content; not overwritten` | The cells where the data goes contain content the script didn't write, such as an `IMPORTDATA` function, values you entered, or data in a sheet you copied. | Clear the area or change `cell` to an empty cell, and then run the script again. |
| The data is the same as in the previous run | If the same report was queried in the Airbridge dashboard or through a Sharelink within the last 3 hours, the Sharelink returns the report data from that query instead of starting a new one. | This is expected. To start a new query on every run, add `useCache=false` to the end of the `/v3/` Sharelink URL. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.