Is there a tracking API that takes a CSV or spreadsheet of tracking numbers?

There is no file upload; you don't need one. Copy the tracking column out of your spreadsheet and paste it into the dashboard playground (one per line or comma-separated, up to 100 at a time), or have a short script read the CSV and send it to GET /v1/track in requests of 35, writing carrier, status, last scan and location back as new columns. Carriers are detected from each number across 543 carriers. 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

Python: shipments.csv in, tracked.csv out
import csv, os, requests

KEY = os.environ["LASTSCANNED_KEY"]
with open("shipments.csv", newline="") as f:
    rows = list(csv.DictReader(f))  # needs a tracking_number column

numbers = list(dict.fromkeys(r["tracking_number"].strip().upper() for r in rows if r["tracking_number"].strip()))
found = {}
for i in range(0, len(numbers), 35):  # 35 per request never passes the per-carrier cap
    res = requests.get("https://api.lastscanned.com/v1/track",
                       params={"numbers": ",".join(numbers[i:i + 35])},
                       headers={"Authorization": f"Bearer {KEY}"}, timeout=120)
    res.raise_for_status()
    for entry in res.json()["results"]:
        found[entry["trackingNumber"]] = entry

with open("tracked.csv", "w", newline="") as f:
    out = csv.DictWriter(f, fieldnames=[*rows[0].keys(), "carrier", "status", "last_scan_utc", "last_scan", "location", "error"])
    out.writeheader()
    for r in rows:
        e = found.get(r["tracking_number"].strip().upper(), {})
        res_ = e.get("result") or {}
        last = (res_.get("events") or [{}])[0]  # newest first
        out.writerow({**r, "carrier": res_.get("carrier"), "status": res_.get("status"),
                      "last_scan_utc": last.get("timestamp"), "last_scan": last.get("description"),
                      "location": last.get("location"), "error": (e.get("error") or {}).get("code")})
Google Sheets Apps Script: column A in, status columns out
// Extensions > Apps Script. Set LASTSCANNED_KEY under Project Settings > Script Properties.
function trackColumnA() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const key = PropertiesService.getScriptProperties().getProperty("LASTSCANNED_KEY");
  const last = sheet.getLastRow();
  if (last < 2) return;
  const numbers = sheet.getRange(2, 1, last - 1, 1).getValues().map(([n]) => String(n).trim().toUpperCase());
  const found = {};
  const unique = [...new Set(numbers.filter(Boolean))];
  for (let i = 0; i < unique.length; i += 35) {
    const res = UrlFetchApp.fetch("https://api.lastscanned.com/v1/track?numbers=" + encodeURIComponent(unique.slice(i, i + 35).join(",")),
      { headers: { Authorization: "Bearer " + key } });
    for (const e of JSON.parse(res.getContentText()).results) found[e.trackingNumber] = e;
  }
  const out = numbers.map((n) => {
    const e = found[n] || {};
    const r = e.result || {};
    const scan = (r.events || [])[0] || {};
    return [r.carrier || "", r.status || (e.error ? e.error.code : ""), scan.timestamp || "", scan.description || ""];
  });
  sheet.getRange(2, 2, out.length, 4).setValues(out);
}

Ways to track a spreadsheet of numbers

OptionHow it worksPriceNotes
Dashboard playgroundPaste a column, one number per line or comma-separated.Free: 5 numbers a day. Pro: $4.99/mo for 1,000 unique numbers, then $0.015 each, no daily limit.Up to 100 numbers at a time; pick a carrier if they are all plain-digit numbers.
Python or Node scriptRead the CSV, send 35 numbers per GET /v1/track, write results to a new CSV.Free: 5 numbers a day. Pro: $4.99/mo for 1,000 unique numbers, then $0.015 each, no daily limit.Any length of list. Code below.
Google SheetsAn Apps Script menu reads column A and writes status into the next columns.Free: 5 numbers a day. Pro: $4.99/mo for 1,000 unique numbers, then $0.015 each, no daily limit.Keep the key in Script Properties, not in a cell.
WatchesPOST /v1/watches with up to 100 numbers and metadata such as the row or order id.Counts once per number; re-checks are freeWebhook on every change, so the sheet doesn't need re-running.

Details

Why 35 at a time

A request takes up to 100 numbers but no more than 35 for one carrier, and a spreadsheet is usually mostly one carrier. Sending 35 at a time is always valid; send up to 100 when you know the mix.

Plain-digit numbers

Most numbers detect their carrier from the format. Plain-digit numbers such as Aramex's need the carrier named: add carrier=aramex to the request, which applies to every number in it.

Duplicates and bad rows

Duplicates are removed before lookup and each number gets its own entry with result or error, so one bad row never fails the rest.

Sample audit

The bulk tracking auditor shows a sample SLA audit of a mixed batch and the script that writes it to CSV; it runs on sample rows, so use the code here for your own list.

FAQ

Can I upload a CSV file?
Not as a file. Paste the column into the dashboard (up to 100 at a time) or use the script above for any length of list.
How many tracking numbers per API request?
Up to 100, at most 35 for any one carrier.
Does re-running the sheet cost more?
On Pro, no: a number counts once a month however often you re-check it. On a free key every lookup counts, up to 5 a day.
Can the sheet update itself?
Run the Apps Script on a time-driven trigger, or watch the numbers with POST /v1/watches and write webhook events into the sheet.