# UPS Bulk Tracker - Status, ETA & Delay Alerts (`neverempty/ups-bulk-tracker`) Actor

Track up to 500 UPS tracking numbers per run: status, estimated delivery date and window, full scan history, delivered and left-at details, and delay or address-problem flags. Watch mode returns only packages that changed. No UPS account or API key. Unofficial.

- **URL**: https://apify.com/neverempty/ups-bulk-tracker.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $22.50 / 1,000 tracking results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## UPS Bulk Tracker - Status, ETA & Delay Alerts

Track up to 500 **UPS tracking numbers** in one run and get one clean row per package: the current status, the estimated delivery date and time window, the last scan and where it happened, the delivered date with where it was left and who signed, and the full scan history. Every row also says plainly whether the package **needs attention**: delayed, an address problem, a missed delivery attempt, returning to sender, weather, customs or an action UPS asks for.

Turn on **watch mode** and schedule the Actor: each run returns the full row only for packages that changed since the last run (a new scan, a new delivery date, a new status such as out for delivery or delivered, or a new exception), with the changes listed. Unchanged packages come back as a short status row at a fraction of the price. No UPS account, login or API key is needed.

### What you can do with it

- **Check every open UPS shipment at once** instead of pasting numbers into ups.com one by one, and send the rows to a sheet, your store, a helpdesk or Slack.
- **Catch problems early**: filter on `hasException` to see every package that is delayed, has an address problem, had a failed delivery attempt or is going back to the sender.
- **Tell customers when their order will arrive** with the UPS delivery date and time window, and when it was delivered and where it was left.
- **Get told only when something changes**: watch mode returns a package when it moves, its delivery date slips or it is delivered.

### What you get for each package

| Field | Example |
|---|---|
| `trackingNumber` | `1Z0V1W590338116384` |
| `statusText` / `statusCategory` | `Out for Delivery` / `out-for-delivery` (also `label-created`, `picked-up`, `in-transit`, `delivered`, `exception`, `returned-to-sender`) |
| `estimatedDelivery` / `estimatedDeliveryWindow` | `2026-10-23` / `11:45 A.M. - 1:45 P.M.` |
| `delivered` / `deliveredAt` / `deliveredAtUtc` | `true` / `2026-10-01T19:13` (local time) / `2026-10-02T02:13:01Z` |
| `leftAt` / `receivedBy` / `proofOfDeliveryUrl` | `Front Door` / the name UPS shows / the UPS proof of delivery page |
| `hasException` / `exceptionReasons` | `true` / `["UPS marks this package as delayed", "address problem"]` |
| `flags` | `exception`, `delayed`, `addressProblem`, `deliveryAttempted`, `returningToSender`, `weatherOrEmergency`, `customs`, `actionRequired` (each `true` or `false`) |
| `lastEvent` / `lastLocation` | the latest scan with its time and place / `Columbia, MO, United States` |
| `service` / `shippedDate` / `weight` | `UPS Ground` / `2026-10-21` / the weight when UPS shows it |
| `shipTo` | `{ "city": "COLUMBIA", "state": "MO", "country": "US" }` |
| `packagesInShipment` | for a multi-piece shipment, how many packages it has (UPS shows "1 of 20 Piece Shipment") |
| `eventCount` / `events` | every scan with `at` (local time), `atUtc`, `utcOffset`, `location`, `description`, `milestone`, `code` and `isException`, newest first |
| `changes` | watch mode: `newEvents`, `newEventCount`, `etaBefore`, `etaNow`, `statusBefore`, `statusNow`, `newFlags` |
| `trackingUrl`, `scrapedAt`, `note` | the UPS tracking page for that number, when it was checked, and a note when there is something to say |

Values UPS does not show are `null`, never guessed.

### How to use it

**Paste your numbers.** Put UPS tracking numbers into **UPS tracking numbers**, one per line. You can also paste a list separated by commas, or a ups.com tracking link, including a link that holds several numbers. Spaces and dashes are ignored, and a number listed twice is looked up and charged once.

**Run it.** Each package comes back as a row. Download the results as CSV, Excel or JSON, or connect them to Google Sheets, Zapier, Make or a webhook with an Apify integration.

**Watch mode.** Switch on **Watch mode** and add a schedule (hourly or daily). Run 1 remembers each package and returns one short `watch-started` row per number. From run 2 on, a package with a new scan, a new delivery date, a new status or a new exception flag is returned in full with its `changes`. A package with no change returns a short `no-new-since-last-run` row with its status, delivery date and `hasException`. Packages that did not fit under your spending limit are not remembered, so they come back on the next run. Watch memory is kept per saved task, so two tasks that share numbers each get their own changes; runs started without a task share one memory. **Reset watch memory** starts over for the numbers in that run.

### Input

| Field | What it does |
|---|---|
| `trackingNumbers` | UPS tracking numbers or ups.com tracking links, one per line (commas also work). Up to 500 per run. |
| `monitoringMode` | Watch mode: return only packages that changed since the last run. |
| `resetMonitoringState` | Forget what was remembered for the numbers in this run. |
| `proxyConfiguration` | Leave on the default (Apify residential proxy, United States). |

