Home → Blog

You need to embed Antwerp (BEANR) port congestion into a PHP workflow and return a simple status your ops team can trust. By the end of this guide, you’ll query FreightPulse for Antwerp, parse congestion fields relevant to planners, and expose a safe, cacheable PHP function you can drop into a dashboard or webhook handler.

What you’ll build and how it behaves

This article shows how to:

Illustration: Antwerp (BEANR) Congestion

Everything here stays specific to Antwerp (BEANR). Links to registration and docs are included so you can move from prototype to production with minimal friction.

How FreightPulse models Antwerp congestion

FreightPulse sources port activity and congestion metrics from IMF PortWatch using UN Global Platform AIS data. The signal is lagged by roughly 6–7 days and is updated daily. It is not a live representation of anchorage queue lengths or berth-level wait times.

Core facts you’ll rely on:

For planning and user messaging, clarify that these values are a 7-day average with a ~6–7 day reporting lag. Do not present them as a same-day live snapshot, and avoid implying a definitive wait-time metric.

Authentication and request shape

Send your key in the X-API-Key header. If you don’t have a key, you may still test as a guest with an IP-based limit (100 requests/day per IP). The Antwerp endpoint returns an envelope with success and data at the top level, and a nested meta for methodology and source transparency.

Key points to implement:

Quick-start cURL for Antwerp (BEANR)

Run this command to retrieve Antwerp congestion with the required header and port code:

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

Response structure and the exact fields you’ll use

Below is the official JSON for the Antwerp request. You’ll read congestion (categorical), metrics.ratio_vs_baseline (numeric ratio), and as_of (date). The ratio is the 7-day average vs a 90-day baseline; a value near 1.0 means activity is close to typical. Dates are provided as YYYY-MM-DD; update cadence is daily with ~6–7 days lag.

{
    "success": true,
    "data": {
        "results": [
            {
                "port": "Antwerp",
                "locode": "BE ANR",
                "country": "Belgium",
                "activity": "moderate",
                "congestion": "moderate",
                "as_of": "2026-10-30",
                "metrics": {
                    "recent_avg_portcalls_per_day": 49,
                    "baseline_avg_portcalls_per_day": 52.9,
                    "ratio_vs_baseline": 0.93
                }
            }
        ],
        "global_summary": {
            "ports_high_activity": 0,
            "ports_moderate_activity": 1,
            "ports_low_activity": 0,
            "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": "activity is port-call volume, not congestion: last 7 days' avg calls vs a 90-day baseline for the same port. low means fewer calls than usual, which can be a closure or ships waiting offshore. It does not measure vessel wait or anchorage time. congestion is a deprecated alias of activity."
        }
    },
    "meta": {
        "request_id": "cdefa38a-968d-4da4-a8f2-c22f32952ec6"
    }
}

Fields to map into your application:

Additional context useful for dashboards:

PHP implementation: production-ready Antwerp lookup

The goal is a single function that returns a normalized record you can print in a view, feed into a cron job, or ship to a webhook. It validates the envelope, extracts only the Antwerp row, and includes basic resilience.

Minimal PHP using ext-curl

<?php
function fetchAntwerpCongestion($apiKey) {
$url = "https://freightpulsehq.com/api/v1/port-congestion?port=BEANR";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . $apiKey,
"Accept: application/json"
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);

$body = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($err) {
throw new RuntimeException("cURL error: " . $err);
}
if ($code < 200 || $code >= 300) {
throw new RuntimeException("HTTP error: " . $code . " body=" . substr((string)$body, 0, 500));
}

$json = json_decode($body, true);
if (!is_array($json) || empty($json["success"]) || empty($json["data"]["results"])) {
throw new RuntimeException("Malformed response or unsuccessful flag");
}

$row = $json["data"]["results"][0];

