Home → Blog

You need to programmatically check current congestion at Guangzhou (Nansha) (CNGGZ) and surface it inside your TMS, pricing engine, or internal dashboard. By the end of this post, you’ll have a working PHP integration to call the FreightPulse congestion endpoint for CNGGZ, interpret the response correctly, and cache it safely, plus a JavaScript example you can adapt for front-end or serverless use.

What this API returns for Guangzhou (Nansha) and how to read it

The FreightPulse congestion endpoint returns an envelope with success and data keys. For a port-specific lookup, data.results is an array (typically one element when filtered by a single port), and the congestion assessment is derived from IMF PortWatch AIS aggregates, lagged by roughly 6–7 days. It is not a live AIS wait-time feed. The congestion classification reflects the 7-day average port calls versus a 90-day baseline for the same port.

Illustration: Guangzhou (Nansha) (CNGGZ) Congestion

Use the documentation to confirm general behaviors and response schemas or any optional metadata: Documentation.

Quick start: cURL you can paste into a terminal

This request targets Guangzhou (Nansha) using its port code CNGGZ. The X-API-Key header accepts YOUR_API_KEY if you have one; omit the header for guest mode with the daily per-IP cap.

Live FreightPulse congestion for CNGGZ.
Source: live

cURL:

curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/port-congestion?port=CNGGZ"

JSON:

{
    "success": true,
    "data": {
        "results": [
            {
                "port": "Guangzhou (Nansha)",
                "locode": "CN GGZ",
                "country": "China",
                "congestion": "moderate",
                "as_of": "2026-10-23",
                "metrics": {
                    "recent_avg_portcalls_per_day": 27.9,
                    "baseline_avg_portcalls_per_day": 28,
                    "ratio_vs_baseline": 0.99
                }
            }
        ],
        "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": "fe7406bb-5ca3-4b66-9115-9ede14b339a1"
    }
}

What to use in your app:

PHP guide: calling, validating, and caching CNGGZ congestion

This PHP section shows how to call the endpoint, guard against partial responses, normalize values, and implement lightweight caching so you don’t burn through your guest quota (100/day/IP) or key-based plan. You can embed this in Laravel, Symfony, a WordPress plugin, or a standalone PHP worker.

1) Minimal PHP client with error handling

The example uses ext-curl, decodes JSON into associative arrays, and checks required fields before returning a typed result.

<?php
declare(strict_types=1);

/**
* Fetch congestion for Guangzhou (Nansha) (CNGGZ) from FreightPulse.
* Returns an array with keys: congestion, ratio_vs_baseline, as_of, raw.
* Throws RuntimeException on transport or schema errors.
*/
function fetchCnggzCongestion(string $apiKey = ''): array
{
$url = 'https://freightpulsehq.com/api/v1/port-congestion?port=CNGGZ';

$ch = curl_init($url);
if ($ch === false) {
throw new RuntimeException('Failed to initialize cURL');
}

$headers = [];
if ($apiKey !== '') {
// Auth via X-API-Key header. Guests can omit this (100 requests/day per IP).
$headers[] = 'X-API-Key: ' . $apiKey;
}

curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => $headers,
]);

$responseBody = curl_exec($ch);
$errno = curl_errno($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($errno !== 0) {
throw new RuntimeException('cURL error: ' . $errno);
}
if ($status < 200 || $status >= 300) {
throw new RuntimeException('HTTP status ' . $status . ' from FreightPulse');
}
if (!is_string($responseBody) || $responseBody === '') {
throw new RuntimeException('Empty response body');
}

$decoded = json_decode($responseBody, true);
if (!is_array($decoded) || !isset($decoded['success']) || $decoded['success'] !== true) {
throw new RuntimeException('Invalid envelope: success !== true');
}

if (!isset($decoded['data']['results']) || !is_array($decoded['data']['results']) || count($decoded['data']['results']) === 0) {
throw new RuntimeException('No results for CNGGZ');
}

$item = $decoded['data']['results'][0];

// Validate important fields before returning.
$congestion = isset($item['congestion']) ? (string)$item['congestion'] : null;
$asOf = isset($item['as_of']) ? (string)$item['as_of'] : null;

$ratio = null;
if (isset($item['metrics']) && is_array($item['metrics']) && isset($item['metrics']['ratio_vs_baseline'])) {
$ratio = (float)$item['metrics']['ratio_vs_baseline'];
}

if ($congestion === null || $asOf === null || $ratio === null) {
throw new RuntimeException('Missing required fields (congestion, ratio_vs_baseline, as_of)');
}

return [
'congestion' => $congestion,
'ratio_vs_baseline' => $ratio,
'as_of' => $asOf,
'raw' => $decoded, // keep the original payload if you need more fields
];
}

2) Simple file cache to avoid repeated calls

Given the dataset updates daily with a ~6–7 day lag, caching per port for 12–24 hours is reasonable. Below is a minimalistic cache using a temp directory; swap this with Redis or your framework cache in production.

<?php
declare(strict_types=1);

function getCnggzWithCache(string $apiKey = '', int $ttlSeconds = 43200): array
{
$cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'fp_cache';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0777, true);
}
$cacheFile = $cacheDir . DIRECTORY_SEPARATOR . 'cnggz_congestion.json';