Example input:

```json
{
  "trackingNumbers": ["1Z1J329C0217555166", "1Z3YY4081327545719"],
  "monitoringMode": false
}
```

### Pricing

Pay per result, with two events:

- **Tracking result** (`tracking-returned`): one per package returned with its data and full scan history. In watch mode, this is a package that changed.
- **Tracking check** (`tracking-checked`, watch mode only): one per watched package that UPS returned but that had nothing new, including the first watch run. It costs a small part of a tracking result. A package is never charged both events in the same run.

Rows that explain why there is no result (a number UPS does not know, a number with a typo, a number UPS would not answer for, numbers past the 500 limit) are free and say so in their `note` field.

### FAQ

**How do I track many UPS packages at once?**
Paste up to 500 UPS tracking numbers into `trackingNumbers`, one per line, and run the Actor. Each package comes back as its own row with the status, the delivery date and time window, the last scan and the full history, ready to download as CSV or Excel.

**How do I find UPS packages that are delayed or have a problem?**
Filter the results on `hasException`. `exceptionReasons` says why in plain words (delayed, address problem, delivery attempted, returning to sender, weather, customs, action needed), and `flags` gives each one as `true` or `false`.

**How do I get an alert when a UPS package is delivered or its delivery date changes?**
Turn on `monitoringMode` and schedule the Actor hourly or daily. Packages that changed are returned in full with `changes.etaBefore`, `changes.etaNow`, `changes.statusBefore`, `changes.statusNow` and the new scans, so you can send each `ok` row to email, Slack or a webhook.

**Do I need a UPS account or a UPS API key?**
No. Paste the numbers and run it.

**Why did a number come back as not found?**
UPS has no record of it yet (a new label can take a few hours to appear), it has a typo, or UPS removed it from tracking some months after delivery. The `note` says which, and the row is free. If the number looks like a FedEx, USPS or DHL number, the note says so.

### Good to know

- UPS only. FedEx, USPS and other carriers' numbers come back as a free row; when the number has the shape of a FedEx, USPS or DHL number, the note says so.
- In watch mode, if the same task is started twice at the same moment, both runs can return the same change. Give each schedule its own task.
- Times in `at` and `deliveredAt` are the local times UPS prints for each scan; `atUtc` and `deliveredAtUtc` are the same moments in UTC.
- The delivery date and window are UPS's own estimate, as UPS shows it on its tracking page.
- When UPS refuses a request or times out, the number comes back as a free row saying it could not be read. Nothing is solved or bypassed; run it again later.
- This Actor is not affiliated with or endorsed by United Parcel Service of America, Inc. UPS is a trademark of its owner.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

## `trackingNumbers` (type: `array`):

UPS tracking numbers (usually starting with 1Z), one per line (commas also work). A ups.com tracking link also works, including one with several numbers. Up to 500 numbers per run; each package UPS returns is one result. Numbers UPS does not know, numbers with a typo and duplicates come back as free rows with the reason.

## `monitoringMode` (type: `boolean`):

Return a package only when something changed since the last run: a new scan, a new estimated delivery date, a new status (for example out for delivery or delivered) or a new exception flag, with the changes listed. The first run for a number remembers it and each run with no change returns a short status row; both are charged as one low-priced tracking check instead of a full result. Schedule it hourly or daily.

## `resetMonitoringState` (type: `boolean`):

Forget what was remembered for the numbers in this run. With watch mode on, this run starts a new watch for them; with it off, the memory is only cleared.

## `proxyConfiguration` (type: `object`):

UPS usually refuses data-center addresses. The default (Apify residential proxy, United States) is recommended.

## Actor input object example

```json
{
  "trackingNumbers": [
    "1Z1J329C0217555166",
    "1Z3YY4081327545719"
  ],
  "monitoringMode": false,
  "resetMonitoringState": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `results` (type: `string`):

One row per UPS tracking number: status and status category, estimated delivery date and time window, delivered date, left at and received by, last scan and location, service, weight, ship-to city, the full scan history and exception flags (delay, address problem, delivery attempt, return to sender, weather, customs, action needed). Watch-mode rows add the new scans and the ETA and status before and after. Numbers UPS does not know, invalid numbers and numbers that could not be read come back as their own free rows.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "trackingNumbers": [
        "1Z1J329C0217555166",
        "1Z3YY4081327545719"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/ups-bulk-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "trackingNumbers": [
        "1Z1J329C0217555166",
        "1Z3YY4081327545719",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/ups-bulk-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "trackingNumbers": [
    "1Z1J329C0217555166",
    "1Z3YY4081327545719"
  ]
}' |
apify call neverempty/ups-bulk-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/ups-bulk-tracker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Rz3J3y5p4ecNr6ir0/builds/uUrgYLCyjlwVQpEOw/openapi.json
