PNR Converter API

A PNR decoder and parser API: send the raw text of an Amadeus, Sabre, Galileo or Worldspan booking and get JSON back — passengers, flights with airports, local dates and times, cabin and status, plus a ready itinerary as text and e-mail HTML.

The same engine as the free PNR converter, for your own booking tool, CRM or bot. Included in KeyFlight Pro: 3,000 decodes a month.

Get an API key Try the free converter

Quick start

1
Get KeyFlight Pro
The API comes with the Pro subscription, no separate plan.
2
Create a key
On /subscription use “Already subscribed?”, open the link we e-mail you and press “Create API key”.
3
Send a PNR
POST the booking text with the key in the Authorization header and read the JSON.
curl https://keyflight.io/api/v1/pnr/decode \
  -H "Authorization: Bearer $KEYFLIGHT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "text": "RP/LHRBA/LHRBA            BA/BA  26APR26/1200Z   PQ7XNR\nPQ7XNR/BA QLHR 03MAY\n1.JOHNSON JOHN\n2.JOHNSON JANE\n3  BA 178 Y 03MAY 7 LHRJFK HK2  0855 1205  03MAY  E  BA/PQ7XNR\n4  BA 117 Y 10MAY 7 JFKLHR HK2  1830 0640  11MAY  E  BA/PQ7XNR\n5 TKT/TIME LIMIT\n6 OPC LHRBA-10MAY:1900/1S2/Q TKT/T-BA\n7 FA PAX 125-2196837482/ETBA/GBP560.20/03MAY26/LHRBA2104/00390755/S3-4",
  "options": {
    "view": "table"
  }
}'

Authentication

Send the key in the header of every request:

Authorization: Bearer kfp_…
  • A key is kfp_ followed by 43 letters, digits, - and _.
  • Keys are for active KeyFlight Pro subscribers. Create one on /subscription → “Already subscribed?” → the link in the e-mail → “Create API key”. A subscription can have 2 keys at a time; revoke a key on the same page.
  • The key is shown once, when you create it. Keep it on your server.
  • The key keeps working when the subscription renews. While the subscription is not active, the key answers 403 subscription_inactive; renew it and the same key works again.
  • Call the API from your server, never from a browser: it sends no CORS headers, and a key in a web page is visible to everyone.
  • We do not store PNR texts: the API keeps no copy and does not log them. Answers carry Cache-Control: no-store.

Decode a PNR

POST https://keyflight.io/api/v1/pnr/decode
Content-Type: application/json

Request body

FieldValuesDefault
text Required. The booking as the GDS shows it, up to 20,000 characters. The format is recognised from the text. —
options.view table, lines2, lines3, whatsapp: the layout of itinerary, see Itinerary views. table
options.timeFormat "24" (18:30), "12" (6:30 PM) "24"
options.dateFormat "short" (3 May 2026), "numeric" (03.05.2026) "short"
options.show.logo true / false. Airline logos (HTML only). true
options.show.airports true / false. Airport names next to the city. true
options.show.cabin true / false. Cabin and booking class: Economy (Y). false
options.show.status true / false. Segment status: Confirmed, Waitlisted. false
options.show.passengers true / false. Passenger names above the flights. false
options.show.pnr true / false. The booking reference above the flights. false

All options are optional. An unknown option or value answers 400 invalid_option instead of a silent default. The options change only itinerary: the fields of the segments always carry ISO dates and 24-hour times.

Response

FieldMeaning
pnrCodeThe booking reference found in the text, or null.
passengers[].nameA passenger name as the PNR writes it.
segments[].airlineCodeIATA code of the airline.
segments[].airlineNameAirline name; an unknown code is its own name.
segments[].flightNumberFlight number without leading zeros: "950" for UA0950.
segments[].bookingClassBooking class letter, or null.
segments[].cabinBy the booking class: Economy, Premium Economy, Business, First Class.
segments[].statusStatus code as in the PNR ("HK2"), or null.
segments[].statusTextConfirmed, Waitlisted, or the code itself; null without a status.
segments[].logoUrlURL of the airline logo, 64×64 PNG (a plane silhouette when we have none).
segments[].departure.codeIATA code of the airport.
segments[].departure.cityCity, or null.
segments[].departure.airportAirport name, or null.
segments[].departure.dateLocal date at the airport, YYYY-MM-DD, or null when the text has none.
segments[].departure.timeLocal time at the airport, HH:MM (24 h), or null.
segments[].arrivalThe same fields for the arrival airport.
itinerary.viewThe view used.
itinerary.textThe itinerary as plain text.
itinerary.htmlThe itinerary as e-mail HTML (tables, inline styles); null for whatsapp.

