Home → Blog

You need to programmatically check congestion at the Port of Savannah (USSAV) and turn it into something your app can trust. By the end of this guide, you’ll have a working PHP implementation that calls the FreightPulse USSAV congestion endpoint, handles the lagged IMF PortWatch dataset, and safely degrades when no official data is available.

What you are integrating and what “congestion” means here

The FreightPulse USSAV congestion endpoint returns IMF PortWatch-derived congestion metrics that are lagged by roughly 6–7 days. The signal is a 7‑day average compared against a 90‑day baseline. This is not a live AIS vessel wait-time feed, and you should not treat it as a real‑time linehaul delay. It’s a structured historical indicator you can poll and cache for planning dashboards and risk flags.

Illustration: Savannah (USSAV) Congestion

Responses are wrapped in an envelope of the form {"success":true,"data":{...}}. You authenticate with the X-API-Key header. Guests without a key are allowed 100 requests per day per IP. When data is available, you will read fields like congestion, ratio_vs_baseline, and as_of. If data is not available, handle the empty results and diagnostic fields safely in your code.

Authentication, limits, and the request envelope

Every request should include the X-API-Key header with the value YOUR_API_KEY. Without a key, you can still call the API as a guest; the daily cap is 100 requests per IP. The response envelope uses a top-level success boolean and a data object that contains the results or diagnostics. You should treat success as “the API call worked,” and then inspect data for available vs. not available states.

Refer to the official Documentation for any fields not described here.

Quick cURL you can copy/paste

Use this to smoke‑test connectivity and auth. This is the official sample for USSAV congestion:

Live FreightPulse congestion for USSAV.
Source: live

cURL:

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

JSON:

{
    "success": true,
    "data": {
        "results": [],
        "available": false,
        "reason": "Error querying IMF PortWatch"
    },
    "meta": {
        "request_id": "de784e9d-4b31-428a-8ffa-44479525fb42"
    }
}

Interpretation: the API call succeeded but no official Savannah congestion series is currently available. Build your integration to handle available=false and empty results. Do not try to infer a live wait time from this endpoint.

Field names you will read when data is present

When data is available for USSAV, the results array will contain objects with fields you should parse:

Do not fabricate values. If the results array is empty or available is false, render a clear “data unavailable” state in your UI or logs and retry later according to your cache/refresh policy.

PHP implementation for USSAV congestion

The following PHP guide shows a production‑ready pattern for calling the endpoint, mapping the expected fields, handling empty or unavailable data, and preparing output that you can drop into an API layer or a controller.

PHP: minimal, robust client

<?php
declare(strict_types=1);

final class FreightPulseClient
{
private string $baseUrl = 'https://freightpulsehq.com/api/v1';
private string $apiKey;

public function __construct(string $apiKey)
{
$this->apiKey = $apiKey;
}

/**
* Fetch congestion metrics for Savannah (USSAV).
* Returns a normalized array:
* [
* 'available' => bool,
* 'observations' => [
* [
* 'congestion' => float|null,
* 'ratio_vs_baseline' => float|null,
* 'as_of' => string|null
* ],
* ...
* ],
* 'raw' => array, // original decoded JSON
* ]
*/
public function getSavannahCongestion(): array
{
$url = $this->baseUrl . '/port-congestion?port=USSAV';

$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FAILONERROR => false, // keep body on non-2xx
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . $this->apiKey,
'Accept: application/json',
],
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 10,
]);

$body = curl_exec($ch);
$curlErr = curl_error($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($body === false) {
return [
'available' => false,
'observations' => [],
'raw' => [
'error' => 'curl_exec_failed',
'message' => $curlErr,
'http_status' => $status,
],
];
}

$json = json_decode($body, true);
if (!is_array($json)) {
return [
'available' => false,
'observations' => [],
'raw' => [
'error' => 'invalid_json',
'body' => $body,
'http_status' => $status,
],
];
}

// Expect envelope: {"success":true,"data":{...}}
$data = $json['data'] ?? null;
if (!is_array($data)) {
return [
'available' => false,
'observations' => [],
'raw' => $json,
];
}

$available = (bool)($data['available'] ?? false);
$results = $data['results'] ?? [];

// Normalize observations if present; use nulls defensively
$observations = [];
if (is_array($results)) {
foreach ($results as $row) {
if (!is_array($row)) {
continue;
}
$observations[] = [
'congestion' => isset($row['congestion']) ? (float)$row['congestion'] : null,
'ratio_vs_baseline' => isset($row['ratio_vs_baseline']) ? (float)$row['ratio_vs_baseline'] : null,
'as_of' => isset($row['as_of']) ? (string)$row['as_of'] : null,
];
}
}

return [
'available' => $available,
'observations' => $observations,
'raw' => $json,
];
}
}

// Example usage:
$apiKey = 'YOUR_API_KEY'; // Guests are limited to 100 requests/day per IP without a key
$client = new FreightPulseClient($apiKey);
$response = $client->getSavannahCongestion();

