# UPS Tracking API & MCP

URL: https://docs.lastscanned.com/carriers/ups

Query live **UPS** (`ups`) package status and full scan history in a single synchronous `GET` request or MCP tool call. `lastscanned` validates UPS `1Z` Mod-10 check digits upfront and returns a normalized `TrackingResult` JSON payload—without requiring a UPS Developer Portal OAuth application, shipper account number, or webhook receiver.

## Overview & Quick Facts

| Property | Value |
|---|---|
| **Carrier slug (`?carrier=`)** | `ups` |
| **Brand & service aliases** | United Parcel Service, UPS Ground, UPS Next Day Air, UPS 2nd Day Air, UPS 3 Day Select, UPS SurePost, UPS Worldwide Express / Saver / Expedited |
| **Category** | National & international parcel carrier (Detection precedence **#1 of 31**) |
| **Single-package REST endpoint** | `GET https://api.lastscanned.com/v1/track/{trackingNumber}` |
| **Batch REST endpoint** | `GET https://api.lastscanned.com/v1/track?numbers=1Z...,1Z...` (up to 35 numbers per carrier in a 100-number call) |
| **MCP server (`https://mcp.lastscanned.com`)** | `track_package`, `track_packages`, `list_carriers` (`claude mcp add --transport http lastscanned https://mcp.lastscanned.com`) |
| **Authentication & pricing** | `Authorization: Bearer lsk_live_...` · **1 free keyless lookup/day per IP** · **Free tier** (5 unique numbers/day, $0) · **Pro** ($4.99/mo for 1,000 unique numbers, unlimited re-polls, $0.015/extra) · **HTTP 402 Stripe MPP** ($0.50 for 25-number `lsk_pack_` key or $0.02/number in USDC on Tempo) |

## Why developers use `lastscanned` for UPS instead of direct requests

- **Avoid Akamai Bot Manager `403` blocks from serverless & edge IPs**: If you try to call `webapis.ups.com/track/api/Track/GetStatus` directly from a Cloudflare Worker, Vercel function, or AWS Lambda, Akamai rejects datacenter TLS fingerprints and IPs, and requires paired `GetCSRFToken` / `X-XSRF-TOKEN-ST` session cookies. Calling `GET https://api.lastscanned.com/v1/track/:number` from your Worker or Lambda returns clean JSON in one `fetch()`.
- **Skip UPS Developer Kit OAuth 2.0 & shipper account linking**: Official UPS Track API credentials require registering an enterprise app, linking a 6-character UPS shipper account, and refreshing short-lived OAuth tokens. `lastscanned` works immediately with a single Bearer key (or keyless via HTTP 402).
- **Synchronous GET—no webhook registration**: Unlike AfterShip, Ship24, or EasyPost where you must `POST` a tracker first and wait for an async webhook, `GET /v1/track/:number` returns the live UPS scan timeline inline.

## Tracking Number Formats & Auto-Detection (`matches` vs `mayMatch`)

UPS is evaluated **first (#1)** in `lastscanned`'s two-pass detector:

| Pass | Rule / Regex | Check Digit | Examples |
|---|---|---|---|
| **Pass 1 (`matches`)** | `/^1Z[0-9A-Z]{16}$/i` (18 characters starting with `1Z`) | **Yes — `ups1Z(n)` (Mod-10)** | `1Z999AA10123456784`, `1Z5868Y60389226681` |
| **Pass 2 (`mayMatch`)** | *None* | — | — |

### `ups1Z` Mod-10 check-digit algorithm

Every 18-character `1Z` number consists of `1Z` + 6-character shipper account + 2-digit service code + 7-digit package identifier + 1 Mod-10 check digit. `lastscanned` validates the check digit before making any network call so typos fail fast with `400 unknown_carrier`:

```ts
/** UPS 1Z: letters count as (char code - 63) mod 10, odd positions doubled, mod 10. */
export function ups1Z(n: string): boolean {
  const body = n.slice(2, -1).toUpperCase();
  let sum = 0;
  for (let i = 0; i < body.length; i++) {
    const ch = body[i];
    const v = ch >= "0" && ch <= "9" ? Number(ch) : (ch.charCodeAt(0) - 63) % 10;
    sum += i % 2 ? v * 2 : v;
  }
  return (10 - (sum % 10)) % 10 === Number(n.slice(-1));
}
```

### When is `?carrier=ups` optional vs required?

- **Optional (auto-detected)**: All standard 18-character `1Z` tracking numbers with a valid Mod-10 check digit. No other carrier uses the `1Z` prefix.
- **Required (`?carrier=ups`)**: Non-`1Z` UPS formats such as 9-digit or 12-digit UPS Freight / TForce PRO numbers, UPS InfoNotice numbers, or UPS Mail Innovations labels that do not use a USPS `92…`/`93…` IMpb barcode.

## Synchronous `GET` Request & Normalized JSON Response

### `curl`

```bash
curl -s "https://api.lastscanned.com/v1/track/1Z999AA10123456784" \
  -H "Authorization: Bearer $LASTSCANNED_API_KEY"
```

### JavaScript / TypeScript (`fetch`)

```ts
const res = await fetch("https://api.lastscanned.com/v1/track/1Z999AA10123456784", {
  headers: { Authorization: `Bearer ${process.env.LASTSCANNED_API_KEY}` },
});
const pkg = await res.json();
console.log(pkg.status, pkg.service, pkg.deliveredAt);
```

### MCP `track_package` tool call

```json
{
  "tool": "track_package",
  "arguments": {
    "tracking_number": "1Z999AA10123456784"
  }
}
```

### Normalized `TrackingResult` JSON response

```json
{
  "carrier": "ups",
  "trackingNumber": "1Z999AA10123456784",
  "status": "delivered",
  "statusDescription": "Delivered",
  "service": "UPS Ground",
  "deliveredAt": "2026-10-23T20:17:14Z",
  "deliveredAtLocal": "2026-10-23T15:17:00",
  "signedBy": "FRONT DOOR",
  "destination": {
    "city": "AUSTIN",
    "state": "TX",
    "country": "US"
  },
  "events": [
    {
      "timestamp": "2026-10-23T20:17:14Z",
      "localTime": "2026-10-23T15:17:00",
      "status": "delivered",
      "description": "Delivered",
      "location": "AUSTIN, TX, US",
      "code": "KB"
    },
    {
      "timestamp": "2026-10-23T13:42:08Z",
      "localTime": "2026-10-23T08:42:00",
      "status": "out_for_delivery",
      "description": "Out For Delivery Today",
      "location": "AUSTIN, TX, US",
      "code": "OT"
    },
    {
      "timestamp": "2026-10-23T09:15:30Z",
      "localTime": "2026-10-23T04:15:00",
      "status": "in_transit",
      "description": "Arrived at Facility",
      "location": "AUSTIN, TX, US",
      "code": "AR"
    },
    {
      "timestamp": "2026-10-21T22:04:11Z",
      "localTime": "2026-10-21T18:04:00",
      "status": "label_created",
      "description": "Shipper created a label, UPS has not received the package yet.",
      "location": "US",
      "code": "MP"
    }
  ]
}
```

> **Raw status & scan codes**: Top-level `statusDescription` preserves UPS's exact status wording (e.g. `"Delivered"`, `"On the Way"`), while each entry in `events[]` includes normalized `status`, raw `description`, and UPS's 2-character activity `code` (e.g. `"KB"`, `"OT"`, `"AR"`, `"DP"`, `"MP"`).

## Carrier-Specific Status & Integration Notes

- **Exact UTC (`timestamp`) and scan-local (`localTime`) clocks**: UPS reports both `gmtDate`/`gmtTime` (down to the second in UTC, e.g. `"2026-10-23T20:17:14Z"`) and the scan location's wall-clock time (`date`/`time`, normalized to `"2026-10-23T15:17:00"`). Both `timestamp` and `localTime` are populated on every scan.
- **UPS status type mapping**: UPS package status types map deterministically to the 7 normalized statuses: `M` → `label_created`, `I` → `in_transit`, `O` → `out_for_delivery`, `D` → `delivered`, `X` → `exception`, and `RS` → `returned`.
- **Service level & proof of delivery**: `service` returns the specific UPS service product (such as `"UPS Ground"`, `"UPS Next Day Air"`, or `"UPS 2nd Day Air"`), and `signedBy` captures the receiver name or release location (`receivedBy`) when available.

## Related Resources

- **Live browser lookup tool**: [Track a UPS package live on lastscanned.com/lookup/ups](https://lastscanned.com/lookup/ups)
- **Carrier hub**: [All 31 supported carriers](/carriers)
- **Regex & check digits**: [Tracking number formats & auto-detection](/formats)
- **AI support workflows**: [WISMO AI agents & synchronous tracking](/wismo)
