You need a transportation-grade signal for how busy Port Klang is without wiring up raw AIS feeds. By the end of this guide, you will query FreightPulse’s Port Congestion API for Port Klang (MYPKG), interpret the 7‑day average vs 90‑day baseline, and ship a production-safe integration that caches results and surfaces only the fields your operations team needs.
What this API measures (and what it does not)
FreightPulse’s congestion data for Port Klang comes from IMF PortWatch and is updated daily with a typical lag of about 6–7 days. It compares the recent 7‑day average of port calls to a 90‑day baseline for the same port. This is a throughput proxy, not a live AIS anchorage or wait-time metric.
Key takeaways for transportation teams:
- The signal reflects recent average port calls vs a rolling baseline, which helps you contextualize changes in activity level.
- It is not a real-time vessel queue length or berth wait time. Do not treat it as live AIS-derived delays.
- Updates are daily; plan to cache and refresh on a 24-hour cadence to avoid unnecessary polling.
Endpoint, method, auth, and envelope
Use a simple HTTP GET request with an API key header. Guests without a key are allowed up to 100 requests per day per IP.
- Method: GET
- URL: https://freightpulsehq.com/api/v1/port-congestion?port=
- Port code for Port Klang: MYPKG
- Auth: header X-API-Key: YOUR_API_KEY
- Envelope: {"success":true,"data":{...}}
The payload will include a data object with the results array and metadata that documents IMF PortWatch as the source and the lag characteristics. The fields you will actually use in transports ops are congestion (categorical), metrics.ratio_vs_baseline (numeric), and as_of (ISO date).
Copy-pasteable curl for Port Klang
You can paste the following into your terminal to retrieve congestion for MYPKG (Port Klang). Replace YOUR_API_KEY with your key; you can also call as a guest up to the guest limits.
curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/port-congestion?port=MYPKG"
Official live sample: reading the JSON for MYPKG
The block below is an official sample for Port Klang. It shows the response structure, including the categorical congestion level, the calculation window noted in meta, and the 7‑day vs 90‑day baseline metrics that underpin the label.
Live FreightPulse congestion for MYPKG.
Source: live
cURL:
JSON:
{
"success": true,
"data": {
"results": [
{
"port": "Port Klang",
"locode": "MY PKG",
"country": "Malaysia",
"congestion": "moderate",
"as_of": "2026-10-02",
"metrics": {
"recent_avg_portcalls_per_day": 32.4,
"baseline_avg_portcalls_per_day": 31.6,
"ratio_vs_baseline": 1.03
}
},
{
"port": "Pelabuhan Sungai Udang",
"locode": "MY PKG",
"country": "Malaysia",
"congestion": "moderate",
"as_of": "2026-10-02",
"metrics": {
"recent_avg_portcalls_per_day": 1.6,
"baseline_avg_portcalls_per_day": 1.9,
"ratio_vs_baseline": 0.83
}
}
],
"global_summary": {
"ports_high_congestion": 0,
"ports_moderate": 2,
"ports_low": 0
},
"meta": {
"source": "IMF PortWatch (portwatch.imf.org) - UN Global Platform AIS data",
"data_lag_days": "~6-7 (daily updates, not real-time)",
"methodology": "congestion level is an approximation: last 7 days' avg port calls vs 90-day baseline for the same port. Does not measure vessel wait/anchorage time."
}
},
"meta": {
"request_id": "5459622d-5463-4fcd-a821-a4643aee3b1d"
}
}
How to read it:
- congestion: A categorical label (e.g., moderate) derived from the ratio of recent to baseline port calls.
- as_of: ISO date (YYYY-MM-DD) for the latest data point. Data reflects conditions with an expected 6–7 day lag.
- metrics.ratio_vs_baseline: Numerical ratio; values near 1.0 indicate similar activity to baseline; above/below suggests higher/lower recent activity.
- metrics.recent_avg_portcalls_per_day and metrics.baseline_avg_portcalls_per_day: 7‑day and 90‑day averages, respectively; unit is port calls per day.
Minimal JavaScript example (fetch)
This snippet calls the same endpoint, extracts the top-line congestion label, the as_of date, and the ratio_vs_baseline you will surface on dashboards or push to your TMS.
async function getPortKlangCongestion() {
const url = "https://freightpulsehq.com/api/v1/port-congestion?port=MYPKG";
const res = await fetch(url, {
headers: { "X-API-Key": "YOUR_API_KEY" }
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const body = await res.json();
if (!body.success || !body.data || !Array.isArray(body.data.results)) {
throw new Error("Unexpected API envelope");
}
// Depending on your use case, you may select the first matching facility in results.
const first = body.data.results[0];
if (!first) {
return { available: false };
}
const congestion = first.congestion; // e.g., "moderate"
const asOf = first.as_of; // e.g., "2026-10-02"
const ratio = first.metrics && first.metrics.ratio_vs_baseline;
return {
available: true,
port: first.port,
locode: first.locode,
country: first.country,
congestion,
ratio_vs_baseline: ratio,
as_of: asOf
};
}
// Example usage:
getPortKlangCongestion()
.then(data => console.log("Port Klang congestion:", data))
.catch(err => console.error("API error:", err));
Practical integration notes for transportation teams
These details tend to save an hour during production rollout:
- Data scope: The response includes one or more entries in data.results that match the port code filter. Treat results as an array, not a single object.
- Timestamps: as_of is an ISO date (YYYY‑MM‑DD). Treat it as end-of-day in UTC for caching and comparisons.
- Update cadence: Daily refresh, with a typical 6–7 day lag. A 24‑hour cache (e.g., keyed by as_of) is a good default.
- Units: recent_avg_portcalls_per_day and baseline_avg_portcalls_per_day are in port calls per day. The ratio is unitless.
- What not to do: Do not derive or present “live wait times” from this dataset. It is not a measure of vessel anchorage or queue duration.
- Backfilling: If you build historical charts, store the entire response keyed by as_of. Avoid overwriting prior days.
- Error handling: Check the success boolean and that data.results is an array before reading fields.
- Guest usage: Without an API key, expect a 100 requests/day per IP ceiling. Add application-level rate limiting to stay within that cap.
Python example: production-safe retrieval with caching and validation
The following Python code demonstrates a defensive pattern: calling the endpoint, validating the envelope, extracting congestion, and stashing a one-day cache using the as_of date.
import os
import json
import time
import hashlib
import requests
API_URL = "https://freightpulsehq.com/api/v1/port-congestion?port=MYPKG"
API_KEY = os.environ.get("FREIGHTPULSE_API_KEY", "YOUR_API_KEY")
CACHE_DIR = ".fp_cache"
os.makedirs(CACHE_DIR, exist_ok=True)
def _cache_path(key: str) -> str:
h = hashlib.sha256(key.encode("utf-8")).hexdigest()
return os.path.join(CACHE_DIR, f"{h}.json")
def _read_cache(path: str):
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
def _write_cache(path: str, data):
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
os.replace(tmp, path)
def get_port_klang_congestion(force_refresh=False):
headers = {"X-API-Key": API_KEY}
# Use a stable cache key for the daily fetch.
daily_key = f"{API_URL}"
path = _cache_path(daily_key)
if not force_refresh:
cached = _read_cache(path)
if cached and isinstance(cached, dict) and cached.get("success") is True:
return cached
resp = requests.get(API_URL, headers=headers, timeout=20)
resp.raise_for_status()
body = resp.json()
# Validate envelope and expected fields
if body.get("success") is not True:
raise RuntimeError("API returned success=false")
data = body.get("data", {})
results = data.get("results", [])
if not isinstance(results, list) or len(results) == 0:
raise RuntimeError("No results for MYPKG")
# Extract the first result for Port Klang
first = results[0]
congestion = first.get("congestion")
as_of = first.get("as_of")
metrics = first.get("metrics", {}) or {}
ratio = metrics.get("ratio_vs_baseline")
# You can enrich the body with a compact summary for downstream systems
summary = {
"port": first.get("port"),
"locode": first.get("locode"),
"country": first.get("country"),
"as_of": as_of,
"congestion": congestion,
"ratio_vs_baseline": ratio
}
body["_summary"] = summary
_write_cache(path, body)
return body
if __name__ == "__main__":
data = get_port_klang_congestion()
print(json.dumps(data.get("_summary", {}), indent=2))
Interpreting congestion for dispatch and routing
Because the level is derived from a 7‑day average versus a 90‑day baseline, it is best used as context rather than a binary “go/no-go” on sailings or drayage timing. Suggestions for transportation workflows:
- Use congestion plus ratio_vs_baseline to annotate ETAs with a qualitative note (e.g., “Port activity near baseline as of YYYY‑MM‑DD”).
- Trigger soft alerts only when the ratio_vs_baseline departs materially from 1.0 for several consecutive as_of dates you store in your cache.
- Do not convert congestion to USD surcharges or quote a freight rate as “today’s price” using this dataset. It is not a pricing feed.
Quality and methodology references
The response includes a meta block documenting IMF PortWatch as the underlying source and describing the 7‑day vs 90‑day approach. Since the dataset is lagged by about 6–7 days, you should align internal SLAs on data freshness accordingly and make that clear in any UI labels.
If you need more detail on the envelope and fields shown here, see the product Documentation. Use the same HTTP method, endpoint, and header pattern shown above for MYPKG.
Common pitfalls and how to avoid them
- Confusing port calls with wait times: The API measures throughput proxies, not queue length. Keep labels precise.
- Over-polling: With daily updates and a lag, polling hourly wastes quota. Cache for 24 hours keyed by as_of.
- Assuming single-result structure: Always index into data.results; multiple facilities may appear.
- Dropping meta-methodology: Surface the lag note in your UI to avoid misinterpretation by planners or customers.
FAQ
Is this real-time congestion for Port Klang?
No. It is based on IMF PortWatch with a typical 6–7 day lag. It compares the last 7 days of port calls to a 90‑day baseline and is updated daily.
Which fields should I store for operations dashboards?
Store congestion, as_of, and metrics.ratio_vs_baseline. Optionally persist recent_avg_portcalls_per_day and baseline_avg_portcalls_per_day if you plot context bands.
How often should I refresh the data?
Once per day is appropriate. Cache results and key them by as_of to keep a historical trail and avoid reprocessing.
Can I use this to price shipments or quote surcharges?
Do not quote a freight-rates USD fare as today’s price from this dataset. It is not a pricing feed and does not represent live wait times.
What happens if I call without an API key?
Guests without a key may call the endpoint with a limit of 100 requests per day per IP. For higher volumes, add your key in the X-API-Key header.
Get access and ship your integration
Start with a key, copy the curl above, and wire up your fetch or requests client to read congestion, ratio_vs_baseline, and as_of for MYPKG. You can create an account here: Register. For field-by-field details and any transport-specific clarifications, visit the Documentation.