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:
- Call the Antwerp (BEANR) congestion endpoint with an X-API-Key in PHP.
- Parse the envelope structure (success, data) to extract congestion, ratio_vs_baseline, and as_of.
- Cache results to respect API limits and prevent over-polling.
- Render concise, accurate UI text that avoids implying live AIS wait times.
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:
- Auth: Provide your key via header X-API-Key: YOUR_API_KEY. Guests without a key can make up to 100 requests/day per IP.
- Envelope: All responses wrap fields inside {"success": true, "data": {...}}.
- Methodology: Activity compares the last 7 days of port calls vs a 90-day baseline for the same port. Congestion is a deprecated alias of activity; treat it as categorical activity, not time-in-queue.
- Target port: Antwerp (BEANR).
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:
- HTTP Method: GET
- Header: X-API-Key: YOUR_API_KEY
- Path and query: /api/v1/port-congestion?port=BEANR
- Error handling: Check HTTP codes and the success boolean before parsing data.
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:
- data.results[0].congestion (string): categorical label; treat it as the activity category.
- data.results[0].metrics.ratio_vs_baseline (number): recent/baseline activity ratio; unitless.
- data.results[0].as_of (string, YYYY-MM-DD): most recent date of the 7-day window; daily cadence, ~6–7 days behind wall time.
Additional context useful for dashboards:
- recent_avg_portcalls_per_day and baseline_avg_portcalls_per_day (numbers): helpful for tooltips or detailed views.
- meta.data_lag_days: communicate that values are not live AIS wait times.
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:
- Label: “Antwerp activity: moderate (7-day avg vs 90-day baseline), as of 2026-10-30.”
- Supplementary: “Data lag ~6–7 days. Source: IMF PortWatch via FreightPulse.”
- Avoid: claims about live wait times, anchorage duration, queue counts, or berth delays.
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.
- Timeouts: 10–15 seconds are reasonable for server-side calls.
- Retries: Exponential backoff on 429 and 5xx only; do not retry on 4xx except 429.
- Data freshness: Use the as_of field to annotate UIs and analytics.
- Clock and timezone: Dates are provided as YYYY-MM-DD; treat them as local-to-port calendar dates without needing a timezone conversion for UI labels.
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
- Assuming “congestion” means live queue severity: it’s a categorical activity signal versus baseline, not a wait-time measure.
- Polling too frequently: the feed updates daily. Cache for a day unless your product requires tighter control.
- Dropping the envelope: always check success before parsing data.results.
- Forgetting the X-API-Key header: guests are rate-limited per IP; move to an API key for production.
Full Antwerp workflow checklist
- Build the GET request: /api/v1/port-congestion?port=BEANR with X-API-Key.
- Verify HTTP 2xx and success: true.
- Parse data.results[0]: congestion, metrics.ratio_vs_baseline, as_of.
- Cache normalized fields for 24 hours.
- Display with an “as of” date and a “lagged, daily” note.
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.