A PNR has no years in its dates. The API takes the year from the date the booking was created (the RP line of Amadeus, the footer of Sabre); without it, each flight gets the nearest date in the future, and the next flights are not earlier than the one before.

Example

The Amadeus booking from the Amadeus PNR converter, sent with Node.js or Python:

// Node.js 18+ (fetch is built in), in an ES module or an async function
const text = `RP/LHRBA/LHRBA            BA/BA  26APR26/1200Z   PQ7XNR
PQ7XNR/BA QLHR 03MAY
1.JOHNSON JOHN
2.JOHNSON JANE
3  BA 178 Y 03MAY 7 LHRJFK HK2  0855 1205  03MAY  E  BA/PQ7XNR
4  BA 117 Y 10MAY 7 JFKLHR HK2  1830 0640  11MAY  E  BA/PQ7XNR
5 TKT/TIME LIMIT
6 OPC LHRBA-10MAY:1900/1S2/Q TKT/T-BA
7 FA PAX 125-2196837482/ETBA/GBP560.20/03MAY26/LHRBA2104/00390755/S3-4`;

const response = await fetch('https://keyflight.io/api/v1/pnr/decode', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.KEYFLIGHT_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ text, options: { "view": "table" } }),
});
const data = await response.json();
if (response.ok) {
  data.segments.forEach((s) => console.log(s.airlineCode, s.flightNumber, s.departure.date));
} else {
  console.error(response.status, data.error, data.message);
}
import os
import requests

text = """RP/LHRBA/LHRBA            BA/BA  26APR26/1200Z   PQ7XNR
PQ7XNR/BA QLHR 03MAY
1.JOHNSON JOHN
2.JOHNSON JANE
3  BA 178 Y 03MAY 7 LHRJFK HK2  0855 1205  03MAY  E  BA/PQ7XNR
4  BA 117 Y 10MAY 7 JFKLHR HK2  1830 0640  11MAY  E  BA/PQ7XNR
5 TKT/TIME LIMIT
6 OPC LHRBA-10MAY:1900/1S2/Q TKT/T-BA
7 FA PAX 125-2196837482/ETBA/GBP560.20/03MAY26/LHRBA2104/00390755/S3-4"""

response = requests.post(
    "https://keyflight.io/api/v1/pnr/decode",
    headers={"Authorization": f"Bearer {os.environ['KEYFLIGHT_API_KEY']}"},
    json={"text": text, "options": { "view": "table" }},
    timeout=30,
)
data = response.json()
if response.ok:
    for s in data["segments"]:
        print(s["airlineCode"], s["flightNumber"], s["departure"]["date"])
else:
    print(response.status_code, data["error"], data["message"])

The answer, made by the API code when this page was built (itinerary.html is shortened here, it is shown in full below):

{
  "pnrCode": "PQ7XNR",
  "passengers": [
    {
      "name": "JOHNSON JOHN"
    },
    {
      "name": "JOHNSON JANE"
    }
  ],
  "segments": [
    {
      "airlineCode": "BA",
      "airlineName": "British Airways",
      "flightNumber": "178",
      "bookingClass": "Y",
      "cabin": "Economy",
      "status": "HK2",
      "statusText": "Confirmed",
      "logoUrl": "https://keyflight.io/img/airlines/BA.png",
      "departure": {
        "code": "LHR",
        "city": "London",
        "airport": "London Heathrow Airport",
        "date": "2026-05-03",
        "time": "08:55"
      },
      "arrival": {
        "code": "JFK",
        "city": "New York",
        "airport": "John F Kennedy International Airport",
        "date": "2026-05-03",
        "time": "12:05"
      }
    },
    {
      "airlineCode": "BA",
      "airlineName": "British Airways",
      "flightNumber": "117",
      "bookingClass": "Y",
      "cabin": "Economy",
      "status": "HK2",
      "statusText": "Confirmed",
      "logoUrl": "https://keyflight.io/img/airlines/BA.png",
      "departure": {
        "code": "JFK",
        "city": "New York",
        "airport": "John F Kennedy International Airport",
        "date": "2026-05-10",
        "time": "18:30"
      },
      "arrival": {
        "code": "LHR",
        "city": "London",
        "airport": "London Heathrow Airport",
        "date": "2026-05-11",
        "time": "06:40"
      }
    }
  ],
  "itinerary": {
    "view": "table",
    "text": "BA 178 · British Airways\n3 May 2026 08:55  London (LHR), London Heathrow Airport\n3 May 2026 12:05  New York (JFK), John F Kennedy International Airport\n\nBA 117 · British Airways\n10 May 2026 18:30  New York (JFK), John F Kennedy International Airport\n11 May 2026 06:40  London (LHR), London Heathrow Airport",
    "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size…"
  }
}

Itinerary views

itinerary is the booking laid out the way the free converter shows and copies it: text for plain-text fields and messengers, html for e-mail.

