Please see the temporary solution at the end.
Summary (added 12/24/22 for clarification):
USPS's tracking API is not returning responses in the same format as their documentation. The actual format makes it difficult to extract the event date since there is no EventDate XML element. Worst case, I can use regex, but was wondering if there was a way to receive API responses as showing in USPS's documentation.
Details
In USPS's Track and Confirm API documentation page 19, the sample response shows <TrackSummary>
with child elements (<EventTime>, <EventDate>
, etc.):
Screenshot of USPS's sample response
Here's USPS's sample response in text:
<TrackResponse>
<TrackInfo ID=" XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ">
<GuaranteedDeliveryDate>June 24, 2022</GuaranteedDeliveryDate>
<TrackSummary>
<EventTime>9:00 am</EventTime>
<EventDate>June 22, 2022</EventDate>
<Event>Delivered, To Agent</Event>
<EventCity>AMARILLO</EventCity>
<EventState>TX</EventState>
<EventZIPCode>79109</EventZIPCode>
<EventCountry/>
<FirmName/>
<Name>RXXXXXX XXXXXXX</Name>
<AuthorizedAgent>false</AuthorizedAgent>
<DeliveryAttributeCode>23</DeliveryAttributeCode>
<GMT>14:00:00</GMT>
<GMTOffset>-05:00</GMTOffset>
</TrackSummary>
However, when performing the call, the actual XML response lacks these children elements within TrackSummary:
<?xml version="1.0" encoding="UTF-8"?>
<TrackResponse>
<TrackInfo ID="9405511206213782679396">
<TrackSummary>Your item departed our WEST PALM BEACH FL DISTRIBUTION CENTER destination facility on December 23, 2022 at 12:40 pm. The item is currently in transit to the destination.</TrackSummary>
<TrackDetail>Arrived at USPS Regional Facility, December 23, 2022, 4:49 am, WEST PALM BEACH FL DISTRIBUTION CENTER</TrackDetail>
<TrackDetail>In Transit to Next Facility, 12/22/2022, 9:41 pm</TrackDetail>
<TrackDetail>In Transit to Next Facility, 12/22/2022, 1:36 pm</TrackDetail>
<TrackDetail>Departed USPS Facility, 12/22/2022, 5:58 am, HARRISBURG, PA 17112</TrackDetail>
<TrackDetail>Arrived at USPS Regional Origin Facility, 12/21/2022, 10:12 pm, HARRISBURG PA PACKAGE SORTING CENTER</TrackDetail>
<TrackDetail>Departed Post Office, December 21, 2022, 4:34 pm, DALLASTOWN, PA 17313</TrackDetail>
<TrackDetail>USPS picked up item, December 21, 2022, 2:37 pm, DALLASTOWN, PA 17313</TrackDetail>
<TrackDetail>Shipping Label Created, USPS Awaiting Item, December 21, 2022, 2:16 pm, DALLASTOWN, PA 17313</TrackDetail>
</TrackInfo>
</TrackResponse>
This can be reproduced with Lob's USPS Postman workspace
The problem I'm trying to solve is obtaining the date from the TrackSummary data, which now requires regex since USPS's API is not returning an EventDate child element.
Is there an option when making the request to return these helpful XML child elements? I couldn't find one in the documentation and the sample responses I've seen all contain these child elements.
I've tried forming the request in Python and with Lob's USPS workspace and both XML responses lack the TrackSummary child elements.
Long-term solution (in progress 12/26/22)
@Parfait pointed out that I should use the Package Tracking “Fields” API instead of the Package Track API.
Here's how I'm currently forming the XML request with Package Track API:
from lxml import etree
def generate_url_tracking(tracking_numbers: list[str]) -> str:
"""generate the USPS tracking request url
:param: tracking_numbers - list of strings of tracking numbers
:return url: str tracking url for calling the USPS API
"""
xml = generate_xml_tracking(tracking_numbers)
url = f"{base_url}{url_vars['track']}{xml}"
return url
def generate_xml_tracking(tracking_numbers: list[str]) -> str:
"""
Generate USPS track and confirm API xml
:param tracking_numbers: list of strings of tracking numbers
:return: xml string
"""
xml = etree.Element("TrackRequest", {"USERID": config("USPS_USER")})
# loop through tracking numbers
for tracking in tracking_numbers:
etree.SubElement(xml, "TrackID", {"ID": tracking})
xml_string = etree.tostring(xml, encoding="utf8", method="xml").decode()
return xml_string
I'll update this to the Package Tracking “Fields” API request when I get time.
Temporary Solution (12/25/22)
Until USPS's actual responses match their API docs, this solution extracts the last updated date from <TrackSummary>
for several different statuses (pre-shipment, delivered, RTS, etc.)
The TRACK_SUMMARIES dict has the different statuses it's tested against. Some statuses without dates (no_info, out_for_delivery_no_date) return None.
import re
from dateutil.parser import ParserError, parse
TRACK_SUMMARIES = {
"delivered": """Your
item was delivered in or at the mailbox at 10:23 am on December 24, 2022 in HOBE SOUND, FL 33455.""",
"out_for_delivery": "Out for Delivery, December 13, 2021, 6:10 am, ARLINGTON, VA 22204.",
"out_for_delivery_no_date": "Out for Delivery, Expected Delivery Between 9:45am and 1:45pm",
"arrived_at_post_office": """Arrived at Post Office,
Arrived at USPS Regional Origin Facility, December 11, 2021, 9:23 pm, HARRISBURG PA PACKAGE SORTING CENTER""",
"acceptance": "Acceptance, December 10, 2021, 12:54 pm, DALLASTOWN, PA 17313",
"pre_shipment": "Pre-Shipment Info Sent to USPS, USPS Awaiting Item, December 27, 2021",
"rts": """Your item was returned to the sender on January 31, 2022 at 9:14 am in YORK, PA 17402
because of an incorrect address.""",
"no_info": "The Postal Service could not locate the tracking information for your request",
"label_prepared": "A shipping label has been prepared for your item at 10:47 am on December 16, 2021 in WINSTON",
"forwarded": """Your item was forwarded to a different address at 5:13 pm on January 4, 2022
in REDDING, CA. This was because of forwarding instructions or because the
address or ZIP Code on the label was incorrect.
""",
}
def get_last_updated(track_summary: str) -> Optional[datetime]:
"""Takes the USPS TrackSummary string and return the last updated datetime"""
# remove the zip code since it interferes with the date parser
track_summary = re.sub(r"\d{5}", "", track_summary)
months_regex = "January|February|March|April|May|June|July|August|September|October|November|December"
first_result = re.search(rf"(?={months_regex}).*", track_summary)
# return early if there's no Month
if not first_result:
return
first_result = first_result.group()
# some summaries have am/pm and some don't
result_for_parser = re.search(r".*(?<=am|pm)", first_result)
if result_for_parser:
result_for_parser = result_for_parser.group()
else:
result_for_parser = first_result
try:
# fuzzy parsing is required for dates in certain summaries
result = parse(result_for_parser, fuzzy=True)
except ParserError:
return
return result
Sources: