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.
API facts you need before coding
- 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: Ningbo (CNNBO)
- curl: curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/port-congestion?port=CNNBO"
- No official congestion JSON is published for CNNBO. Show the curl and field names (congestion, ratio_vs_baseline, as_of). Do not reuse the USLAX 0.85 fixture.
Field names you will care about when data is available
- congestion: high-level congestion indicator for the specific port.
- ratio_vs_baseline: 7-day average port calls versus the 90-day baseline (unitless ratio).
- as_of: timestamp of the data window end, reflecting the ~6–7 day lag (not real-time).
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:
- data.results[0].available is false for Ningbo (CNNBO) right now. Your application should handle this gracefully by showing a message like “congestion data currently unavailable” rather than failing.
- When the dataset becomes available, you will look for congestion, ratio_vs_baseline, and as_of within the port-level record. Do not assume they are always present; check that available is true first and then access them if present.
- meta.source and meta.data_lag_days confirm that the series is built from IMF PortWatch and is lagged by roughly 6–7 days, not live AIS wait times.
PHP implementation: CNNBO congestion lookup
The PHP code below does three things:
- Calls the CNNBO endpoint with the required X-API-Key header.
- Safely parses the envelope and evaluates the availability flag for Ningbo.
- 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
- Scheduling dashboards: add a CNNBO congestion line item with either the three fields (when available) or a clear “data currently unavailable” badge.
- Inbound EDI/ETL checks: emit a soft warning if congestion is unavailable and recheck after your cache TTL elapses.
- API layer: expose a lightweight JSON endpoint that forwards the parsed result to internal clients.
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.
- Data timeliness: The congestion series is built from IMF PortWatch and is lagged by approximately 6–7 days. Treat as historical context, not live AIS berth or anchorage wait times.
- Availability variance: As currently shown for CNNBO, congestion data may be temporarily unavailable. This is a normal condition; cache the unavailability state for a short TTL and retry later.
- Caching: Since updates are daily and lagged, a cache TTL of 6–12 hours is reasonable. Avoid re-polling more frequently than your operational needs.
- Authentication: Use the X-API-Key header with YOUR_API_KEY. Guests without a key have a 100 requests/day per IP budget; plan your polling and caching to stay within this cap if you are not using an API key.
- Error handling: Distinguish between transport errors (HTTP, timeouts) and “available: false” responses. Only the former should trigger alerts.
- Schema checks: Always verify the envelope (success, data) and the existence of data.results before dereferencing fields.
- Field presence: Only reference congestion, ratio_vs_baseline, and as_of if available is true and those keys exist. Do not coerce missing values into zeros.
- Timestamps and time zone: When present, as_of should be treated as a standard timestamp. Since the data is lagged and aggregated, align it with your reporting time zone consistently and label it clearly in your UI.
- No price quoting: The congestion endpoint is not a freight rate feed. Do not present any USD fare as “today’s price” based on this endpoint.
End-to-end flow for a CNNBO widget
- On startup, load your last cached CNNBO record (if any). If it is younger than your TTL (for example, 6 hours), render it immediately.
- If the cache is stale, call the endpoint once using your X-API-Key header and parse the envelope.
- If available is false, render “Data unavailable for CNNBO” and schedule a refresh after the TTL. Log the reason field for observability.
- 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.
- Persist the normalized record with a write timestamp and the API request_id (from meta) if you need auditability.
Testing checklist
- Connectivity: Verify the curl request works from your deployment environment and that outbound HTTPS to freightpulsehq.com is permitted.
- Envelope parsing: Unit-test cases for success true/false, missing data, and empty results arrays.
- Unavailability handling: Simulate available false and ensure your UI shows a stable message without stack traces or retries loops.
- Field guards: Test that your code survives when congestion, ratio_vs_baseline, or as_of are absent even when available is true.
- Rate-limiting: If operating as a guest, assert aggregate calls per IP don’t exceed 100/day. Use caching to keep far below this ceiling.
Where to go next
- See all parameter and envelope details in the Documentation.
- Create an account to get a dedicated API key and higher reliability for your CNNBO checks: Register.
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.