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 with 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 the KeyFlight subscription: 3,000 decodes a month.

Get an API key Try the free converter

Decode PNRs where your agents already work

Let agents paste a GDS display instead of typing flights. The API turns it into segments with airports, local dates and times, ready for your forms.

Your first request in three steps

One endpoint, one key, JSON in and out.


  1. Get the subscription. The API comes with the KeyFlight 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.
POST /api/v1/pnr/decode
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 subscribers of the KeyFlight subscription. 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 and terminals 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 and the airlines' references 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[].durationFlight time in minutes, counted across the two airports' time zones (summer time included), or null when an airport, its time zone, a date or a time is not known.
segments[].distance.kmGreat-circle distance between the airports in kilometres; distance is null when an airport is not known.
segments[].distance.milesThe same distance in statute miles.
segments[].aircraftCodeIATA code of the aircraft type when the PNR shows it (Amadeus E 0 744 LM after the times), or null.
segments[].aircraftAircraft name for aircraftCode: "Boeing 747-400", or null.
segments[].operatedByOperating airline as the PNR prints it under the flight (OPERATED BY …), or null.
segments[].airlineLocator.airlineCodeAirline of the airline's own booking reference (Amadeus BA/PQ7XNR, Sabre /DCBA*PQ7XNR); airlineLocator is null when the text has none.
segments[].airlineLocator.codeThe airline's booking reference, 6 characters.
segments[].departure.codeIATA code of the airport.
segments[].departure.cityCity, or null.
segments[].departure.airportAirport name, or null.
segments[].departure.terminalTerminal as the PNR prints it ("5", "2E"), 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",
      "duration": 490,
      "distance": {
        "km": 5540,
        "miles": 3442
      },
      "aircraftCode": null,
      "aircraft": null,
      "operatedBy": null,
      "airlineLocator": {
        "airlineCode": "BA",
        "code": "PQ7XNR"
      },
      "departure": {
        "code": "LHR",
        "city": "London",
        "airport": "London Heathrow Airport",
        "terminal": null,
        "date": "2026-05-03",
        "time": "08:55"
      },
      "arrival": {
        "code": "JFK",
        "city": "New York",
        "airport": "John F Kennedy International Airport",
        "terminal": null,
        "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",
      "duration": 430,
      "distance": {
        "km": 5540,
        "miles": 3442
      },
      "aircraftCode": null,
      "aircraft": null,
      "operatedBy": null,
      "airlineLocator": {
        "airlineCode": "BA",
        "code": "PQ7XNR"
      },
      "departure": {
        "code": "JFK",
        "city": "New York",
        "airport": "John F Kennedy International Airport",
        "terminal": null,
        "date": "2026-05-10",
        "time": "18:30"
      },
      "arrival": {
        "code": "LHR",
        "city": "London",
        "airport": "London Heathrow Airport",
        "terminal": null,
        "date": "2026-05-11",
        "time": "06:40"
      }
    }
  ],
  "itinerary": {
    "view": "table",
    "text": "BA 178 · British Airways · 8h 10m\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 · 7h 10m\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 · 8h 10m
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 · 7h 10m
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 · 8h 10m
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 · 7h 10m
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 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 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.

Use it from Claude, Cursor and other AI assistants (MCP)

The same key works with our MCP server: an AI assistant decodes PNRs and makes PRO ticket PDFs for you, in the chat. Server address: https://keyflight.io/mcp (Streamable HTTP, the key in the Authorization header).

  • decode_pnr: the itinerary as text and the JSON of this API. Counts towards the 3,000 decodes a month, like an API call.
  • make_ticket: a PRO ticket PDF from a PNR, a link to the file. Passengers, booking code, class, price and currency can be set in the request. Included in the subscription; up to 30 tickets an hour per key.

Claude Code

claude mcp add --transport http keyflight https://keyflight.io/mcp --header "Authorization: Bearer kfp_…"

Cursor (.cursor/mcp.json)

{
  "mcpServers": {
    "keyflight": {
      "url": "https://keyflight.io/mcp",
      "headers": {
        "Authorization": "Bearer kfp_…"
      }
    }
  }
}

Claude Desktop (claude_desktop_config.json, needs Node.js)

{
  "mcpServers": {
    "keyflight": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://keyflight.io/mcp",
        "--header",
        "Authorization: Bearer kfp_…"
      ]
    }
  }
}

Then ask, for example: “Decode this PNR and make a PRO ticket with a price of 850 EUR” and paste the booking.

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 the KeyFlight subscription: $29 a month or $249 a year.

3,000 API decodes a month, plus unlimited Pro tickets and hotel confirmations. 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?

Get the KeyFlight subscription, 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 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.