return [
"port" => isset($row["port"]) ? $row["port"] : null,
"locode" => isset($row["locode"]) ? $row["locode"] : null,
"country" => isset($row["country"]) ? $row["country"] : null,
"congestion" => isset($row["congestion"]) ? $row["congestion"] : null,
"as_of" => isset($row["as_of"]) ? $row["as_of"] : null,
"ratio_vs_baseline" => isset($row["metrics"]["ratio_vs_baseline"]) ? $row["metrics"]["ratio_vs_baseline"] : null,
"recent_avg" => isset($row["metrics"]["recent_avg_portcalls_per_day"]) ? $row["metrics"]["recent_avg_portcalls_per_day"] : null,
"baseline_avg" => isset($row["metrics"]["baseline_avg_portcalls_per_day"]) ? $row["metrics"]["baseline_avg_portcalls_per_day"] : null
];
}

// Example usage:
try {
$result = fetchAntwerpCongestion("YOUR_API_KEY");
echo "Antwerp congestion: " . $result["congestion"] . " (ratio " . $result["ratio_vs_baseline"] . ") as of " . $result["as_of"] . PHP_EOL;
} catch (Exception $e) {
error_log("Failed to fetch Antwerp congestion: " . $e->getMessage());
// Fallback: show cached value or a neutral message in UI
}

Production touches: retry with backoff and simple 24h cache

Because the dataset updates daily and is lagged, you can safely cache for 12–24 hours without losing fidelity. This also helps stay under guest/IP limits if you haven’t provisioned a key yet.

<?php
function httpGetWithRetry($url, $headers, $maxAttempts = 3) {
$attempt = 0;
$backoff = 1.0; // seconds

while ($attempt < $maxAttempts) {
$attempt++;

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);

$body = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!$err && $code >= 200 && $code < 300) {
return $body;
}

// For 4xx other than 429, don't retry
if ($code >= 400 && $code != 429) {
throw new RuntimeException("HTTP " . $code . ": " . substr((string)$body, 0, 500));
}

// Retry 429/5xx with exponential backoff
usleep((int)($backoff * 1000000));
$backoff *= 2.0;
}

throw new RuntimeException("Failed after " . $maxAttempts . " attempts");
}

function getCachePath() {
$dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "freightpulse_cache";
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
return $dir . DIRECTORY_SEPARATOR . "beanr_congestion.json";
}

function fetchAntwerpCongestionCached($apiKey, $ttlSeconds = 86400) {
$cacheFile = getCachePath();
if (file_exists($cacheFile)) {
$age = time() - filemtime($cacheFile);
if ($age <= $ttlSeconds) {
$cached = file_get_contents($cacheFile);
$decoded = json_decode($cached, true);
if (is_array($decoded)) {
return $decoded;
}
}
}

$url = "https://freightpulsehq.com/api/v1/port-congestion?port=BEANR";
$headers = [
"X-API-Key: " . $apiKey,
"Accept: application/json"
];
$body = httpGetWithRetry($url, $headers, 3);

$json = json_decode($body, true);
if (!is_array($json) || empty($json["success"]) || empty($json["data"]["results"])) {
throw new RuntimeException("Malformed response or unsuccessful flag");
}

$row = $json["data"]["results"][0];

$normalized = [
"port" => isset($row["port"]) ? $row["port"] : null,
"locode" => isset($row["locode"]) ? $row["locode"] : null,
"country" => isset($row["country"]) ? $row["country"] : null,
"congestion" => isset($row["congestion"]) ? $row["congestion"] : null,
"as_of" => isset($row["as_of"]) ? $row["as_of"] : null,
"ratio_vs_baseline" => isset($row["metrics"]["ratio_vs_baseline"]) ? $row["metrics"]["ratio_vs_baseline"] : null,
"recent_avg" => isset($row["metrics"]["recent_avg_portcalls_per_day"]) ? $row["metrics"]["recent_avg_portcalls_per_day"] : null,
"baseline_avg" => isset($row["metrics"]["baseline_avg_portcalls_per_day"]) ? $row["metrics"]["baseline_avg_portcalls_per_day"] : null,
// Optional metadata for UI notes:
"data_lag" => isset($json["data"]["meta"]["data_lag_days"]) ? $json["data"]["meta"]["data_lag_days"] : null,
"source" => isset($json["data"]["meta"]["source"]) ? $json["data"]["meta"]["source"] : null
];

// Write-through cache
@file_put_contents($cacheFile, json_encode($normalized));

return $normalized;
}

