Every trip starts with an inbox flood: one confirmation from Delta, another from Marriott, a third from Hertz — each with a different layout, a different way of saying “here’s your booking,” and a different set of fields buried in HTML. Your code needs the confirmation code, the dates, the traveler name, and the price, not a mountain of styled markup.
MailFrame turns those emails into a single typed JSON object you can write straight to your travel-management database or expense system.
The Travel Booking schema page covers the field reference; this tutorial walks through parsing a real booking email end to end, handling multi-segment itineraries, and wiring the result into your backend.
What you get
MailFrame’s travel_booking schema extracts these fields from every
confirmation email:
| Field | Type | Example | Notes |
|---|---|---|---|
confirmation_code | string | X7K9PQ | Booking reference / PNR |
booking_type | enum | flight | One of flight, hotel, car_rental |
provider_name | string | Delta Air Lines | Airline, hotel brand, or rental agency |
provider_domain | string | delta.com | Sender domain for deduplication |
traveler_name | string | Jordan Rivera | Name on the booking |
total_price_cents | integer | 48200 | Full booking amount in minor units (cents) |
currency | string | usd | ISO 4217, lower-cased |
booking_status | enum | confirmed | One of confirmed, changed, cancelled, pending |
manage_url | string | https://www.delta.com/mytrips/X7K9PQ | Deep link to manage the booking |
segments | array | see below | Per-leg flight/stay/rental details |
Each segment includes:
| Field | Type | Example | Notes |
|---|---|---|---|
start_datetime | string | 2026-07-14T08:05:00-04:00 | ISO 8601 datetime with timezone |
end_datetime | string | 2026-07-14T10:55:00-07:00 | ISO 8601 datetime with timezone |
start_location | string | JFK | Departure airport, hotel city, or pickup location |
end_location | string | LAX | Arrival airport, hotel city, or drop-off location |
detail | string | DL415 · Main Cabin | Flight number + cabin, room type + nights, or vehicle class |
Sample input
A typical flight confirmation email looks like this:
From: delta-reservations@delta.com
Subject: Your Flight Confirmation - X7K9PQ
Date: Thu, 03 Jul 2026 09:00:00 -0400
To: jordan@example.com
Booking Reference: X7K9PQ
Passenger: Jordan Rivera
Itinerary:
DL 415 JFK → LAX Jul 14, 2026 Depart 08:05 AM ET Arrive 10:55 AM PT
DL 1180 LAX → JFK Jul 19, 2026 Depart 06:40 PM PT Arrive 02:59 AM+1 ET
Cabin: Main Cabin (Economy)
Total charged: $482.00 USD
Manage your trip: https://www.delta.com/mytrips/X7K9PQ
Structured JSON output
Pass the raw email and the schema to MailFrame. You can reference the
pre-built schema by id (schema_id) or pass an inline JSON Schema. Here it is
with curl, using the built-in travel_booking schema against the Delta
confirmation above:
curl -X POST https://api.mailframe.ai/v1/parse \
-H "Authorization: Bearer $MAILFRAME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"schema_id": "travel_booking",
"raw_mime": "From: delta-reservations@delta.com\r\nTo: jordan@example.com\r\nSubject: Your Flight Confirmation - X7K9PQ\r\nDate: Thu, 03 Jul 2026 09:00:00 -0400\r\n\r\nBooking Reference: X7K9PQ\r\nPassenger: Jordan Rivera\r\nItinerary:\r\n DL 415 JFK → LAX Jul 14, 2026 Depart 08:05 AM ET Arrive 10:55 AM PT\r\n DL 1180 LAX → JFK Jul 19, 2026 Depart 06:40 PM PT Arrive 02:59 AM+1 ET\r\nCabin: Main Cabin (Economy)\r\n\r\nTotal charged: $482.00 USD\r\n\r\nManage your trip: https://www.delta.com/mytrips/X7K9PQ"
}'
The response wraps the extracted object in the standard MailFrame envelope:
{
"id": "parse_9f3c2a1b",
"status": "completed",
"validation_errors": [],
"data": {
"booking_type": "flight",
"confirmation_code": "X7K9PQ",
"provider_name": "Delta Air Lines",
"provider_domain": "delta.com",
"traveler_name": "Jordan Rivera",
"total_price_cents": 48200,
"currency": "usd",
"booking_status": "confirmed",
"manage_url": "https://www.delta.com/mytrips/X7K9PQ",
"segments": [
{
"start_datetime": "2026-07-14T08:05:00-04:00",
"end_datetime": "2026-07-14T10:55:00-07:00",
"start_location": "JFK",
"end_location": "LAX",
"detail": "DL415 · Main Cabin"
},
{
"start_datetime": "2026-07-19T18:40:00-07:00",
"end_datetime": "2026-07-20T02:59:00-05:00",
"start_location": "LAX",
"end_location": "JFK",
"detail": "DL1180 · Main Cabin"
}
]
}
}
TypeScript
const res = await fetch("https://api.mailframe.ai/v1/parse", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
schema_id: "travel_booking",
raw_mime: rawEmail,
}),
});
const result = await res.json();
// result.data.confirmation_code → "X7K9PQ"
// result.data.segments[0].detail → "DL415 · Main Cabin"
Python
import os
import requests
resp = requests.post(
"https://api.mailframe.ai/v1/parse",
headers={
"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}",
"Content-Type": "application/json",
},
json={
"schema_id": "travel_booking",
"raw_mime": raw_email,
},
)
result = resp.json()
# result["data"]["confirmation_code"] → "X7K9PQ"
# result["data"]["segments"][0]["detail"] → "DL415 · Main Cabin"
Real-world patterns
Multi-segment itineraries
A round-trip or multi-city booking has more than one segment. The segments
array captures each leg independently so you can store, filter, and join on
individual flights or stays without re-parsing.
// Store each segment as its own row for per-leg queries
for (const seg of result.data.segments) {
await db.query(
"INSERT INTO booking_segments (booking_id, type, start_dt, end_dt, origin, destination, detail) VALUES ($1, $2, $3, $4, $5, $6, $7)",
[
result.data.confirmation_code,
result.data.booking_type,
seg.start_datetime,
seg.end_datetime,
seg.start_location,
seg.end_location,
seg.detail,
],
);
}
Deduplication by confirmation code
Travel providers sometimes send the same confirmation twice — once when you
book, once 24 hours before departure. The confirmation_code field is your
natural idempotency key:
// Upsert on confirmation_code — second parse updates instead of duplicating
await db.query(
`INSERT INTO travel_bookings (confirmation_code, booking_type, provider_name, total_price_cents, currency, status, raw_data)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (confirmation_code) DO UPDATE SET
status = EXCLUDED.status,
raw_data = EXCLUDED.raw_data,
updated_at = NOW()`,
[
result.data.confirmation_code,
result.data.booking_type,
result.data.provider_name,
result.data.total_price_cents,
result.data.currency,
result.data.booking_status,
result.data,
],
);
Expense automation
Map booking_type to your expense categories. Since total_price_cents is a
booking-level total, create a single expense per booking rather than per
segment:
const categoryMap: Record<string, string> = {
flight: "travel_flights",
hotel: "travel_hotels",
car_rental: "travel_rental_cars",
};
await expenses.create({
category: categoryMap[result.data.booking_type] ?? "travel_other",
amount: result.data.total_price_cents / 100,
currency: result.data.currency,
description: `${result.data.provider_name} — ${result.data.segments.map((s: any) => `${s.start_location}→${s.end_location}`).join(", ")}`,
date: result.data.segments[0].start_datetime.split("T")[0],
});
Best practices
- Use
confirmation_codeas your idempotency key. Airlines and hotels send duplicate confirmations; upsert on the code to avoid double-booking rows. - Store
segmentsas separate rows. A single parse response may contain flights, hotels, and rental cars in one email — flatten them into per-segment records so you can query “all flights” or “all hotel stays” without re-parsing. - Use
total_price_cents(integer minor units) for all money calculations. Avoid floating-point rounding; convert to dollars only at the display layer. - Keep the raw MIME intact. Don’t pre-strip the HTML before sending — the sender domain and the confirmation’s structure are signal the extractor leans on to tell Delta from United from Southwest. Send the message as you receive it.
- Treat
booking_statusas an enum. The schema normalises the provider’s wording (“Confirmed”, “Itinerary”, “Your trip is set”) into one ofconfirmed,changed,cancelled, orpending. Build your state machine on those four states.
Next steps
The example above POSTs from wherever you already hold the message — an IMAP poller on a shared travel inbox, an S3 trigger on a landing bucket, or a no-code pipeline forwarding confirmations to a single mailbox. That keeps ingestion in your own code and under your own retries, which is the model we generally recommend; the trade-offs are laid out in How to Choose an Email Parsing API.
For teams that would rather receive results asynchronously, signed webhook
delivery (HMAC-SHA256, with retries and delivery history) is available in early
access — see Designing Webhooks That Don’t Break
for how to verify and handle those payloads. Forwarding a travel inbox to a
unique MailFrame inbox address, instead of POSTing it yourself, is on the
roadmap; until it ships, the direct POST shown here is the supported path.
The full endpoint reference — request and response shapes, error handling, and language samples — lives in the API docs. If you want to try the travel-booking schema against your own confirmations, request early access and we’ll help you wire it up.