// Return cached if fresh.
if (file_exists($cacheFile)) {
$age = time() - filemtime($cacheFile);
if ($age < $ttlSeconds) {
$cached = file_get_contents($cacheFile);
if (is_string($cached) && $cached !== '') {
$data = json_decode($cached, true);
if (is_array($data)) {
return $data;
}
}
}
}

$fresh = fetchCnggzCongestion($apiKey);
file_put_contents($cacheFile, json_encode($fresh, JSON_UNESCAPED_SLASHES));
return $fresh;
}

// Example usage:
try {
// Replace with your real key if available; pass empty string for guest mode.
$result = getCnggzWithCache('YOUR_API_KEY', 43200);
// Now you can render values:
// $result['congestion'], $result['ratio_vs_baseline'], $result['as_of']
} catch (RuntimeException $e) {
// Decide how to handle errors: log, fallback, or display a neutral state in UI.
error_log('FreightPulse error: ' . $e->getMessage());
}

3) Mapping the congestion label to UI badges

Most dashboards render a color-coded badge from the congestion label. Keep the mapping straightforward and ensure it degrades gracefully if the label is absent (e.g., show “N/A”).

<?php
function congestionBadge(string $label): array
{
$normalized = strtolower(trim($label));
if ($normalized === 'high') {
return ['text' => 'High', 'color' => '#b91c1c'];
}
if ($normalized === 'moderate') {
return ['text' => 'Moderate', 'color' => '#d97706'];
}
if ($normalized === 'low') {
return ['text' => 'Low', 'color' => '#065f46'];
}
return ['text' => 'N/A', 'color' => '#374151'];
}

// Render example:
$badge = congestionBadge($result['congestion']);
echo '<span style="background:' . htmlspecialchars($badge['color']) . ';color:#fff;padding:2px 6px;border-radius:4px;">'
. htmlspecialchars($badge['text']) . '</span>';

4) Notes on units, dates, and interpretation

Front-end or serverless: JavaScript fetch example

This is a minimal JavaScript example. For browser usage, configure your backend to proxy requests if CORS or secret management is a concern. For Node.js or serverless runtimes, you can send the X-API-Key header directly.

// Node.js 18+ or modern browsers (with a secure backend proxy for the key).
async function getCnggzCongestion(apiKey) {
const url = 'https://freightpulsehq.com/api/v1/port-congestion?port=CNGGZ';
const headers = {};
if (apiKey && apiKey.length > 0) {
headers['X-API-Key'] = apiKey;
}

const res = await fetch(url, { headers, method: 'GET' });
if (!res.ok) {
throw new Error('HTTP ' + res.status + ' from FreightPulse');
}
const json = await res.json();
if (!json || json.success !== true) {
throw new Error('Invalid envelope: success !== true');
}
const results = json.data && json.data.results ? json.data.results : [];
if (!Array.isArray(results) || results.length === 0) {
throw new Error('No results for CNGGZ');
}
const item = results[0];
const congestion = item.congestion;
const asOf = item.as_of;
const ratio = item.metrics && typeof item.metrics.ratio_vs_baseline !== 'undefined'
? item.metrics.ratio_vs_baseline
: null;

if (!congestion || !asOf || ratio === null) {
throw new Error('Missing required fields (congestion, ratio_vs_baseline, as_of)');
}

return { congestion, ratio_vs_baseline: ratio, as_of: asOf, raw: json };
}

// Example:
getCnggzCongestion('YOUR_API_KEY')
.then(r => {
console.log('CNGGZ congestion:', r.congestion);
console.log('Ratio vs baseline:', r.ratio_vs_baseline);
console.log('As of:', r.as_of);
})
.catch(err => {
console.error('FreightPulse error:', err.message);
});

Production guidance: quotas, retries, and display rules

When deploying this for Guangzhou (Nansha), a few practical details can prevent confusing UI states and unnecessary load.

Validations you should keep in place

Because the envelope is consistent but individual readings can be absent in edge cases, keep schema checks and defaults:

How to integrate into pricing and ETD workflows without misleading users

Because the congestion metric is based on port-calls activity and not anchorage wait times, avoid suggesting precise queue durations or vessel ETAs that imply real-time AIS. The safest usage patterns for Guangzhou (Nansha):

Security and deployment notes

If you render congestion on public pages, keep your API key out of client-side code. Use a backend proxy and short-lived caches. For internal tools, lock down the key with environment variables and role-based access. Log request_id from the meta envelope in error traces to make support interactions easier.

FAQ

Is the CNGGZ congestion data real-time?
No. It is derived from IMF PortWatch aggregates and lags by roughly 6–7 days, updating daily. It should not be shown as live AIS wait-time.

Which fields do I need to show a simple congestion widget?
congestion, metrics.ratio_vs_baseline, and as_of. Optionally include recent_avg_portcalls_per_day and baseline_avg_portcalls_per_day for context.

Can I call this endpoint without an API key?
Yes. Guests can call it with a per-IP limit of 100 requests per day. For higher throughput or consistent service behind shared IPs, use an API key via the X-API-Key header.

What cache duration is reasonable?
Between 12 and 24 hours per port is typical, since the dataset updates daily and is already lagged compared to real time.

Can I use the congestion ratio as a price or wait-time proxy?
No. It is an activity ratio (7-day average port calls vs 90-day baseline), not a fare, rate, or a direct measure of anchorage or berth waiting times.

Ready to build this into your ops stack and keep Guangzhou (Nansha) in view alongside other ports? Create your account and get an API key here: Register. If you need deeper field-by-field references while coding, keep this open: Documentation.