You need to programmatically check current congestion conditions for Qingdao (CNQDG) and surface the key fields your operations team can act on. By the end of this guide, you will have a production-ready PHP integration for the FreightPulse congestion lookup and a minimal Python script you can use for validation and monitoring.
What you will build
This post focuses on one task: fetch the congestion status for Qingdao (CNQDG), extract the fields congestion, ratio_vs_baseline, and as_of, and render them safely in your app or job. We will implement the end-to-end flow in PHP, including headers, JSON parsing, and error handling. A compact Python example is included so you can quickly compare outputs between runtimes.
Origin catalog facts you must use
- Auth: header X-API-Key: YOUR_API_KEY. Guest without key: 100 requests/day per IP.
- Envelope: {"success":true,"data":{...}}
- Port congestion is IMF PortWatch, lagged ~6-7 days, 7-day avg vs 90-day baseline — not live AIS.
- Do not quote a freight-rates USD fare as today’s price.
- Port: Qingdao (CNQDG)
- curl: curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/port-congestion?port=CNQDG"
- No official congestion JSON is published for CNQDG. Show the curl and field names (congestion, ratio_vs_baseline, as_of). Do not reuse the USLAX 0.85 fixture.
Endpoint and authentication
All requests are GET calls to a single endpoint with the port code as a query parameter. Authentication is via an HTTP header:
- Endpoint: https://freightpulsehq.com/api/v1/port-congestion?port=CNQDG
- Header: X-API-Key: YOUR_API_KEY
- Guest usage: 100 requests/day per IP if you omit the key
The top-level response envelope follows {"success": true, "data": {...}}. Parse the nested fields inside data.results[0] for application logic. See the Documentation for broader details.
Quick test with cURL
Live FreightPulse congestion for CNQDG.
Source: live
cURL:
curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/port-congestion?port=CNQDG"
Understanding the response
JSON:
{
"success": true,
"data": {
"results": [
{
"port": "Qingdao Port",
"locode": "CN QDG",
"country": "China",
"congestion": "moderate",
"as_of": "2026-10-23",
"metrics": {
"recent_avg_portcalls_per_day": 25.6,
"baseline_avg_portcalls_per_day": 28.9,
"ratio_vs_baseline": 0.89
}
}
],
"global_summary": {
"ports_high_congestion": 0,
"ports_moderate": 1,
"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": "822ee340-53aa-47fb-bc43-9d222a932fa1"
}
}
You will typically read these fields:
- data.results[0].congestion: the level label to present to users.
- data.results[0].metrics.ratio_vs_baseline: numerical ratio of the recent 7-day average of port calls versus the 90-day baseline.
- data.results[0].as_of: ISO date string for the reading (updates daily, but lagged ~6–7 days).
Important context: congestion is computed from AIS-derived port-call counts and is not a live vessel wait-time measure. The data is lagged and represents a 7-day signal against a 90-day baseline.
PHP implementation guide
The following PHP script calls the endpoint, validates the envelope, extracts congestion, ratio_vs_baseline, and as_of, and provides safe defaults. It also shows how to handle HTTP failures and missing fields gracefully.
PHP cURL example (ready to paste)
<?php
$apiUrl = 'https://freightpulsehq.com/api/v1/port-congestion?port=CNQDG';
$apiKey = 'YOUR_API_KEY'; // use guest (no key) for up to 100 requests/day per IP
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: ' . $apiKey,
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
$responseBody = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrNo = curl_errno($ch);
$curlErr = curl_error($ch);
curl_close($ch);
if ($curlErrNo !== 0) {
http_response_code(502);
echo json_encode([
'error' => 'upstream_transport_error',
'detail' => $curlErr
]);
exit;
}
if ($httpCode < 200 || $httpCode >= 300) {
http_response_code(502);
echo json_encode([
'error' => 'upstream_http_' . $httpCode,
'detail' => 'Unexpected status from FreightPulse'
]);
exit;
}
$decoded = json_decode($responseBody, true);
if (!is_array($decoded)) {
http_response_code(502);
echo json_encode([
'error' => 'invalid_json',
'detail' => 'Could not parse FreightPulse response'
]);
exit;
}
if (!isset($decoded['success']) || $decoded['success'] !== true || !isset($decoded['data'])) {
http_response_code(502);
echo json_encode([
'error' => 'bad_envelope',
'detail' => 'Missing success=true or data'
]);
exit;
}
$data = $decoded['data'];
$results = isset($data['results']) && is_array($data['results']) ? $data['results'] : [];
$first = count($results) > 0 ? $results[0] : [];
$port = isset($first['port']) ? $first['port'] : 'Qingdao Port';
$asOf = isset($first['as_of']) ? $first['as_of'] : null;
$congestion = isset($first['congestion']) ? $first['congestion'] : null;
$ratio = null;
if (isset($first['metrics']) && isset($first['metrics']['ratio_vs_baseline'])) {
$ratio = $first['metrics']['ratio_vs_baseline'];
}
// Minimal payload your app can display or store
$output = [
'port' => $port,
'as_of' => $asOf, // ISO date; updates daily but is lagged ~6-7 days
'congestion' => $congestion, // e.g., "moderate"
'ratio_vs_baseline' => $ratio // numeric, e.g., 0.89
];
// Example: cache control headers for downstream clients
header('Content-Type: application/json');
// Congestion is not real-time; permit short client-side caching
header('Cache-Control: public, max-age=900');
echo json_encode($output);
Notes for production hardening (PHP)
- Timeouts: keep low connect timeout (e.g., 5s) and reasonable overall timeout (10–15s).
- Retries: retry on transient network failures (timeouts, 5xx). Add jitter between attempts.
- Caching: because data is lagged and updates daily, a 15-minute in-memory cache per port is usually sufficient.
- Validation: always check success and the presence of data.results before reading fields.
- Guest usage: if you run without a key, budget for the 100 requests/day per IP limit and consider server-side caching to stay under quota.
Minimal Python verification script
Use this to quickly compare values from your server against a local run. It fetches the same endpoint and prints congestion, ratio_vs_baseline, and as_of.
import json
import sys
import urllib.request
API_URL = "https://freightpulsehq.com/api/v1/port-congestion?port=CNQDG"
API_KEY = "YOUR_API_KEY" # omit to use guest (100 requests/day per IP)
req = urllib.request.Request(API_URL, headers={
"X-API-Key": API_KEY,
"Accept": "application/json"
})
try:
with urllib.request.urlopen(req, timeout=15) as resp:
body = resp.read().decode("utf-8")
data = json.loads(body)
except Exception as e:
print("transport_error:", e, file=sys.stderr)
sys.exit(2)
if not isinstance(data, dict) or not data.get("success") or "data" not in data:
print("bad_envelope", file=sys.stderr)
sys.exit(2)
results = data["data"].get("results") or []
first = results[0] if results else {}
congestion = first.get("congestion")
as_of = first.get("as_of")
ratio = None
metrics = first.get("metrics") or {}
if "ratio_vs_baseline" in metrics:
ratio = metrics["ratio_vs_baseline"]
print(json.dumps({
"port": first.get("port", "Qingdao Port"),
"as_of": as_of,
"congestion": congestion,
"ratio_vs_baseline": ratio
}, ensure_ascii=False))
Field handling and display guidance
- as_of: an ISO date string indicating the date of the reading. Expect daily updates with a ~6–7 day lag.
- congestion: a label such as low, moderate, or high derived from the 7-day average of port calls vs a 90-day baseline.
- ratio_vs_baseline: a float; values below 1.0 generally reflect fewer recent port calls vs the baseline.
- Units: portcalls are counts/day; ratio is unitless. Do not present this as live vessel wait times.
Operational considerations
- Lag and methodology: the data is sourced from IMF PortWatch using AIS data and reflects a 7-day average vs a 90-day baseline, not live AIS anchorage waits.
- Scheduling: a daily pull is often sufficient. If you poll more frequently, cache results for several minutes to reduce duplicate calls.
- Error surfaces: implement fallbacks if the envelope is missing or success is false; show last cached good value and its as_of date so users know its currency.
- Change detection: alert on label transitions (e.g., low → moderate) or significant ratio deltas (e.g., absolute change ≥ 0.05) instead of every minor fluctuation.
- Localization: if you present the date to non-technical users, keep the original as_of ISO string alongside any localized display.
End-to-end test checklist
- Authentication header present: X-API-Key with either YOUR_API_KEY or omitted for guest usage.
- HTTP 200 response with application/json content-type.
- Parsed envelope: success == true and data exists.
- results array non-empty; handle empty arrays without throwing.
- Extracted fields congestion, metrics.ratio_vs_baseline, and as_of are present or safely null with defaults.
- Caching works and respects your chosen TTL; cache is invalidated daily.
- Monitoring alarms on transport failures, 5xx status codes, or JSON parse errors.
FAQ
Is this congestion data real-time AIS?
No. It is derived from IMF PortWatch and uses AIS to compute a 7-day average versus a 90-day baseline. Expect a ~6–7 day lag.
Which fields should I store to compare trend changes?
Store as_of, congestion, and metrics.ratio_vs_baseline. These allow you to detect label changes and quantify shifts against the baseline.
Can I call the endpoint without an API key?
Yes, as a guest with 100 requests/day per IP. For higher volume or server-side jobs, add X-API-Key: YOUR_API_KEY.
How should I display these values to non-technical users?
Show the congestion label prominently, include the as_of date, and optionally display the ratio as a secondary metric. Clarify that it is not a live wait-time estimate.
What retry and caching policy makes sense?
Retry on timeouts or 5xx with backoff, and cache responses for at least several minutes. Because the data updates daily and is lagged, short-lived caching improves reliability without sacrificing freshness.
Ready to wire Qingdao (CNQDG) congestion into your app? Create an API key and start testing now: Register. For schema references and additional details, visit the Documentation.