Google Sheets indexing workflow
Published
A Google Sheets indexing workflow keeps URLs in one column and uses Apps Script with UrlFetchApp to send them to an index-check API. A second function, run on a time trigger, fetches the finished report and writes indexed, not indexed or check failed beside each URL. The API key stays in Script Properties.
When a spreadsheet is the right tool
Many link builders already track placements in Google Sheets: one row per live URL, with columns for the client, the vendor and the date the link went up. Adding an index status column to that sheet avoids copying lists between tools. Apps Script, the JavaScript runtime built into Google Workspace, can call any HTTPS API, so the sheet can request checks and record results without an add-on.
This recipe uses the public API of IndexChex, the publisher of this handbook, described on the IndexChex software and API page. The same structure works for any index checker with a create-job and fetch-report pair of endpoints; the general pattern is covered under backlink indexer APIs.
Sheet layout
Create a tab named URLs. Row 1 holds headers. Column A holds one URL per row from row 2 down; column B is where the script writes the result. Other columns are left alone, so client names, prices and notes can sit to the right.
Storing the key
In the Apps Script editor (Extensions, then Apps Script), open Project Settings and add a script property named INDEXCHEX_API_KEY with the key from your account's API settings. The key is sent as the full value of the Authorization header, with no prefix. Script Properties keep the key out of the grid, out of version history and out of copies made by collaborators. The API key security page covers rotation and scoping.
The script
const BASE = 'https://indexchex.com/api/v1';
const SHEET = 'URLs';
function api_(method, path, body) {
const key = PropertiesService.getScriptProperties().getProperty('INDEXCHEX_API_KEY');
const options = {
method: method,
headers: { Authorization: key, Accept: 'application/json' },
muteHttpExceptions: true,
};
if (body) {
options.contentType = 'application/json';
options.payload = JSON.stringify(body);
}
const res = UrlFetchApp.fetch(BASE + path, options);
return { code: res.getResponseCode(), data: JSON.parse(res.getContentText()) };
}
function urlRange_() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET);
const rows = sheet.getLastRow() - 1;
if (rows < 1) throw new Error('No URLs below the header row.');
return sheet.getRange(2, 1, rows, 2);
}
function startCheck() {
const urls = urlRange_().getValues()
.map(row => String(row[0]).trim())
.filter(url => url !== '');
const res = api_('post', '/index-check/jobs', { urls: urls, name: 'Sheet: ' + SHEET });
if (res.code !== 202) throw new Error(res.data.message);
PropertiesService.getDocumentProperties()
.setProperty('CHECK_JOB_ID', String(res.data.job_id));
}
function collectReport() {
const props = PropertiesService.getDocumentProperties();
const jobId = props.getProperty('CHECK_JOB_ID');
if (!jobId) return;
const res = api_('get', '/index-check/jobs/' + jobId + '/report');
if (res.code === 422) return; // not finished yet
if (res.code !== 200) throw new Error(res.data.message);
const result = {};
res.data.indexed_links.forEach(url => { result[url] = 'Indexed'; });
res.data.unindexed_links.forEach(url => { result[url] = 'Not indexed'; });
res.data.failed_links.forEach(url => { result[url] = 'Check failed'; });
const range = urlRange_();
range.setValues(range.getValues().map(row => {
const url = String(row[0]).trim();
return [row[0], result[url] || row[1]];
}));
props.deleteProperty('CHECK_JOB_ID');
}
startCheck reads column A, drops blanks and creates one check job. A successful create returns HTTP 202 with a job_id, which the script keeps in Document Properties so the second function knows what to fetch. collectReport asks for the report; until the job is complete the API answers 422, and the function simply exits. Once the report arrives it maps each URL to one of three labels and writes them into column B in the original row order.
Running it
- Run
startCheckonce from the editor and approve the authorisation prompt (the script needs spreadsheet and external request scopes). - In Triggers, add a time-driven trigger for
collectReport, every 10 or 15 minutes. - When column B fills, the stored job ID is cleared and later trigger runs do nothing until
startCheckis run again.
A time trigger is better than a loop with Utilities.sleep, because Apps Script stops long-running executions. Polling every few minutes is gentle on the per-minute request limit; the polling guide explains how to choose an interval.
Reading the three labels
| Label | Meaning | Next step |
|---|---|---|
| Indexed | Google currently shows the URL in search results | Nothing; re-check later if the link matters |
| Not indexed | The check completed and the URL was not found | Inspect the page, then consider a submission |
| Check failed | The lookup could not complete | Re-check; it is not evidence that the URL is unindexed |
Not indexed URLs can be sent for crawling from the same sheet by calling the index-submit jobs endpoint with the URLs from those rows, or from the IndexChex dashboard, where check results can be moved into a new submission in one click. Indexing itself remains Google's decision; a submission gets the page crawled, not ranked or kept.
Variations
- Several tabs. Store one job ID per tab name instead of a single property.
- Submit with a re-check. Use the submission endpoint with
checkerset to a number of days (1 to 5), then read the re-check report later. The Python script shows that flow end to end. - Client hand-off. Copy the filled columns into a summary tab; client reporting suggests table layouts.
- No code. The same calls can run from an automation platform; see Zapier and Make workflows.
Field names and response codes are listed in the API reference. For a broader view of where scripts like this sit among indexing tools, see what backlink indexer software is.
FAQ
Why not put the API key in a hidden cell?
Anyone with view access to the spreadsheet can read every cell, hidden or not, and copies of the file carry the cells with them. Script Properties are attached to the script project and are not shown in the grid.
Can the sheet submit URLs for crawling instead of checking them?
Yes. Change the path to the index-submit jobs endpoint and the body to the submission fields. Submissions cost 1 credit per URL in standard mode, so keep the trigger manual rather than timed to avoid repeat submissions.
What happens if the report is requested before the job finishes?
The report endpoint answers HTTP 422 with a message saying the report is not ready. The script treats that as a signal to try again on the next trigger run.
How many URLs can one sheet send?
One check job accepts up to 10,000 URLs. Larger lists should be split into several jobs, each with its own stored job ID.
Terms used on this page
Sources
Cite this entry
IndexChex. (2026, October 8). Google Sheets indexing workflow. backlinkindexersoftware.com. https://backlinkindexersoftware.com/google-sheets-indexing-workflow/