How do I track packages in Google Sheets automatically?
Put tracking numbers in column A of a sheet named Tracking, paste the Apps Script below into Extensions → Apps Script, save your key as a script property, and run installHourlyTrigger once. Every hour the script sends the rows that aren't delivered yet to GET /v1/track, 35 per request, and writes carrier, status, newest scan time, newest scan and estimated delivery into columns B to F. On Pro a number counts once a month however often it's refreshed. Pro is $4.99/mo for 1,000 unique numbers, then $0.015 each, with no daily limit. A free key tracks 5 numbers a day. With no key you get 1 lookup a day, then pay per request over MPP: $0.50 by card for a 25-number pack, or $0.02 a number in USDC on Tempo.
Try it
// Extensions → Apps Script. Project Settings → Script properties: LASTSCANNED_KEY = lsk_live_...
// Sheet "Tracking": A tracking number | B carrier | C status | D newest scan (UTC) | E newest scan | F estimated delivery
function onOpen() {
SpreadsheetApp.getUi().createMenu("Tracking").addItem("Refresh now", "refreshTracking").addToUi();
}
function refreshTracking() {
const key = PropertiesService.getScriptProperties().getProperty("LASTSCANNED_KEY");
const sheet = SpreadsheetApp.getActive().getSheetByName("Tracking");
const count = sheet.getLastRow() - 1;
if (count < 1) return;
const rows = sheet.getRange(2, 1, count, 3).getValues()
.map((r, i) => ({ number: String(r[0]).trim().toUpperCase(), status: r[2], row: i + 2 }))
.filter((r) => r.number && r.status !== "delivered" && r.status !== "returned");
for (let i = 0; i < rows.length; i += 35) {
const batch = rows.slice(i, i + 35);
const url = "https://api.lastscanned.com/v1/track?numbers=" + encodeURIComponent(batch.map((r) => r.number).join(","));
const res = UrlFetchApp.fetch(url, { headers: { Authorization: "Bearer " + key }, muteHttpExceptions: true });
if (res.getResponseCode() !== 200) throw new Error(res.getContentText());
JSON.parse(res.getContentText()).results.forEach(({ result, error }, j) => {
const last = result && result.events[0]; // newest first
sheet.getRange(batch[j].row, 2, 1, 5).setValues([[
result ? result.carrier : "",
result ? result.status : error.code,
last ? last.timestamp : "",
last ? last.description : "",
(result && result.estimatedDelivery) || "",
]]);
});
}
}
function installHourlyTrigger() {
ScriptApp.newTrigger("refreshTracking").timeBased().everyHours(1).create();
}// Optional: watched numbers are re-checked for you and listed at app.lastscanned.com/watches.
function watchRows() {
const key = PropertiesService.getScriptProperties().getProperty("LASTSCANNED_KEY");
const sheet = SpreadsheetApp.getActive().getSheetByName("Tracking");
const numbers = sheet.getRange(2, 1, Math.max(sheet.getLastRow() - 1, 1), 1).getValues()
.map((r) => String(r[0]).trim()).filter(Boolean);
for (let i = 0; i < numbers.length; i += 100) {
UrlFetchApp.fetch("https://api.lastscanned.com/v1/watches", {
method: "post",
contentType: "application/json",
headers: { Authorization: "Bearer " + key },
payload: JSON.stringify({ trackingNumbers: numbers.slice(i, i + 100), metadata: { sheet: "Tracking" } }),
});
}
}Ways to keep a Google Sheet of tracking numbers up to date
| Option | How it works | Price | Notes |
|---|---|---|---|
| Check by hand | Copy each number into the carrier's site and type the status back. | Free | Fine for ten rows, not for a few hundred. |
| A formula per cell | A custom function that fetches one number per cell. | Every recalculation is a lookup | Sheets recalculates when it decides to, so one cell can be fetched many times. |
| Apps Script + hourly trigger | One script refreshes the open rows 35 per request and skips delivered ones. | On Pro re-checks are free | Works for any of 549 carriers, mixed in one column. |
Details
Results line up with your rows
Each request returns one entry per number, in the order sent, with either result or error, so the script writes each answer back to the row it came from. A number the carrier doesn't show yet gets not_found in the status column and is tried again next hour.
Delivered rows drop out
Rows marked delivered or returned are skipped, so the hourly run only sends what is still moving.
Free key or Pro
A free key tracks 5 numbers a day and every lookup counts, so an hourly sheet needs Pro: $4.99/mo for 1,000 unique numbers, with every refresh after the first free that month.
Why not a webhook straight into Sheets
An Apps Script web app can't read request headers, so it can't check the Lastscanned-Signature on a webhook. Receive webhooks on a small server and write to the sheet from there, or keep the hourly trigger.
FAQ
- Can Google Sheets track packages automatically?
- Yes, with an Apps Script and a time-driven trigger. The script above refreshes every open row once an hour.
- Can one sheet mix UPS, FedEx and USPS numbers?
- Yes. The carrier is detected from each number. A request takes up to 100 numbers with no more than 35 for one carrier, so the script sends 35 at a time.
- Where do I put the API key?
- In Project Settings → Script properties, not in a cell, so people who can view the sheet can't read it.
- Is there a version for Excel or a CSV file?
- Yes. The CSV and spreadsheet guide has a script that reads a file and writes a results CSV.