Skip to content
travel flights hotels booking confirmation schema email-to-json tutorial

Parse Travel Booking Confirmation Emails to JSON

Turn flight, hotel, and rental-car confirmation emails into typed JSON — confirmation code, provider, traveler, dates, locations, price, and per-segment legs. Schema-first tutorial.

MailFrame Team

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:

FieldTypeExampleNotes
confirmation_codestringX7K9PQBooking reference / PNR
booking_typeenumflightOne of flight, hotel, car_rental
provider_namestringDelta Air LinesAirline, hotel brand, or rental agency
provider_domainstringdelta.comSender domain for deduplication
traveler_namestringJordan RiveraName on the booking
total_price_centsinteger48200Full booking amount in minor units (cents)
currencystringusdISO 4217, lower-cased
booking_statusenumconfirmedOne of confirmed, changed, cancelled, pending
manage_urlstringhttps://www.delta.com/mytrips/X7K9PQDeep link to manage the booking
segmentsarraysee belowPer-leg flight/stay/rental details

Each segment includes:

FieldTypeExampleNotes
start_datetimestring2026-07-14T08:05:00-04:00ISO 8601 datetime with timezone
end_datetimestring2026-07-14T10:55:00-07:00ISO 8601 datetime with timezone
start_locationstringJFKDeparture airport, hotel city, or pickup location
end_locationstringLAXArrival airport, hotel city, or drop-off location
detailstringDL415 · Main CabinFlight 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_code as your idempotency key. Airlines and hotels send duplicate confirmations; upsert on the code to avoid double-booking rows.
  • Store segments as 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_status as an enum. The schema normalises the provider’s wording (“Confirmed”, “Itinerary”, “Your trip is set”) into one of confirmed, changed, cancelled, or pending. 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.

Turn your inbox into an API

MailFrame parses raw email into typed JSON against your own schema — PDF and image input are planned. Request early developer access.

Request early access