Home → Blog

You need a reliable way to programmatically check congestion for Ningbo (CNNBO) and decide whether to alert, reroute, or annotate ETAs. By the end of this guide, you’ll have a production-ready PHP lookup for FreightPulse’s CNNBO congestion endpoint, plus a copy-paste curl test, a minimal JavaScript sample, and precise handling for the case where congestion data is currently unavailable for this port.

What you will build

This post walks you through a focused PHP implementation that queries the FreightPulse congestion endpoint for Ningbo (CNNBO), parses the response, and safely handles the “data unavailable” state. You will also see how to expose three key fields—congestion, ratio_vs_baseline, and as_of—when the dataset is available, and how to design safe fallbacks and caching around the ~6–7 day lag in IMF PortWatch updates.

Illustration: Ningbo (CNNBO) Congestion

API facts you need before coding

Field names you will care about when data is available

Quick test with curl

Run the official curl below to verify connectivity and see the current availability for CNNBO. Replace YOUR_API_KEY with your key if you have one; the endpoint also supports limited guest access per IP.

Live FreightPulse congestion for CNNBO.
Source: live

cURL:

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

Understand the current response shape for CNNBO

At the time of writing, the official response for CNNBO indicates that recent activity data is not available. Copying the exact JSON below ensures you can align your parser with the current envelope and metadata. Your integration must treat this as a normal, expected state and not as an error.

JSON:

{
    "success": true,
    "data": {
        "results": [
            {
                "port": "Ningbo",
                "locode": "CN NBO",
                "country": "China",
                "available": false,
                "reason": "No recent activity data for this port"
            }
        ],
        "global_summary": {
            "ports_high_congestion": 0,
            "ports_moderate": 0,
            "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": "0d063f15-30d8-4089-9c70-f98f4717f4a0"
    }
}

What to read from this:

PHP implementation: CNNBO congestion lookup

The PHP code below does three things:

  1. Calls the CNNBO endpoint with the required X-API-Key header.
  2. Safely parses the envelope and evaluates the availability flag for Ningbo.
  3. Surfaces congestion, ratio_vs_baseline, and as_of when available, and emits a stable “unavailable” message otherwise.

Minimal client (cURL) with robust parsing

<?php
declare(strict_types=1);

function fetchCnnboCongestion(string $apiKey): array {
$url = 'https://freightpulsehq.com/api/v1/port-congestion?port=CNNBO';
$ch = curl_init($url);

$headers = [
'Accept: application/json',
'X-API-Key: ' . $apiKey,
];

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

$raw = curl_exec($ch);
if ($raw === false) {
$err = curl_error($ch);
curl_close($ch);
throw new RuntimeException('HTTP request failed: ' . $err);
}

$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
throw new RuntimeException('Unexpected HTTP status: ' . $status);
}

$json = json_decode($raw, true);
if (!is_array($json)) {
throw new RuntimeException('Invalid JSON from API');
}

// Validate top-level envelope
if (!array_key_exists('success', $json) || $json['success'] !== true) {
throw new RuntimeException('API indicated failure');
}

if (!isset($json['data']) || !is_array($json['data'])) {
throw new RuntimeException('Missing data envelope');
}

$data = $json['data'];
$results = $data['results'] ?? [];

if (!is_array($results) || count($results) === 0) {
// Defensive fallback: treat as unavailable
return [
'available' => false,
'message' => 'No results for CNNBO',
];
}

$portRec = $results[0];

// Current official sample shows "available": false for CNNBO.
$available = $portRec['available'] ?? false;

if (!$available) {
$reason = $portRec['reason'] ?? 'No recent activity data for this port';
return [
'available' => false,
'message' => $reason,
'port' => $portRec['port'] ?? 'Ningbo',
'locode' => $portRec['locode'] ?? 'CN NBO',
'country' => $portRec['country'] ?? 'China',
];
}

// When data is available, prefer these fields if present.
// Only read them if they exist; do not assume presence.
$congestion = $portRec['congestion'] ?? null;
$ratioVsBaseline = $portRec['ratio_vs_baseline'] ?? null;
$asOf = $portRec['as_of'] ?? null;

return [
'available' => true,
'congestion' => $congestion,
'ratio_vs_baseline' => $ratioVsBaseline,
'as_of' => $asOf,
];
}

