US regulations put the requirement in one sentence. Under 14 CFR 91.103, "each pilot in command shall, before beginning a flight, become familiar with all available information concerning that flight", which for most flights means weather reports and forecasts, NOTAMs, fuel, alternates and known delays. Other authorities say much the same. Pulling that information together is the preflight briefing, and a lot of software exists to make it faster.
SkyLink has two endpoints for it. /briefing/flight returns the briefing as data, and /briefing/pdf returns it as a printable document. They overlap a lot, and picking the wrong one makes the integration harder than it needs to be. This post covers when to use which, and the handling each one needs.
One thing up front, because the docs are firm about it: both are assistive. The summaries are generated by an AI model, and neither replaces an official briefing, dispatch release or authority publication. Anything you build should say so where users can see it.
The short version
| You're building | Use | Format |
|---|---|---|
| An app screen, dashboard or dispatch card | /briefing/flight | json |
| A message, email or chat post | /briefing/flight | markdown, plain_text or html |
| Something a crew prints, signs or files | /briefing/pdf | PDF bytes |
| A record of what was known before departure | /briefing/pdf | PDF bytes, stored |
One trap applies to both. The same route uses different parameter names on each endpoint: origin and destination on /briefing/flight, and departure_icao and arrival_icao on /briefing/pdf. Both want ICAO codes, so KJFK, not JFK.
JSON: for anything your code reads
curl "https://skylink-api.p.rapidapi.com/briefing/flight?origin=KJFK&destination=EGLL&format=json" \
-H "X-RapidAPI-Key: YOUR_KEY" \
-H "X-RapidAPI-Host: skylink-api.p.rapidapi.com"{
"origin": "KJFK",
"destination": "EGLL",
"summary": "VFR at KJFK, MVFR at EGLL with light rain and wet-runway delay risk.",
"critical_restrictions": [],
"origin_briefing": {
"icao": "KJFK",
"weather": {
"metar_raw": "KJFK 151856Z 31008KT 10SM FEW250 08/M06 A3012 RMK AO2 SLP204",
"taf_raw": "TAF KJFK 151730Z 1518/1624 30010KT P6SM FEW250"
},
"notams": []
},
"destination_briefing": {
"icao": "EGLL",
"weather": {
"metar_raw": "EGLL 151850Z 22012KT 4000 -RA BKN030 07/05 Q1009",
"taf_raw": "TAF EGLL 151658Z 1518/1624 22012KT 6000 -RA BKN030"
},
"notams": []
},
"pireps": []
}The useful thing about the JSON form is that the AI summary sits next to the raw sources it came from. Keep it that way in your interface. Show critical_restrictions first, then the summary, then the raw METAR, TAF and NOTAMs where a pilot can check them. A summary nobody can check against its sources is the wrong design for this kind of information.
Two fields need defensive handling. metar_raw and taf_raw can be null when a report isn't available, which is common at smaller fields that don't issue a TAF. And a METAR that is present can still be old. A station that has stopped reporting keeps serving its last observation, so check the age:
import re
import requests
from datetime import datetime, timezone
BASE = "https://skylink-api.p.rapidapi.com"
H = {"X-RapidAPI-Key": KEY, "X-RapidAPI-Host": "skylink-api.p.rapidapi.com"}
def get(path, **params):
r = requests.get(f"{BASE}{path}", headers=H, params=params, timeout=60)
r.raise_for_status()
return r.json()
def report_age_minutes(raw, now):
"""METARs and TAFs carry only day-of-month and time, e.g. 151856Z."""
m = re.search(r"\b(\d{2})(\d{2})(\d{2})Z\b", raw)
if not m:
return None
day, hh, mm = map(int, m.groups())
year, month = now.year, now.month
if day > now.day: # issued last month
month -= 1
if month == 0:
year, month = year - 1, 12
issued = datetime(year, month, day, hh, mm, tzinfo=timezone.utc)
return round((now - issued).total_seconds() / 60)
def briefing_card(origin, destination, now=None):
b = get("/briefing/flight", origin=origin, destination=destination, format="json")
now = now or datetime.now(timezone.utc)
card = {"route": f"{b['origin']} → {b['destination']}",
"restrictions": b.get("critical_restrictions") or [],
"summary": b.get("summary"),
"airports": []}
for side in ("origin_briefing", "destination_briefing"):
ap = b[side]
wx = ap.get("weather") or {}
metar = wx.get("metar_raw")
age = report_age_minutes(metar, now) if metar else None
card["airports"].append({
"icao": ap["icao"],
"metar": metar or "No METAR available",
"metar_stale": age is None or age > 90,
"taf": wx.get("taf_raw") or "No TAF available",
"notam_count": len(ap.get("notams") or []),
})
return cardThe month logic is there because a METAR timestamp has no month. A report stamped 282350Z read at 00:20 on 1 March came from 28 February, not from 28 March. Most stations report hourly, so 90 minutes is a sensible point to flag a METAR as stale. Our METAR and TAF guide covers what's inside the reports, and decoding NOTAMs covers sorting the NOTAM list so the runway closure doesn't sit under forty crane notices.
PIREPs are off by default. Turning on include_pireps=true adds a spatial search along the route and noticeably slows the response, so leave it off for screens that refresh often, and turn it on for the briefing a pilot reads before departure.