// Example usage:
try {
$beanr = fetchAntwerpCongestionCached("YOUR_API_KEY", 86400); // 24h TTL
echo "Antwerp (BEANR): " . $beanr["congestion"] . " activity, "
. "ratio " . $beanr["ratio_vs_baseline"]
. ", as of " . $beanr["as_of"] . PHP_EOL;
// Display a footnote like: "Activity is a 7-day average vs 90-day baseline; data lag ~6–7 days (IMF PortWatch)."
} catch (Exception $e) {
error_log("BEANR fetch failed: " . $e->getMessage());
}

A small JavaScript example for testing and CI

If you want a quick Node.js check (e.g., in CI to verify the endpoint and basic shape), this snippet fetches Antwerp and logs the three fields you’ll surface in your UI.

import fetch from "node-fetch";

async function getAntwerp() {
const res = await fetch("https://freightpulsehq.com/api/v1/port-congestion?port=BEANR", {
headers: {
"X-API-Key": "YOUR_API_KEY",
"Accept": "application/json"
},
timeout: 15000
});

if (!res.ok) {
const text = await res.text();
throw new Error(`HTTP ${res.status}: ${text.slice(0, 500)}`);
}

const json = await res.json();
if (!json || json.success !== true || !json.data || !Array.isArray(json.data.results)) {
throw new Error("Malformed response");
}

const row = json.data.results[0];
const congestion = row.congestion;
const asOf = row.as_of;
const ratio = row.metrics?.ratio_vs_baseline;

console.log(`Antwerp congestion=${congestion}, ratio=${ratio}, as_of=${asOf}`);
}

getAntwerp().catch(err => {
console.error("Fetch error:", err.message);
process.exitCode = 1;
});

How to communicate Antwerp (BEANR) results in your UI

Keep wording precise so teams don’t misinterpret the signal:

If you show ratio_vs_baseline, present it as a relative index (unitless). For example, “0.93 of baseline” or “-7% vs baseline” if you compute the delta. If you do compute a delta, do it client-side and clearly mark it as a derived value, not an API field.

Operational considerations: limits, retries, and caching

Guest access without a key is capped at 100 requests/day per IP. If your service fans out across multiple nodes or functions, enforce app-level caching to avoid unexpected throttling. The data updates daily with a ~6–7 day lag, so a 12–24 hour TTL is typically safe and keeps results consistent across users.

Testing your integration

For unit tests, snapshot the normalized PHP array and assert keys exist: congestion, ratio_vs_baseline, as_of. For integration tests, you can hit the live endpoint with a real key in a secure CI secret store, but write test logic to accept a lagged date or skip date-specific assertions entirely. If you must run without secrets, mock the HTTP layer and replay the official sample JSON.

Common pitfalls and how to avoid them

Full Antwerp workflow checklist

FAQ

Is this live AIS queue data?
No. It is a 7-day average of port calls versus a 90-day baseline, sourced from IMF PortWatch with ~6–7 days of lag. It does not measure anchorage or berth wait times.

How often should I refresh Antwerp (BEANR) data?
Daily is appropriate. Cache results 12–24 hours to reduce load and provide consistency to users.

Which fields should I show to planners?
Show congestion (categorical), ratio_vs_baseline (numeric index), and as_of (date). Optionally include recent and baseline port-call averages for detail views.

What’s the correct way to authenticate?
Send your key in the X-API-Key header. Without a key, you’re limited to 100 requests/day per IP.

Can I quote a USD freight rate alongside this?
Do not present a current freight-rate USD fare as today’s price in this context. This endpoint provides activity/congestion, not price discovery.

Ready to integrate Antwerp (BEANR) congestion into your stack? Create an API key and start shipping with a daily, lag-aware signal. Register for access, then read the endpoint details in the Documentation.