try {
// Replace with your key. Guest requests are limited per IP.
$result = fetchCnnboCongestion('YOUR_API_KEY');

if (!$result['available']) {
// For UI logs or API responses in your system
echo 'CNNBO congestion: unavailable - ' . ($result['message'] ?? 'No reason provided') . PHP_EOL;
} else {
// Safely display present fields only
$out = [];
if ($result['congestion'] !== null) $out[] = 'congestion=' . $result['congestion'];
if ($result['ratio_vs_baseline'] !== null) $out[] = 'ratio_vs_baseline=' . $result['ratio_vs_baseline'];
if ($result['as_of'] !== null) $out[] = 'as_of=' . $result['as_of'];
echo 'CNNBO congestion available: ' . implode(', ', $out) . PHP_EOL;
}
} catch (Throwable $e) {
// Do not throw raw errors to end users in production
error_log('CNNBO lookup error: ' . $e->getMessage());
echo 'CNNBO congestion: temporarily unavailable' . PHP_EOL;
}

Where to show this in your application

JavaScript example (Node.js)

This example mirrors the same logic, including graceful handling when availability is false and opportunistic reads for congestion, ratio_vs_baseline, and as_of when present.

import fetch from 'node-fetch';

async function fetchCnnboCongestion(apiKey) {
const url = 'https://freightpulsehq.com/api/v1/port-congestion?port=CNNBO';
const res = await fetch(url, {
headers: {
'Accept': 'application/json',
'X-API-Key': apiKey
},
redirect: 'follow'
});

if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}

const json = await res.json();

if (!json || json.success !== true || !json.data) {
throw new Error('Invalid envelope');
}

const results = Array.isArray(json.data.results) ? json.data.results : [];
if (results.length === 0) {
return { available: false, message: 'No results for CNNBO' };
}

const rec = results[0];
const available = !!rec.available;

if (!available) {
return {
available: false,
message: rec.reason || 'No recent activity data for this port',
port: rec.port || 'Ningbo',
locode: rec.locode || 'CN NBO',
country: rec.country || 'China'
};
}

// When present, surface the three key fields
const out = { available: true };
if ('congestion' in rec) out.congestion = rec.congestion;
if ('ratio_vs_baseline' in rec) out.ratio_vs_baseline = rec.ratio_vs_baseline;
if ('as_of' in rec) out.as_of = rec.as_of;

return out;
}

// Example usage
fetchCnnboCongestion('YOUR_API_KEY')
.then(result => {
if (!result.available) {
console.log(`CNNBO congestion: unavailable - ${result.message}`);
} else {
console.log('CNNBO congestion available:', result);
}
})
.catch(err => {
console.error('CNNBO lookup error:', err.message);
});

Operational considerations for CNNBO congestion

These implementation notes will keep your integration stable and predictable in production.

End-to-end flow for a CNNBO widget

  1. On startup, load your last cached CNNBO record (if any). If it is younger than your TTL (for example, 6 hours), render it immediately.
  2. If the cache is stale, call the endpoint once using your X-API-Key header and parse the envelope.
  3. If available is false, render “Data unavailable for CNNBO” and schedule a refresh after the TTL. Log the reason field for observability.
  4. If available is true, surface congestion, ratio_vs_baseline, and as_of if present. Add a small “Data lag ~6–7 days (IMF PortWatch)” note to the UI or tooltip.
  5. Persist the normalized record with a write timestamp and the API request_id (from meta) if you need auditability.

Testing checklist

Where to go next

FAQ

Is the CNNBO congestion feed real-time?
No. It is based on IMF PortWatch with a ~6–7 day lag. It compares the last 7 days’ average port calls to a 90-day baseline and does not measure live vessel wait times.

Why do I see “available: false” for Ningbo?
The endpoint currently reports no recent activity data for this port. This is expected behavior. Implement a clear fallback message and retry on a scheduled basis.

Which fields should I use once data becomes available?
Read congestion, ratio_vs_baseline, and as_of if and only if available is true and those fields exist in the port record.

How should I authenticate?
Send your key in the X-API-Key header. Guests without a key are limited to 100 requests per day per IP, so use caching accordingly.

Can I use this to quote freight rates?
No. This endpoint provides congestion context from IMF PortWatch and should not be used to present or imply a USD freight rate as today’s price.

If you’re ready to deploy the CNNBO congestion lookup in production, get your API key and start integrating today: Register. For full endpoint details and envelope references, see the Documentation.