ViewWhat you get
tableHTML: a table with logo, flight, departure and arrival. Text: three lines per flight.
lines2Two lines per flight: the route, then the times; airport names on a third line.
lines3Three lines per flight: flight and airline, departure, arrival.
whatsappText only, flight titles in *bold* for WhatsApp. html is null.

html of the example (table)

FlightDepartureArrival
BABA 178
British Airways
London (LHR)
3 May 2026 08:55
London Heathrow Airport
New York (JFK)
3 May 2026 12:05
John F Kennedy International Airport
BABA 117
British Airways
New York (JFK)
10 May 2026 18:30
John F Kennedy International Airport
London (LHR)
11 May 2026 06:40
London Heathrow Airport

text of the example (lines2)

BA 178  London (LHR) → New York (JFK)
3 May 2026 08:55 → 12:05
London Heathrow Airport → John F Kennedy International Airport

BA 117  New York (JFK) → London (LHR)
10 May 2026 18:30 → 11 May 2026 06:40
John F Kennedy International Airport → London Heathrow Airport

Errors

An error is JSON with the HTTP status: {"error": "<code>", "message": "<text>"}. Check error; message is for people and may change.

StatuserrorWhen
400invalid_jsonThe body is not a JSON object.
400text_requiredtext is missing or empty.
400invalid_optionAn unknown option or a wrong value. field names it, allowed lists the values.
401missing_api_keyNo Authorization: Bearer … header.
401invalid_api_keyThe key is not valid or was revoked.
403subscription_inactiveThe Pro subscription of the key is not active.
404not_foundNo such endpoint.
405method_not_allowedThe method is not POST.
413too_longThe text is longer than 20,000 characters (max in the answer).
413payload_too_largeThe body is larger than 100kb.
415unsupported_encodingThe body is not UTF-8.
422no_flightsNo flights in the text. Does not count towards the monthly limit.
429too_many_requestsOver the per-minute limit; wait Retry-After seconds.
429monthly_limit_reachedThe subscription has used its decodes for this month.
500server_errorSomething went wrong on our side.
503service_unavailableThe key cannot be checked right now. Try again in a minute.

Limits

  • 60 requests a minute per key. Over it: 429 too_many_requests with Retry-After. Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time). From one IP address: 120 requests a minute.
  • 3,000 decodes a month per Pro subscription, both keys together, calendar month in UTC. Over it: 429 monthly_limit_reached until the 1st. Every answer carries X-Monthly-Limit, X-Monthly-Remaining and X-Monthly-Reset (ISO time, the 1st of next month 00:00 UTC).
  • What counts: only decodes that found flights (status 200). 422 no_flights and other errors do not count.
  • Size: the text up to 20,000 characters, the JSON body up to 100kb. A PNR display is a few KB.

Supported GDS formats

The API recognises the format from the text. Each converter page shows an example booking you can send as it is:

Pricing

Included in KeyFlight Pro: $29 a month or $249 a year.

3,000 API decodes a month, plus unlimited Pro and Simple tickets, hotel bookings and the PNR converter. No separate API plan.

Frequently asked questions

What does the PNR Converter API do?

It decodes the text of a booking (a PNR) into JSON: the booking reference, the passengers and, for every flight, the airline, flight number, booking class, cabin, status, airports and local dates and times. It also returns the itinerary as ready text and e-mail HTML, the same one the free PNR converter shows.

Which GDS formats does it read?

Amadeus, Sabre, Galileo and Worldspan. The format is recognised from the text: send the booking as the GDS shows it, without saying which system it came from.

How do I get an API key?

Subscribe to KeyFlight Pro, then use "Already subscribed?" on the same page and open the link we e-mail you. There you create a key (up to 2 at a time) and revoke it. The key is shown once, so put it on your server right away.

How many requests can I make?

60 requests a minute per key and 3,000 decodes a month per Pro subscription (both keys together, calendar month in UTC). Only decodes that found flights count.

Do you store the PNR texts?

No. The API keeps no copy of the text and does not write it to logs. Answers are sent with Cache-Control: no-store, so proxies do not keep them either.

How does the API know the year of a flight?

A PNR shows dates without a year. The API takes it from the date the booking was created (the RP line of Amadeus, the footer of Sabre); without that date, it takes the nearest date in the future.

What happens when my subscription ends?

The key answers 403 subscription_inactive. Renew the subscription and the same key works again: you do not need a new one.

Can I call the API from a web page?

No. The API sends no CORS headers, and a key in a web page would be visible to everyone who opens it. Call the API from your server and pass the result on to your page.

Can the API make PDF tickets?

Not yet. The API returns the data and the itinerary as text and HTML. PDF tickets are made on the PNR converter page.