Carriers send a separate email for almost every meaningful change to a package — the label is created, the package is picked up, it arrives at a sort hub, an exception is filed, it’s out for delivery, it’s delivered. The fields you actually want (which carrier, which tracking number, what status, when, and where) are buried in inconsistent HTML layouts that change per carrier and per event type. MailFrame extracts those fields into a typed, schema-validated JSON object you can route to a customer notification, a tracking dashboard, or a support tool that needs to know whether a package is moving.
It works on the plain-text and HTML parts of the notification emails FedEx, UPS, USPS, and DHL send for shipment creation, in-transit updates, out-for-delivery, delivery, exceptions (delays, address issues, lost parcel), and delivery attempts. Tracking numbers, status, the scheduled or estimated delivery date, and the most recent event are normalized into one consistent shape regardless of which carrier sent the message.
Fields MailFrame extracts
| Field | Type | Example | Notes |
|---|---|---|---|
carrier | enum | fedex | One of fedex, ups, usps, dhl |
tracking_number | string | 9205123456789012345678 | Carrier’s tracking number, as printed |
status | enum | out_for_delivery | One of label_created, picked_up, in_transit, out_for_delivery, delivered, exception, delivery_attempt_failed, returned_to_sender |
status_detail | string | Address corrected - delivery rescheduled | Carrier’s free-text status line when present |
origin_location | string | Memphis, TN | Origin city / sort facility when present |
destination_location | string | Brooklyn, NY 11201 | Destination city / postal area when present |
estimated_delivery_date | string | 2026-06-30 | Carrier’s ETA, normalized to ISO 8601 |
actual_delivery_date | string | 2026-06-29 | Actual delivery date when status is delivered |
last_event_at | string | 2026-06-29T08:14:00-04:00 | Timestamp of the most recent carrier event, ISO 8601 with offset |
last_event_location | string | Brooklyn, NY 11201 | Location the last event was recorded at |
service_level | string | Ground | Service level when printed (Ground, 2-Day, Priority, Express, etc.) |
weight_lb | number | 3.2 | Package weight in pounds when present |
tracking_url | string | https://www.fedex.com/fedextrack/?trknbr=9205123456789012345678 | Carrier’s direct tracking page |
reference_number | string | PO-5567 | Shipper’s reference, PO, or order number when present |
date | string | 2026-06-29 | Notification date normalized to ISO 8601 |
Sample input
A typical FedEx delivery notification email looks like this:
From: tracking@fedex.com
Subject: FedEx delivery notification - 9205123456789012345678
Date: Mon, 29 Jun 2026 08:14:00 -0400
To: jordan@example.com
Your package has been delivered.
Tracking number: 9205123456789012345678
Service: FedEx Ground
Status: Delivered
Delivered to: Brooklyn, NY 11201
Delivered on: June 29, 2026 at 08:14 AM
Signed by: J. RIVERA
Track at: https://www.fedex.com/fedextrack/?trknbr=9205123456789012345678
The same shape is produced for UPS Quantum View Notify messages, USPS
Informed Delivery delivery alerts, and DHL On Demand Delivery
notifications — carrier distinguishes them and the rest of the fields
normalize to the same JSON keys.
Structured JSON output
{
"carrier": "fedex",
"tracking_number": "9205123456789012345678",
"status": "delivered",
"status_detail": "Signed for by J. RIVERA",
"origin_location": "Memphis, TN",
"destination_location": "Brooklyn, NY 11201",
"actual_delivery_date": "2026-06-29",
"last_event_at": "2026-06-29T08:14:00-04:00",
"last_event_location": "Brooklyn, NY 11201",
"service_level": "FedEx Ground",
"weight_lb": 3.2,
"tracking_url": "https://www.fedex.com/fedextrack/?trknbr=9205123456789012345678",
"reference_number": "PO-5567",
"date": "2026-06-29"
}
JSON Schema definition
Every field is validated against the schema before MailFrame returns it.
You can copy this as a starting point and tighten it for your own use
case — for example, require tracking_number if every email in your flow
should carry one, or add an enum to status if you only care about a
subset of events:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "shipping_notification",
"type": "object",
"required": ["carrier", "tracking_number", "status"],
"properties": {
"carrier": {
"type": "string",
"enum": ["fedex", "ups", "usps", "dhl"]
},
"tracking_number": {
"type": "string",
"minLength": 1,
"pattern": "\\S+"
},
"status": {
"type": "string",
"enum": [
"label_created",
"picked_up",
"in_transit",
"out_for_delivery",
"delivered",
"exception",
"delivery_attempt_failed",
"returned_to_sender"
]
},
"status_detail": { "type": "string" },
"origin_location": { "type": "string" },
"destination_location": { "type": "string" },
"estimated_delivery_date": { "type": "string", "format": "date" },
"actual_delivery_date": { "type": "string", "format": "date" },
"last_event_at": { "type": "string", "format": "date-time" },
"last_event_location": { "type": "string" },
"service_level": { "type": "string" },
"weight_lb": { "type": "number", "minimum": 0 },
"tracking_url": { "type": "string", "format": "uri" },
"reference_number": { "type": "string" },
"date": { "type": "string", "format": "date" }
}
}
Parse via the API
POST the raw email (MIME or plain text) to /v1/parse with the schema you
want to extract against:
# The page above documents the normalized shipping-notification shape.
# The engine currently ships per-carrier schemas (fedex-tracking, ups-tracking,
# usps-tracking, dhl-tracking); the example below targets the FedEx one to
# match the Sample input above.
curl https://api.mailframe.ai/v1/parse \
-H "Authorization: Bearer $MAILF...KEY" \
-H "Content-Type: application/json" \
-d '{
"schema_id": "fedex-tracking",
"raw_mime": "From: tracking@fedex.com\r\nSubject: FedEx delivery notification - 9205123456789012345678\r\n\r\nYour package has been delivered."
}'
The call is synchronous: /v1/parse validates the extraction against
your schema and returns the typed JSON in the HTTP response, so you can
act on it inline — push a delivery notification, mark an order fulfilled
in your OMS, or trigger a follow-up workflow on exception.
Routing on the result
The most useful split is between terminal and non-terminal status: any
status in {"label_created", "picked_up", "in_transit", "out_for_delivery"} is a normal lifecycle event you can fold into a
tracking view, while delivered triggers a fulfillment update and any
value in {"exception", "delivery_attempt_failed", "returned_to_sender"}
is worth surfacing to a support queue. Use the tracking_number as the
deduplication key — carriers often send one message per event, so a
single shipment produces several notifications, and the same
carrier + tracking_number pair is the right join.
Signed webhook delivery
Signed async webhook delivery — where MailFrame POSTs a signed envelope
to your endpoint when a parse completes — is available during early
access alongside the synchronous API. Each delivery carries an
HMAC-SHA256 signature in the MailFrame-Signature header for
verification, with exponential-backoff retries on failure. Inbox
forwarding — pointing a Gmail or Outlook filter at a unique inbox
address MailFrame assigns you — is on the roadmap.