Home → Blog

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.

Illustration: Qingdao (CNQDG) Congestion

Origin catalog facts you must use

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:

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:

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)

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

Operational considerations

End-to-end test checklist

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.