if ($response['available'] && !empty($response['observations'])) {
// Use the most recent observation
$latest = $response['observations'][0];
// Render or store $latest['congestion'], $latest['ratio_vs_baseline'], $latest['as_of']
// Remember: the metric is lagged (~6–7 days) and is a 7‑day average vs a 90‑day baseline.
} else {
// Graceful fallback: no official USSAV series available now
// Consider caching this state for several hours and showing a "data unavailable" badge.
// You can inspect $response['raw'] for a 'reason' field from the API.
error_log('USSAV congestion unavailable; reason: ' . json_encode($response['raw']));
}

PHP notes that will save you time

JavaScript example (Node.js fetch)

This sample calls the same endpoint and extracts congestion, ratio_vs_baseline, and as_of if available.

import fetch from 'node-fetch';

const API_KEY = 'YOUR_API_KEY';
const URL = 'https://freightpulsehq.com/api/v1/port-congestion?port=USSAV';

async function getSavannahCongestion() {
const res = await fetch(URL, {
headers: {
'X-API-Key': API_KEY,
'Accept': 'application/json'
},
timeout: 20000
});

const status = res.status;
let json;
try {
json = await res.json();
} catch (e) {
return {
available: false,
observations: [],
raw: { error: 'invalid_json', status, body: await res.text() }
};
}

const data = json?.data || {};
const available = Boolean(data.available);
const results = Array.isArray(data.results) ? data.results : [];

const observations = results.map(row => ({
congestion: typeof row.congestion === 'number' ? row.congestion : null,
ratio_vs_baseline: typeof row.ratio_vs_baseline === 'number' ? row.ratio_vs_baseline : null,
as_of: typeof row.as_of === 'string' ? row.as_of : null
}));

return { available, observations, raw: json };
}

getSavannahCongestion()
.then(payload => {
if (payload.available && payload.observations.length > 0) {
const latest = payload.observations[0];
console.log('USSAV congestion:', latest);
} else {
console.warn('USSAV congestion unavailable. Details:', payload.raw?.data?.reason || 'n/a');
}
})
.catch(err => {
console.error('Request failed:', err);
});

Interpreting the dataset and building a UX that won’t mislead

Because this endpoint uses IMF PortWatch with about a 6–7 day lag and a 7‑day average vs 90‑day baseline, avoid labeling any value as “live queue” or “today’s wait time.” If you show a “last updated” label in your UI, use the as_of timestamp to communicate recency when present. If data is unavailable for Savannah, present a neutral badge and explain that the official series is not published for this port at the moment.

Do not quote or imply a USD freight rate as today’s price in connection with this congestion endpoint. It is not a rate API, and congestion is not a fare.

Error handling patterns

Handle three classes of outcomes distinctly:

The meta.request_id can be stored with your logs to help support trace calls if something looks off. Avoid throwing exceptions on available=false; it’s a valid state.

Caching, refresh cadence, and timezones

Given the 6–7 day lag and 7‑day averaging, a refresh interval of several hours is typically sufficient. If you run a high‑throughput service, introduce a shared cache layer keyed by the port code USSAV to keep your request volume low. Guests without a key should be aware of the 100 requests/day per IP cap; aggressive polling will hit that ceiling quickly.

The as_of field is a timestamp string. Treat it as an authoritative observation time for display. If you need precise formatting or timezone confirmation, consult the Documentation and standardize to UTC in your app.

Testing and observability

Start by issuing the official cURL shown above to confirm connectivity and header handling. Record the meta.request_id from sample responses in your logs. In integration tests, simulate three paths: a transport failure, an available=false response with empty results, and a populated results array you control via mocks. Your downstream code should be tolerant of missing fields and unexpected nulls.

Production checklists for Savannah (USSAV)

Frequently asked questions

Q: Why is results empty for Savannah right now?
A: The official USSAV series may not be currently available from IMF PortWatch, or the API could not retrieve it. Inspect data.available and data.reason, then retry later.

Q: What do congestion and ratio_vs_baseline represent?
A: They are derived from IMF PortWatch: a 7‑day average compared to a 90‑day baseline. Use ratio_vs_baseline to indicate relative pressure against recent history. Check the docs for precise definitions and units.

Q: How often should I refresh USSAV congestion?
A: Because the series is lagged by ~6–7 days, refreshing several times per day is sufficient. Cache responses to stay within plan limits and reduce latency.

Q: Can I use this to quote a current freight rate?
A: No. This endpoint reports congestion indicators, not freight rates. Do not quote a USD fare as today’s price based on this feed.

Q: What’s the minimum I must handle in code?
A: Check the envelope (success and data), gate on data.available, handle empty results safely, and parse congestion, ratio_vs_baseline, and as_of when present. Log meta.request_id for support.

Next step

Set up your key and ship the integration. Create an account via Register, then review the Documentation for any field clarifications before moving to production.