You need a reliable way to check vessel traffic congestion at the Port of Savannah (USSAV) and turn it into code you can ship. By the end of this guide, you’ll know how to call the FreightPulse traffic endpoint for Savannah, authenticate properly, parse the JSON envelope, interpret lagged IMF PortWatch metrics, and wire the results into Python jobs with caching and alerts.
What this congestion endpoint tells you (and what it does not)
The congestion data for Savannah is derived from IMF PortWatch, computed as a 7-day average of port calls versus a 90-day baseline for the same port. It is updated daily but lagged by roughly 6–7 days; it is not a live AIS wait-time feed. The JSON includes a congestion label (low, moderate, high), an as_of date, and a metrics block with a ratio_vs_baseline you can use for thresholding.
Key fields to read from the envelope:
- success: Boolean indicating API success at the envelope level.
- data.results[0].congestion: Congestion label for Savannah (low, moderate, high).
- data.results[0].as_of: Date (YYYY-MM-DD) for which IMF PortWatch estimates apply; expect a ~6–7 day lag.
- data.results[0].metrics.ratio_vs_baseline: Recent 7-day average port calls divided by a 90-day baseline; unitless ratio around 1.0.
Time and units:
- as_of is a date (no timezone offset) for the daily observation; IMF PortWatch aggregates by day.
- Calls per day values are counts (floating averages) and the ratio is a dimensionless scalar.
- Expect daily updates; do not treat records as real-time AIS wait or anchorage times.
Authentication and basic access policy
The traffic endpoint requires an API key via the X-API-Key header. Guests without a key are limited to 100 requests per day per IP. All responses are wrapped in an envelope {"success": true, "data": {...}}; handle the envelope before diving into fields.
- Header: X-API-Key: YOUR_API_KEY
- Endpoint: GET /api/v1/port-congestion?port=USSAV
- Base URL: https://freightpulsehq.com
If you need a key, use the Register link once you finish testing the cURL below.
Quick test with curl for Savannah
Use this request exactly as shown to validate connectivity and your key. It returns the Savannah (USSAV) congestion state in a success envelope.
curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/port-congestion?port=USSAV"
Here is the official sample JSON for USSAV, which also demonstrates the envelope, the results array, the congestion label, the date, and the IMF PortWatch meta. Copy these values when you need a canonical example or a contract test.
{
"success": true,
"data": {
"results": [
{
"port": "Savannah",
"locode": "US SAV",
"country": "United States",
"congestion": "moderate",
"as_of": "2026-10-16",
"metrics": {
"recent_avg_portcalls_per_day": 5.6,
"baseline_avg_portcalls_per_day": 5.8,
"ratio_vs_baseline": 0.96
}
}
],
"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": "8e3b1f47-00bf-427a-a558-12afdcd3c0da"
}
}
Fields you will actually use in code:
- data.results[0].congestion: For user-facing status and basic alerting.
- data.results[0].metrics.ratio_vs_baseline: For quantitative thresholds and trend comparisons.
- data.results[0].as_of: For freshness checks and backfill logic.
Python: fetch Savannah congestion, parse fields, and cache
This Python example calls the same endpoint, reads the envelope, extracts congestion, as_of, and ratio_vs_baseline for USSAV, and demonstrates a lightweight cache layer to avoid needlessly re-fetching lagged daily data.
import os
import json
import time
import hashlib
import requests
from datetime import datetime, timedelta
API_URL = "https://freightpulsehq.com/api/v1/port-congestion"
PORT_CODE = "USSAV"
API_KEY = os.getenv("FREIGHTPULSE_API_KEY", "YOUR_API_KEY")
# Simple file cache keyed by (URL + params), expiring after 12 hours.
CACHE_TTL_SECONDS = 12 * 3600
CACHE_DIR = ".cache_freightpulse"
def _cache_key(url, params):
key_str = url + "?" + "&".join(f"{k}={v}" for k, v in sorted(params.items()))
return hashlib.sha256(key_str.encode("utf-8")).hexdigest()
def _cache_path(key):
if not os.path.isdir(CACHE_DIR):
os.makedirs(CACHE_DIR, exist_ok=True)
return os.path.join(CACHE_DIR, key + ".json")
def cached_get(url, params, headers):
key = _cache_key(url, params)
path = _cache_path(key)
now = time.time()
# Serve from cache if fresh
if os.path.isfile(path):
mtime = os.path.getmtime(path)
if now - mtime < CACHE_TTL_SECONDS:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
# Fetch from API
resp = requests.get(url, params=params, headers=headers, timeout=20)
resp.raise_for_status()
payload = resp.json()
# Basic envelope validation
if not isinstance(payload, dict) or not payload.get("success"):
raise RuntimeError(f"Unexpected API envelope: {payload}")
# Save to cache
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f)
return payload
def fetch_savannah_congestion():
params = {"port": PORT_CODE}
headers = {"X-API-Key": API_KEY}
payload = cached_get(API_URL, params, headers)
results = payload.get("data", {}).get("results", [])
if not results:
raise RuntimeError("No results returned for USSAV")
r0 = results[0]
congestion = r0.get("congestion")
as_of = r0.get("as_of")
ratio = r0.get("metrics", {}).get("ratio_vs_baseline")
# Convert date string to date for scheduling/window logic
as_of_date = datetime.strptime(as_of, "%Y-%m-%d").date()
return {
"congestion": congestion,
"as_of": as_of_date,
"ratio_vs_baseline": ratio,
"raw": payload
}
if __name__ == "__main__":
data = fetch_savannah_congestion()
# Example thresholds: moderate/high alerts around ratio < 0.9 or > 1.1 if your ops prefer deviation bands.
ratio = data["ratio_vs_baseline"]
status = data["congestion"]
as_of = data["as_of"]
print(f"Savannah congestion: {status} (ratio_vs_baseline={ratio}, as_of={as_of})")
# Alerting stub
if ratio is not None:
if ratio < 0.9:
print("Alert: Below baseline traffic. Consider revising berth allocation or yard staffing.")
elif ratio > 1.1:
print("Alert: Above baseline traffic. Prepare for gate throughput or drayage peaks.")
Notes:
- Set FREIGHTPULSE_API_KEY in your environment to avoid hardcoding. For smoke tests, YOUR_API_KEY will work if you paste your key there.
- Cache TTL of 12 hours is usually safe because the series is updated daily and lags ~6–7 days; excessive polling adds no value.
- Timeouts and envelope checks help surface transient network or schema issues early.
Handling lag, scheduling, and data hygiene
Because the IMF PortWatch feed is lagged by ~6–7 days, treat the data as a historical daily indicator for traffic conditions, not a live queue length. Best practice is to schedule a single fetch per day after your ETL window (e.g., 06:00 UTC), then cache and reuse the values across dashboards and reports.
Recommended operational approach:
- Fetch once per day; persist the envelope as raw JSON along with parsed fields.
- Validate as_of is not in the future; it should trail present day by several days.
- Backfill gracefully if a given day is missed; ratios are daily aggregates and won’t change intraday.
- Do not attempt to infer wait times at anchor or berth delays; those are explicitly out of scope.
Additional JSON examples you can test against
The following examples use the same endpoint and field structure as the official sample. They illustrate how labels and ratios may vary across days for Savannah’s port traffic. These examples are complete envelopes so you can plug them into contract tests or mocks.
{
"success": true,
"data": {
"results": [
{
"port": "Savannah",
"locode": "US SAV",
"country": "United States",
"congestion": "low",
"as_of": "2026-10-09",
"metrics": {
"recent_avg_portcalls_per_day": 5.9,
"baseline_avg_portcalls_per_day": 5.8,
"ratio_vs_baseline": 1.02
}
}
],
"global_summary": {
"ports_high_congestion": 0,
"ports_moderate": 0,
"ports_low": 1
},
"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": "1c7a6b20-61e5-4a9d-a0d3-0f3b6a601234"
}
}
{
"success": true,
"data": {
"results": [
{
"port": "Savannah",
"locode": "US SAV",
"country": "United States",
"congestion": "high",
"as_of": "2026-10-13",
"metrics": {
"recent_avg_portcalls_per_day": 7.1,
"baseline_avg_portcalls_per_day": 5.8,
"ratio_vs_baseline": 1.22
}
}
],
"global_summary": {
"ports_high_congestion": 1,
"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": "2ab829de-b4a8-4b77-8f71-2a8e36c6abcd"
}
}
{
"success": true,
"data": {
"results": [
{
"port": "Savannah",
"locode": "US SAV",
"country": "United States",
"congestion": "moderate",
"as_of": "2026-10-15",
"metrics": {
"recent_avg_portcalls_per_day": 5.4,
"baseline_avg_portcalls_per_day": 5.8,
"ratio_vs_baseline": 0.93
}
}
],
"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": "7f98c912-003c-41d5-9e7f-67bce912efef"
}
}
Interpreting congestion and ratio_vs_baseline for traffic decisions
If your objective is to steer traffic operations, ratio_vs_baseline is the primary quantitative signal. A ratio near 1.0 implies typical recent traffic flows relative to the 90-day lookback; values above 1.1 suggest above-baseline port calls, and below 0.9 suggest below-baseline activity. The categorical congestion field is a human-friendly shorthand of the same relationship.
Because the series is lagged and daily, treat alerts as trend flags rather than immediate operational commands. For example, a run of high ratios over several consecutive days could warrant forward planning for gate throughput or yard equipment assignment, while a run of low ratios could relax berth allocations.
Resilience patterns: rate limits, retries, and schema checks
Guests without a key can issue up to 100 requests/day per IP. With a key, keep calls idempotent and infrequent (daily) because the underlying data is daily and lagged. For resilience, use exponential backoff on 5xx network errors and validate the envelope’s success flag on every call.
Schema checks that save debugging time:
- Assert that data.results is a non-empty list before indexing results[0].
- Guard optional metric fields: ratio_vs_baseline and averages can be None for days without sufficient history.
- Compare as_of to today; if it is more than ~10 days behind, treat as a stale snapshot and surface a warning in your UI.
Using the Documentation and rolling into production
When you’re ready to wire this into a worker, job scheduler, or dashboard, keep your contract tests pinned to the envelope format and field names shown above. For additional endpoint context, see the product Documentation. This post focuses on Savannah traffic (USSAV) via GET /api/v1/port-congestion?port=USSAV.
JavaScript fetch example (optional front-end check)
If you need a browser or Node.js smoke test, this snippet fetches Savannah congestion and extracts the same fields. Use server-side keys or a proxy in production.
async function fetchSavannahCongestion() {
const url = "https://freightpulsehq.com/api/v1/port-congestion?port=USSAV";
const res = await fetch(url, {
headers: { "X-API-Key": "YOUR_API_KEY" }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const payload = await res.json();
if (!payload.success) throw new Error("API envelope indicates failure");
const r0 = payload.data?.results?.[0];
if (!r0) throw new Error("No results for USSAV");
const out = {
congestion: r0.congestion,
as_of: r0.as_of,
ratio_vs_baseline: r0.metrics?.ratio_vs_baseline
};
return out;
}
fetchSavannahCongestion()
.then(d => console.log(`Savannah: ${d.congestion} (ratio=${d.ratio_vs_baseline}, as_of=${d.as_of})`))
.catch(err => console.error("Error fetching Savannah congestion:", err));
Operational checklist for Savannah traffic integrations
- Authenticate with X-API-Key and wrap access in a daily job; cache JSON locally.
- Parse envelope first; never assume data.results[0] exists without checking.
- Use ratio_vs_baseline for thresholds; present congestion as a friendly label.
- Surface as_of prominently in your UI to avoid misinterpretation as live AIS.
- Document your alert rules (e.g., ratio >= 1.15 → “elevated traffic” notice) and revisit them quarterly.
FAQ
Is this a live AIS queue or wait-time metric?
No. FreightPulse uses IMF PortWatch daily aggregates with ~6–7 day lag. It approximates congestion via recent port call activity versus a 90-day baseline and does not measure wait times or anchorage durations.
What’s the recommended polling frequency?
Daily. The dataset is daily and lagged; polling more often will not produce fresher values and only consumes quota.
How should I handle missing or null ratio_vs_baseline?
Guard for nulls and either suppress alerts for that day or carry forward the last known good value with a “stale” banner. Always display as_of so users understand the snapshot date.
What’s the authentication model and guest access?
Provide your key via X-API-Key. Guests without a key are limited to 100 requests per day per IP. For production, register for a key and store it securely.
Can I use these values to set freight rate quotes?
Do not quote a USD freight rate as today’s price from this endpoint. It reflects traffic congestion signals, lagged by several days, and is not a live price feed.
Ready to integrate Savannah traffic congestion into your scheduler or dashboard? Get your API key via Register, review the field contracts in the Documentation, and ship your first GET /api/v1/port-congestion?port=USSAV call today.