# Tracking Guide

EasyPost provides two primary methods for tracking packages:

- Supplying an existing carrier tracking number to create a `Tracker` object.
- Purchasing a shipping label through EasyPost, which automatically generates a Tracker.

Tracking updates are delivered through `Event` webhooks. Refer to the Webhooks Guide for implementation details on receiving `Event` objects.

Related:

- [Webhooks Guide](https://docs.easypost.com/guides/getting-started)

---

## Prerequisites

- Sign up or log in to an existing EasyPost account.
- Configured webhook URLs for Test and Production mode.
- One of the official EasyPost client libraries installed.
- Familiarity with the Getting Started Guide (recommended).

---

## Method 1: Track an existing carrier shipment

To track a shipment not created through EasyPost, create a `Tracker` with a `tracking_code` and, optionally, a `carrier`.
If carrier is omitted, EasyPost attempts auto-detection. If the tracking code cannot be matched to a supported carrier, an error is returned.

#### Creating a Tracker

### Example: POST /trackers

#### cURL

```shell
curl -X POST https://api.easypost.com/v2/trackers \
  -u "EASYPOST_API_KEY": \
  -H 'Content-Type: application/json' \
  -d '{
    "tracker": {
      "tracking_code": "EZ1000000001",
      "carrier": "USPS"
    }
  }'
```

#### Go

```go
package example

import (
	"fmt"

	"github.com/EasyPost/easypost-go/v5"
)

func create() {
	client := easypost.New("EASYPOST_API_KEY")

	tracker, _ := client.CreateTracker(
		&easypost.CreateTrackerOptions{
			TrackingCode: "EZ1000000001",
			Carrier:      "USPS",
		},
	)

	fmt.Println(tracker)
}
```

#### Java

```java
package trackers;

import com.easypost.exception.EasyPostException;
import com.easypost.model.Tracker;
import com.easypost.service.EasyPostClient;

import java.util.HashMap;

public class Create {
    public static void main(String[] args) throws EasyPostException {
        EasyPostClient client = new EasyPostClient("EASYPOST_API_KEY");

        HashMap<String, Object> params = new HashMap<String, Object>();
        params.put("tracking_code", "EZ1000000001");
        params.put("carrier", "USPS");

        Tracker tracker = client.tracker.create(params);

        System.out.println(tracker);
    }
}
```

#### C#

```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using EasyPost;
using Newtonsoft.Json;

namespace EasyPostExamples
{
    public class Examples
    {
        public static async Task Main()
        {
            var client = new EasyPost.Client(new EasyPost.ClientConfiguration("EASYPOST_API_KEY"));

            EasyPost.Parameters.Tracker.Create parameters = new()
            {
                TrackingCode = "EZ1000000001",
                Carrier = "USPS"
            };

            EasyPost.Models.API.Tracker tracker = await client.Tracker.Create(parameters);

            Console.WriteLine(JsonConvert.SerializeObject(tracker, Formatting.Indented));
        }
    }
}
```

#### Node.js

```javascript
const EasyPostClient = require('@easypost/api');

const client = new EasyPostClient('EASYPOST_API_KEY');

(async () => {
  const tracker = await client.Tracker.create({
    tracking_code: 'EZ1000000001',
    carrier: 'USPS',
  });

  console.log(tracker);
})();
```

#### PHP

```php
<?php

$client = new \EasyPost\EasyPostClient('EASYPOST_API_KEY');

$tracker = $client->tracker->create([
    'tracking_code' => 'EZ1000000001',
    'carrier' => 'USPS'
]);

echo $tracker;
```

#### Python

```python
import easypost

client = easypost.EasyPostClient("EASYPOST_API_KEY")

tracker = client.tracker.create(
    tracking_code="EZ1000000001",
    carrier="USPS",
)

print(tracker)
```

#### Ruby

```ruby
require 'easypost'

client = EasyPost::Client.new(api_key: 'EASYPOST_API_KEY')

tracker = client.tracker.create(
  tracking_code: 'EZ1000000001',
  carrier: 'USPS',
)

puts tracker
```

After a `Tracker` is created, EasyPost periodically checks for new carrier updates and posts them via `tracker.updated` webhook `Events`.

---

## Method 2: Track a shipment created through EasyPost

Tracking is automatically enabled for all shipping labels purchased through EasyPost. No additional API calls are required.

When a label is purchased:

- A `Tracker` is automatically generated.
- The response includes the `tracking_code`.
- Tracking updates are delivered through webhook `Events`.

For full shipment creation steps, see the Getting Started Guide.

#### Buying Shipment

### Example: POST /shipments/:id/buy

#### cURL

```shell
curl -X POST https://api.easypost.com/v2/shipments \
  -u "EASYPOST_API_KEY": \
  -H 'Content-Type: application/json' \
  -d '{
    "shipment": {
      "to_address": {
        "name": "Dr. Steve Brule",
        "street1": "179 N Harbor Dr",
        "city": "Redondo Beach",
        "state": "CA",
        "zip": "90277",
        "country": "US",
        "phone": "8573875756",
        "email": "dr_steve_brule@gmail.com"
      },
      "from_address": {
        "name": "EasyPost",
        "street1": "417 Montgomery Street",
        "street2": "5th Floor",
        "city": "San Francisco",
        "state": "CA",
        "zip": "94104",
        "country": "US",
        "phone": "4153334445",
        "email": "support@easypost.com"
      },
      "parcel": {
        "length": "20.2",
        "width": "10.9",
        "height": "5",
        "weight": "65.9"
      },
      "customs_info": {
        "id": "cstinfo_..."
      }
    }
  }'
```

#### Go

```go
package example

import (
	"fmt"

	"github.com/EasyPost/easypost-go/v5"
)

func create() {
	client := easypost.New("EASYPOST_API_KEY")

	shipment, _ := client.CreateShipment(
		&easypost.Shipment{
			ToAddress: &easypost.Address{
				Name:    "Dr. Steve Brule",
				Street1: "179 N Harbor Dr",
				City:    "Redondo Beach",
				State:   "CA",
				Zip:     "90277",
				Country: "US",
				Phone:   "4155559999",
				Email:   "dr_steve_brule@gmail.com",
			},
			FromAddress: &easypost.Address{
				Name:    "EasyPost",
				Street1: "417 Montgomery Street",
				Street2: "5th Floor",
				City:    "San Francisco",
				State:   "CA",
				Zip:     "90277",
				Country: "US",
				Phone:   "4155559999",
				Email:   "support@easypost.com",
			},
			Parcel: &easypost.Parcel{
				Length: 20.2,
				Width:  10.9,
				Height: 5,
				Weight: 65.9,
			},
			CustomsInfo: &easypost.CustomsInfo{
				ID: "cstinfo_...",
			},
		},
	)

	fmt.Println(shipment)
}
```

#### Java

```java
package shipments;

import com.easypost.exception.EasyPostException;
import com.easypost.model.Shipment;
import com.easypost.service.EasyPostClient;

import java.util.HashMap;

public class Create {
    public static void main(String[] args) throws EasyPostException {
        EasyPostClient client = new EasyPostClient("EASYPOST_API_KEY");

        HashMap<String, Object> toAddressMap = new HashMap<String, Object>();
        toAddressMap.put("name", "Dr. Steve Brule");
        toAddressMap.put("street1", "179 N Harbor Dr");
        toAddressMap.put("city", "Redondo Beach");
        toAddressMap.put("state", "CA");
        toAddressMap.put("country", "US");
        toAddressMap.put("phone", "8573875756");
        toAddressMap.put("email", "dr_steve_brule@gmail.com");
        toAddressMap.put("zip", "90277");

        HashMap<String, Object> fromAddressMap = new HashMap<String, Object>();
        fromAddressMap.put("name", "EasyPost");
        fromAddressMap.put("street1", "417 Montgomery Street");
        fromAddressMap.put("street2", "5th Floor");
        fromAddressMap.put("city", "San Francisco");
        fromAddressMap.put("state", "CA");
        fromAddressMap.put("zip", "94104");
        fromAddressMap.put("country", "US");
        fromAddressMap.put("phone", "4153334445");
        fromAddressMap.put("email", "support@easypost.com");

        HashMap<String, Object> parcelMap = new HashMap<String, Object>();
        parcelMap.put("length", 20.2);
        parcelMap.put("width", 10.9);
        parcelMap.put("height", 5);
        parcelMap.put("weight", 65.9);

        HashMap<String, Object> customsInfoMap = new HashMap<String, Object>();
        customsInfoMap.put("id", "cstinfo_...");

        HashMap<String, Object> params = new HashMap<String, Object>();
        params.put("to_address", toAddressMap);
        params.put("from_address", fromAddressMap);
        params.put("parcel", parcelMap);
        params.put("customs_info", customsInfoMap);

        Shipment shipment = client.shipment.create(params);

        System.out.println(shipment);
    }
}
```

#### C#

```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using EasyPost;
using Newtonsoft.Json;

namespace EasyPostExamples
{
    public class Examples
    {
        public static async Task Main()
        {
            var client = new EasyPost.Client(new EasyPost.ClientConfiguration("EASYPOST_API_KEY"));

            // Create a shipment using all data in one API call

            EasyPost.Parameters.Shipment.Create parameters = new()
            {
                ToAddress = new EasyPost.Parameters.Address.Create
                {
                    Name = "Dr. Steve Brule",
                    Street1 = "417 Montgomery Street",
                    Street2 = "5th Floor",
                    City = "San Francisco",
                    State = "CA",
                    Country = "US",
                    Zip = "94104"
                },
                FromAddress = new EasyPost.Parameters.Address.Create
                {
                    Company = "EasyPost",
                    Street1 = "417 Montgomery Street",
                    Street2 = "Floor 5",
                    City = "San Francisco",
                    State = "CA",
                    Country = "US",
                    Zip = "94104"
                },
                Parcel = new EasyPost.Parameters.Parcel.Create
                {
                    Length = 8,
                    Width = 6,
                    Height = 5,
                    Weight = 10
                },
                CustomsInfo = new EasyPost.Parameters.CustomsInfo.Create
                {
                    // ...
                }
            };

            EasyPost.Models.API.Shipment shipment = await client.Shipment.Create(parameters);

            Console.WriteLine(JsonConvert.SerializeObject(shipment, Formatting.Indented));

            // Create a shipment using existing addresses, parcel, and customs info

            EasyPost.Models.API.Address toAddress = await client.Address.Retrieve("adr_...");
            EasyPost.Models.API.Address fromAddress = await client.Address.Retrieve("adr_...");
            EasyPost.Models.API.Parcel parcel = await client.Parcel.Retrieve("prcl_...");
            EasyPost.Models.API.CustomsInfo customsInfo = await client.CustomsInfo.Retrieve("cstinfo_...");

            parameters = new()
            {
                ToAddress = toAddress,
                FromAddress = fromAddress,
                Parcel = parcel,
                CustomsInfo = customsInfo
            };

            shipment = await client.Shipment.Create(parameters);

            Console.WriteLine(JsonConvert.SerializeObject(shipmentWithIds, Formatting.Indented));
        }
    }
}
```

#### Node.js

```javascript
const EasyPostClient = require('@easypost/api');

const client = new EasyPostClient('EASYPOST_API_KEY');

(async () => {
  let shipment;

  shipment = await client.Shipment.create({
    to_address: {
      name: 'Dr. Steve Brule',
      street1: '179 N Harbor Dr',
      city: 'Redondo Beach',
      state: 'CA',
      zip: '90277',
      country: 'US',
      email: 'dr_steve_brule@gmail.com',
      phone: '4155559999',
    },
    from_address: {
      street1: '417 montgomery street',
      street2: 'FL 5',
      city: 'San Francisco',
      state: 'CA',
      zip: '94104',
      country: 'US',
      company: 'EasyPost',
      phone: '415-123-4567',
    },
    parcel: {
      length: 20.2,
      width: 10.9,
      height: 5,
      weight: 65.9,
    },
    customs_info: { id: 'cstinfo_...' },
  });

  // or create by using IDs

  shipment = await client.Shipment.create({
    to_address: { id: 'adr_...' },
    from_address: { id: 'adr_...' },
    parcel: { id: 'prcl_...' },
    customs_info: { id: 'cstinfo_...' },
  });

  console.log(shipment);
})();
```

#### PHP

```php
<?php

$client = new \EasyPost\EasyPostClient('EASYPOST_API_KEY');

$shipment = $client->shipment->create([
    'to_address' => [
        'name' => 'Dr. Steve Brule',
        'street1' => '179 N Harbor Dr',
        'city' => 'Redondo Beach',
        'state' => 'CA',
        'zip' => '90277',
        'country' => 'US',
        'phone' => '3331114444',
        'email' => 'dr_steve_brule@gmail.com'
    ],
    'from_address' => [
        'name' => 'EasyPost',
        'street1' => '417 Montgomery Street',
        'street2' => '5th Floor',
        'city' => 'San Francisco',
        'state' => 'CA',
        'zip' => '94104',
        'country' => 'US',
        'phone' => '3331114444',
        'email' => 'support@easypost.com'
    ],
    'parcel' => [
        'length' => 20.2,
        'width' => 10.9,
        'height' => 5,
        'weight' => 65.9
    ]
]);

echo $shipment;
```

#### Python

```python
import easypost

client = easypost.EasyPostClient("EASYPOST_API_KEY")

shipment = client.shipment.create(
    to_address={
        "name": "Dr. Steve Brule",
        "street1": "179 N Harbor Dr",
        "city": "Redondo Beach",
        "state": "CA",
        "zip": "90277",
        "country": "US",
        "phone": "4153334444",
        "email": "dr_steve_brule@gmail.com",
    },
    from_address={
        "name": "EasyPost",
        "street1": "417 Montgomery Street",
        "street2": "5th Floor",
        "city": "San Francisco",
        "state": "CA",
        "zip": "94104",
        "country": "US",
        "phone": "4153334444",
        "email": "support@easypost.com",
    },
    parcel={
        "length": 20.2,
        "width": 10.9,
        "height": 5,
        "weight": 65.9,
    },
    customs_info={"id": "cstinfo_..."},
)

# or create by using IDs

shipment = client.shipment.create(
    to_address={"id": "adr_..."},
    from_address={"id": "adr_..."},
    parcel={"id": "prcl_..."},
    customs_info={"id": "cstinfo_..."},
)

print(shipment)
```

#### Ruby

```ruby
require 'easypost'

client = EasyPost::Client.new(api_key: 'EASYPOST_API_KEY')

shipment = client.shipment.create(
  to_address: {
    name: 'Dr. Steve Brule',
    street1: '179 N Harbor Dr',
    city: 'Redondo Beach',
    state: 'CA',
    zip: '90277',
    country: 'US',
    phone: '4155559999',
    email: 'dr_steve_brule@gmail.com',
  },
  from_address: {
    name: 'EasyPost',
    street1: '417 Montgomery Street',
    street2: '5th Floor',
    city: 'San Francisco',
    state: 'CA',
    zip: '94104',
    country: 'US',
    phone: '4153334445',
    email: 'support@easypost.com',
  },
  parcel: {
    length: 20.2,
    width: 10.9,
    height: 5,
    weight: 65.9,
  },
  customs_info: {
    id: 'cstinfo_...',
  },
)

puts shipment
```

---

## Process Tracking Event Webhooks

Once a `Tracker` is created–either manually or during label purchase–EasyPost begins sending tracking updates as webhook `Events`.

### Test Mode

- Sends a single tracking event in a supported status.
- No additional events follow.

### Production Mode

- Sends an initial event with status unknown.
- Additional events follow as the carrier scans the shipment.

### Event Identification

Tracking updates always use:

- `object: “Event”`
- `description: “tracker.updated”`
- `result:` A nested `Tracker` object containing the latest `status` and details.

The top-level `status` field on the `Tracker` object is the most reliable indicator for business logic.

#### Example Tracking Event Webhook

```json
{
  "id": "evt_...",
  "object": "Event",
  "created_at": "2014-11-19T10:51:54Z",
  "updated_at": "2014-11-19T10:51:54Z",
  "description": "tracker.updated",
  "mode": "test",
  "previous_attributes": {
    "status": "unknown"
  },
  "pending_urls": [],
  "completed_urls": [],
  "result": {
    "id": "trk_...",
    "object": "Tracker",
    "mode": "test",
    "tracking_code": "EZ4000000004",
    "status": "delivered",
    "created_at": "2014-11-18T10:51:54Z",
    "updated_at": "2014-11-19T10:51:54Z",
    "signed_by": "John Tester",
    "weight": 17.6,
    "est_delivery_date": "2014-11-27T00:00:00Z",
    "shipment_id": null,
    "carrier": "UPS",
    "public_url": "https://track.easypost.com/djE7...",
    "tracking_details": [
      {
        "object": "TrackingDetail",
        "message": "BILLING INFORMATION RECEIVED",
        "status": "pre_transit",
        "datetime": "2014-11-21T14:24:00Z",
        "tracking_location": {
          "object": "TrackingLocation",
          "city": null,
          "state": null,
          "country": null,
          "zip": null
        }
      },
      {
        "object": "TrackingDetail",
        "message": "ORIGIN SCAN",
        "status": "in_transit",
        "datetime": "2014-11-21T14:48:00Z",
        "tracking_location": {
          "object": "TrackingLocation",
          "city": "SOUTH SAN FRANCISCO",
          "state": "CA",
          "country": "US",
          "zip": null
        }
      },
      {
        "object": "TrackingDetail",
        "message": "DEPARTURE SCAN",
        "status": "in_transit",
        "datetime": "2014-11-22T08:51:00Z",
        "tracking_location": {
          "object": "TrackingLocation",
          "city": "SOUTH SAN FRANCISCO",
          "state": "CA",
          "country": "US",
          "zip": null
        }
      },
      {
        "object": "TrackingDetail",
        "message": "ARRIVAL SCAN",
        "status": "in_transit",
        "datetime": "2014-11-23T09:31:00Z",
        "tracking_location": {
          "object": "TrackingLocation",
          "city": "SAN FRANCISCO",
          "state": "CA",
          "country": "US",
          "zip": null
        }
      },
      {
        "object": "TrackingDetail",
        "message": "OUT FOR DELIVERY",
        "status": "out_for_delivery",
        "datetime": "2014-11-24T08:10:00Z",
        "tracking_location": {
          "object": "TrackingLocation",
          "city": "SAN FRANCISCO",
          "state": "CA",
          "country": "US",
          "zip": null
        }
      },
      {
        "object": "TrackingDetail",
        "message": "DELIVERED",
        "status": "delivered",
        "datetime": "2014-11-19T10:51:54Z",
        "tracking_location": {
          "object": "TrackingLocation",
          "city": "SAN FRANCISCO",
          "state": "CA",
          "country": "US",
          "zip": null
        }
      }
    ]
  }
}
```

Related:

- [Event Object](https://docs.easypost.com/docs/trackers)

---

## Tracking Details

The `tracking_details` array contains all current and historical status information returned by the carrier. Each `TrackingDetail` includes:

- Carrier message (e.g., `“OUT FOR DELIVERY”`)
- Status
- Timestamp
- `tracking_location` field with location data when supplied.

Oldest events appear first, and new events are appended as they are received. Because carriers occasionally send updates out of order, use
the top-level status field for any primary workflow logic.

---

## Time Zones and Geocoded Tracking Locations

Carriers do not consistently supply time zone information. Some return timestamps in local time without a zone, while others include full
time-zoned timestamps. Historically, timestamps without time zones were interpreted as UTC, which could misrepresent the actual event time.

To improve accuracy, EasyPost now attempts to determine the correct time zone for a tracking event using available carrier location data.
When a `tracking_location` contains usable details–such as city, state, zip, or country—EasyPost:

- Maps the location to a real-world geographic point.
- Determines the likely time zone.
- Stores and presents the timestamp with the correct time zone whenever possible.

If sufficient information is provided, the timestamp may still use UTC or a best-effort estimate.

#### Key considerations

- Time zone precision depends on the level of detail provided by the carrier.
- Not all carriers provide adequate location data.
- Events with explicit time zones from the carrier are preserved as provided.

Applications should treat timestamps as best-effort values rather than guaranteed time-zone-accurate across all carriers.

[Content omitted: table content. Review the rendered documentation for complete tabular data.]

### Confidence Levels

When assigning a time zone to a tracking event, EasyPost evaluates the quality of available carrier location data. Each
event is assigned a confidence level that reflects how precisely the location could be determined.

Confidence levels are informational and should be treated as best-effort indicators. They do not guarantee absolute accuracy.

[Content omitted: table content. Review the rendered documentation for complete tabular data.]

---

## Re-engaging Customers with Tracking Data

Tracking events can be used to provide proactive shipment updates and improve the post-purchase experience.

### Advanced Tracking (recommended)

For a fully branded, no-code post-purchase experience–including automated email and SMS notifications–EasyPost offers Advanced Tracking, powered by WeSupply.

Advanced Tracking provides:

- Custom-branded tracking pages
- Automated email and SMS notifications
- Split-shipment visibility
- End-customer insights and reporting

This option is ideal for teams seeking a complete branded experience without managing custom notification logic.

### Custom Notifications through the API

For workflows that require full programmatic control–such as internal alerts, operational notifications, or integrations with existing systems–tracking
webhooks can drive custom logic for email, SMS, or other channels.

Common patterns include:

- Linking recipients to the public tracking page using the `public_url`.
- Sending email updates when key events occur, such as `out_for_delivery` and `delivered`.
- Sending SMS updates using providers such as Twilio.

![Tracking Details Web](https://docs.easypost.com/images/guides/tracking/tracking-details-web.png)

Related:

- [SMS Tracking Notifications](https://docs.easypost.com/guides/email-tracking-tutorial)