Markdown, plain text and HTML: for messages
With format=markdown, plain_text or html, the response is a small wrapper with the whole briefing in one string:
{
"origin": "KJFK",
"destination": "EGLL",
"briefing": "# Flight Briefing\n\n- Route: KJFK -> EGLL\n- Weather: ..."
}That's the right shape for a briefing posted to a crew chat, sent by email or attached to a ticket. Pick the format for where it's going. Chat tools each have their own Markdown dialect, so plain_text is the safe choice when you're not sure, and html suits email.
If you put the html version into a web page, sanitise it first. It contains NOTAM text and AI-written text, and you shouldn't insert either into your page unescaped. Run it through a sanitiser such as DOMPurify, or render the JSON version yourself instead.
Don't parse these formats in code. If your program needs a field, ask for JSON.
PDF: for people and for records
/briefing/pdf returns a finished document: departure and arrival METAR and TAF, active NOTAM summaries, relevant PIREPs and advisories, and AI-generated route notes. flight_number is optional and only sets the label in the header.
It's the only SkyLink endpoint that returns something other than JSON, which leads to the classic bug. An error still comes back as JSON, and code that writes the response body to briefing.pdf without checking produces a file that won't open. Check the type before saving:
from pathlib import Path
def download_briefing_pdf(dep, arr, flight_number=None, folder="."):
params = {"departure_icao": dep, "arrival_icao": arr}
if flight_number:
params["flight_number"] = flight_number
r = requests.get(f"{BASE}/briefing/pdf", headers=H, params=params, timeout=120)
is_pdf = r.headers.get("Content-Type", "").startswith("application/pdf")
if r.status_code != 200 or not is_pdf or not r.content.startswith(b"%PDF-"):
raise RuntimeError(f"briefing failed ({r.status_code}): {r.text[:300]}")
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%MZ")
path = Path(folder) / f"briefing_{dep}_{arr}_{stamp}.pdf"
path.write_bytes(r.content)
return pathThe %PDF- check is extra caution: every PDF file starts with those five bytes. The timeout is generous because the document includes generated text.
Put the generation time in the filename. The server's suggested name, briefing_EGLL_KJFK.pdf, is the same for every briefing on that route, and a PDF is a snapshot. The weather in it was current when it was made. If you cache PDFs, the docs suggest a key made of both ICAO codes plus the generation time. For anything a crew uses, generate it close to departure, and regenerate after a delay.
That snapshot quality is also why the PDF is the right format for a record. If you need to show later what information was available before a flight, a stored, timestamped PDF answers that question in a way a live JSON response can't.

Serving the PDF to a browser without exposing your key
The usual web setup is a "Download briefing" button. The browser mustn't call SkyLink directly, because that would put your API key in front-end code where anyone can copy it. Instead, route the download through your own server. In a Next.js app:
// app/api/briefing-pdf/route.ts: runs on your server, so the key never reaches the browser
const ICAO = /^[A-Z]{4}$/;
export async function GET(req: Request) {
const params = new URL(req.url).searchParams;
const dep = (params.get("dep") ?? "").toUpperCase();
const arr = (params.get("arr") ?? "").toUpperCase();
if (!ICAO.test(dep) || !ICAO.test(arr)) {
return Response.json({ error: "dep and arr must be 4-letter ICAO codes" }, { status: 400 });
}
const upstream = await fetch(
`https://skylink-api.p.rapidapi.com/briefing/pdf?departure_icao=${dep}&arrival_icao=${arr}`,
{
headers: {
"X-RapidAPI-Key": process.env.SKYLINK_API_KEY!,
"X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
},
cache: "no-store",
},
);
// Errors come back as JSON. Never pass them to the browser as a .pdf.
if (!upstream.ok || !upstream.headers.get("content-type")?.startsWith("application/pdf")) {
return Response.json({ error: "Briefing unavailable, try again shortly" }, { status: 502 });
}
return new Response(upstream.body, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="briefing_${dep}_${arr}.pdf"`,
"Cache-Control": "private, no-store",
},
});
}Validating the codes with a strict pattern does two jobs. It returns a clear error for typos, and it stops anyone adding extra query parameters to the upstream URL through your route.
The button then links to /api/briefing-pdf?dep=EGLL&arr=KJFK. Keep it a click, as the docs recommend: browsers handle downloads best when the user started them. And put your own login or rate limit in front of the route. Otherwise anyone who finds it can spend your quota generating PDFs.
Putting the formats together
Most real products end up using all three. A dispatch tool might show the JSON card on screen, refreshing every few minutes without PIREPs. It posts a plain_text briefing to the crew channel when the flight is assigned, and generates a timestamped PDF at release, stored with the flight record. Building an AI flight dispatcher shows a similar workflow driven by an AI agent.
Both briefing endpoints are on every plan, including the free trial, which is enough to try them on your own routes before you build anything. Keep the disclaimer visible whichever format you choose.
