Your ops team needs a reliable way to check congestion for Shekou (Shenzhen) (Shekou) and wire it into a PHP workflow without guessing from AIS maps. By the end of this guide, you’ll authenticate to the FreightPulse port-congestion endpoint, fetch the Shekou signal, parse congestion fields you actually need (congestion, ratio_vs_baseline, as_of), and productionize it with caching and error handling.
What this integration returns and what it is not
FreightPulse’s Shekou congestion is derived from IMF PortWatch and updated daily with a ~6–7 day lag. It compares the last 7 days’ average port calls against a 90-day baseline. This is not live AIS anchorage wait time and should not be treated as today’s real-time queue length.
The API uses a simple envelope structure: {"success":true,"data":{...}}. You authenticate with a header X-API-Key: YOUR_API_KEY. Guests without a key can call up to 100 requests per day per IP.
- 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.
- Port: Shekou (Shenzhen) (Shekou)
- curl: curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/port-congestion?port=Shekou"
If you need plan specifics or additional endpoints, see the Documentation.
Official sample: request and JSON you will parse
Live FreightPulse congestion for Shekou.
Source: live
cURL:
curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/port-congestion?port=Shekou"
JSON:
{
"success": true,
"data": {
"results": [
{
"port": "Shekou (Shenzhen)",
"locode": null,
"country": "China",
"congestion": "moderate",
"as_of": "2026-10-23",
"metrics": {
"recent_avg_portcalls_per_day": 8.4,
"baseline_avg_portcalls_per_day": 8.5,
"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": "0bec743b-50a7-4346-84b3-ff6d2704d4b6"
}
}
You will primarily consume: - data.results[0].congestion: categorical label such as low, moderate, high. - data.results[0].as_of: ISO date for the reference day (lagged). - data.results[0].metrics.ratio_vs_baseline: numeric ratio vs 90-day baseline. - Optionally, recent_avg_portcalls_per_day and baseline_avg_portcalls_per_day if you display context.
Quick-start: cURL and basic field extraction
Run the cURL exactly as shown above with your key in X-API-Key. For scripts/cron, capture stdout and pipe to jq to extract the three essential fields:
This yields a single line with the as_of date, normalized label, and ratio_vs_baseline you can alert on or persist.
PHP implementation guide (cURL extension)
The snippet below fetches the Shekou congestion in PHP using the cURL extension, validates the envelope, and extracts the fields you actually need. It also shows a minimal cache pattern to avoid redundant calls within the same day.
1) Minimal client with robust parsing
<?php
declare(strict_types=1);
function fetchShekouCongestion(string $apiKey): array {
$url = 'https://freightpulsehq.com/api/v1/port-congestion?port=Shekou';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'X-API-Key: ' . $apiKey,
],
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($body === false) {
throw new RuntimeException('cURL error: ' . $err);
}
if ($httpCode < 200 || $httpCode >= 300) {
throw new RuntimeException('Unexpected HTTP status: ' . $httpCode . ' body=' . substr($body, 0, 500));
}
$json = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!isset($json['success']) || $json['success'] !== true) {
throw new RuntimeException('API indicated failure');
}
if (!isset($json['data']['results'][0])) {
throw new RuntimeException('Missing results in response');
}
$row = $json['data']['results'][0];
// Defensive extraction with defaults
$congestion = $row['congestion'] ?? null;
$asOf = $row['as_of'] ?? null;
$ratio = $row['metrics']['ratio_vs_baseline'] ?? null;
if ($congestion === null || $asOf === null || $ratio === null) {
throw new RuntimeException('Missing required fields: congestion/as_of/ratio_vs_baseline');
}
return [
'as_of' => $asOf, // e.g., 2026-10-23 (UTC date)
'congestion' => $congestion, // low|moderate|high
'ratio_vs_baseline' => (float) $ratio // numeric ratio
];
}
// Example usage:
try {
$apiKey = getenv('FREIGHTPULSE_API_KEY') ?: 'YOUR_API_KEY';
$data = fetchShekouCongestion($apiKey);
printf(
"Shekou congestion: %s (ratio %.2f) as of %s\n",
$data['congestion'],
$data['ratio_vs_baseline'],
$data['as_of']
);
} catch (Throwable $e) {
error_log('FreightPulse error: ' . $e->getMessage());
http_response_code(502);
echo 'Temporarily unavailable';
}
2) Disk caching by as_of date (avoid redundant calls)
Because data is lagged and updates daily, poll at most once per day per port. Cache by as_of to guarantee idempotence across days.
<?php
function getShekouWithDailyCache(string $apiKey, string $cacheDir = '/tmp'): array {
$live = fetchShekouCongestion($apiKey);
$asOf = $live['as_of'];
$cacheFile = rtrim($cacheDir, '/')."/freightpulse_shekou_$asOf.json";
if (file_exists($cacheFile)) {
$cached = json_decode(file_get_contents($cacheFile), true);
if (is_array($cached)) {
return $cached;
}
}
// Write-through cache for the current as_of tick
file_put_contents($cacheFile, json_encode($live, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
return $live;
}
// Example:
$data = getShekouWithDailyCache(getenv('FREIGHTPULSE_API_KEY') ?: 'YOUR_API_KEY');
3) Mapping to internal enums and thresholds
Use the ratio_vs_baseline to support custom thresholds while still displaying the vendor label for transparency.
<?php
function classifyRatio(float $r): string {
if ($r >= 1.20) return 'severe';
if ($r >= 1.05) return 'elevated';
if ($r <= 0.85) return 'below-normal';
return 'near-baseline';
}
$payload = getShekouWithDailyCache(getenv('FREIGHTPULSE_API_KEY') ?: 'YOUR_API_KEY');
$internal = [
'port' => 'Shekou (Shenzhen)',
'vendor_label' => $payload['congestion'],
'ratio' => $payload['ratio_vs_baseline'],
'internal_class' => classifyRatio($payload['ratio_vs_baseline']),
'as_of' => $payload['as_of'],
];
// Persist or render $internal in your dashboard
PHP integration using Guzzle (optional)
If your stack uses Guzzle, here is an alternative client showing the same extraction, plus simple retries on transient 5xx errors.
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
function guzzleShekou(string $apiKey): array {
$client = new Client([
'base_uri' => 'https://freightpulsehq.com',
'timeout' => 15.0,
'headers' => [
'Accept' => 'application/json',
'X-API-Key' => $apiKey,
],
]);
$attempts = 0;
while (true) {
try {
$resp = $client->get('/api/v1/port-congestion', ['query' => ['port' => 'Shekou']]);
$body = (string) $resp->getBody();
$json = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (($json['success'] ?? false) !== true) {
throw new RuntimeException('API success=false');
}
$r = $json['data']['results'][0] ?? null;
if (!$r) {
throw new RuntimeException('Empty results');
}
return [
'as_of' => $r['as_of'],
'congestion' => $r['congestion'],
'ratio_vs_baseline' => (float) ($r['metrics']['ratio_vs_baseline'] ?? NAN),
];
} catch (TransferException $e) {
$attempts++;
if ($attempts >= 3) {
throw $e;
}
usleep(250000 * $attempts); // backoff
continue;
}
}
}
Operational notes: units, time, and polling
- Timestamps and timezone: as_of is an ISO date (YYYY-MM-DD) representing the data’s reference day; treat as UTC for storage and comparisons.
- Units: recent_avg_portcalls_per_day and baseline_avg_portcalls_per_day are counts per day. ratio_vs_baseline is a unitless ratio.
- Caching: cache by as_of and port. Poll daily after your data warehouse loads (e.g., mid-morning UTC) to avoid flapping during refresh windows.
- Backfills: when your cached as_of changes, emit a single event/change-log entry so incident playbooks update once.
- Guest quota: without an API key, the limit is 100 requests/day per IP. Production services should use a key to avoid shared-IP depletion.
Embedding the signal in a PHP/Laravel or Symfony app
In server-rendered pages, resolve the congestion once per request cycle via a service container that reads from your cache layer. For APIs, include a normalized object in your response to front-end clients:
<?php
header('Content-Type: application/json');
$data = getShekouWithDailyCache(getenv('FREIGHTPULSE_API_KEY') ?: 'YOUR_API_KEY');
echo json_encode([
'port' => 'Shekou (Shenzhen) (Shekou)',
'congestion' => $data['congestion'],
'ratio_vs_baseline' => $data['ratio_vs_baseline'],
'as_of' => $data['as_of'],
], JSON_UNESCAPED_SLASHES);
Front ends can map congestion to color scales while also displaying the numeric ratio. Keep the ratio available for alert thresholds even if you primarily surface the label.
JavaScript example (Node.js or browser)
This example fetches the same endpoint and extracts congestion, ratio_vs_baseline, and as_of. In browsers, ensure CORS is permitted by your deployment pattern (e.g., proxy through your backend).
async function fetchShekou() {
const resp = await fetch('https://freightpulsehq.com/api/v1/port-congestion?port=Shekou', {
headers: { 'X-API-Key': 'YOUR_API_KEY', 'Accept': 'application/json' }
});
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
const json = await resp.json();
if (!json.success || !json.data || !json.data.results || !json.data.results[0]) {
throw new Error('Malformed response');
}
const r = json.data.results[0];
return {
as_of: r.as_of,
congestion: r.congestion,
ratio_vs_baseline: r.metrics?.ratio_vs_baseline
};
}
fetchShekou()
.then(d => console.log(`Shekou: ${d.congestion} (ratio ${d.ratio_vs_baseline}) as of ${d.as_of}`))
.catch(err => console.error('FreightPulse error:', err));
Production hardening and troubleshooting
Handle these cases explicitly so your service is resilient:
- Empty results array: return the last cached as_of if present and mark the data as stale in telemetry.
- HTTP 4xx: typically auth or parameter issues. Re-check the X-API-Key header and the exact port=Shekou query value.
- HTTP 5xx or timeouts: retry with jittered backoff; fall back to cache.
- Schema changes: gate parsing behind isset checks and log unknown fields for later adoption.
Do not present the label as live operational wait time. If your users need that, clearly state the lag and methodology in the UI using meta.source and meta.data_lag_days fields from data.meta.
Field-by-field mapping for Shekou (Shenzhen) (Shekou)
- data.results[0].port: string label of the port (“Shekou (Shenzhen)”). Use as a display name if needed.
- data.results[0].country: “China”. Useful for group-level filtering.
- data.results[0].congestion: categorical label you can show directly in dashboards.
- data.results[0].as_of: date string of the latest measurement window (lagged).
- data.results[0].metrics.ratio_vs_baseline: primary numeric for triggers and comparisons.
- data.results[0].metrics.recent_avg_portcalls_per_day and baseline_avg_portcalls_per_day: use to provide context alongside the ratio.
- data.global_summary: aggregate counts across all ports in the response; optional for single-port views.
- data.meta: include methodology and source attribution in tooltips or footnotes.
FAQ
Is this live AIS wait time?
No. It is based on IMF PortWatch and reflects the last 7 days’ average port calls versus a 90-day baseline. Data is updated daily with ~6–7 days of lag.
What header do I use for auth?
Send X-API-Key: YOUR_API_KEY. Without a key, guests are limited to 100 requests/day per IP.
Which fields should I alert on?
Use metrics.ratio_vs_baseline for numeric thresholds and display congestion as the human-readable label. Include as_of in alerts so recipients know the reference date.
How often should I poll?
Once per day is sufficient because the underlying data updates daily and is lagged. Cache by as_of and port to avoid redundant calls.
Can I quote this as a live price or rate?
No. This endpoint reports congestion derived from port-call counts, not freight rates. Do not present it as today’s USD fare.
Ready to ship this to production? Create an API key in minutes and wire up your PHP job now: Register. For parameters, envelopes, and additional notes, see the Documentation.